Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/skills/indexing-diagnostics/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -2743,7 +2743,7 @@ The per-realm reindex endpoints (`POST <realm>_reindex`, `POST <realm>_full-rein
-d '{ "user": "@user:server", "realms": ["<realm-url>/"] }'
```

The response carries a map of `{ <realm-url>: <realm-JWT> }`. **That** is the token you pass on `Authorization` when calling the reindex endpoint in the next section. It's _not_ the token the prerender tab uses — that one comes from the `prerenderer-reproduce` log.
The response carries a map of `{ <realm-url>: <realm-JWT> }`, covering the realms the user has a permission row of their own for. Add `?include-public=true` to the URL to also get tokens for realms only world-readable to them. **That** is the token you pass on `Authorization` when calling the reindex endpoint in the next section. It's _not_ the token the prerender tab uses — that one comes from the `prerenderer-reproduce` log.

Three different JWTs float around in this area, so always be explicit about which one you mean:

Expand Down
2 changes: 1 addition & 1 deletion packages/boxel-cli/src/commands/realm/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export async function listRealms(

let realmServerUrl = active.profile.realmServerUrl.replace(/\/$/, '');
let response = await pm.authedRealmServerFetch(
`${realmServerUrl}/_realm-auth`,
`${realmServerUrl}/_realm-auth?include-public=true`,
{
method: 'POST',
headers: {
Expand Down
4 changes: 3 additions & 1 deletion packages/boxel-cli/src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,9 @@ export async function getRealmTokens(
realmServerUrl: string,
serverToken: string,
): Promise<RealmTokens> {
let url = `${realmServerUrl.replace(/\/$/, '')}/_realm-auth`;
// `include-public=true`: a token for every realm the user may read, public
// ones included, since authed realm fetches resolve their token from this.
let url = `${realmServerUrl.replace(/\/$/, '')}/_realm-auth?include-public=true`;

let response = await fetch(url, {
method: 'POST',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,30 @@ import type { Icon } from '@cardstack/boxel-ui/icons';
import { logger, ri } from '@cardstack/runtime-common';

import config from '@cardstack/host/config/environment';
import { catalogRealmURL } from '@cardstack/host/lib/utils';
import type MatrixService from '@cardstack/host/services/matrix-service';
import type RealmService from '@cardstack/host/services/realm';
import type RealmServerService from '@cardstack/host/services/realm-server';

import AddWorkspace from './add-workspace';
import ArchivedWorkspace from './archived-workspace';
import ManageCatalogs from './manage-catalogs';
import Workspace from './workspace';
import WorkspaceLoadingIndicator from './workspace-loading-indicator';

// Moves `first` to the front of `urls` when it's there, keeping the rest in
// order.
export function leadWith<T extends string>(
urls: T[],
first: string | null,
): T[] {
let index = first ? urls.indexOf(first as T) : -1;
if (index <= 0) {
return urls;
}
return [urls[index], ...urls.slice(0, index), ...urls.slice(index + 1)];
}

interface SortOption {
label: string;
icon: Icon;
Expand Down Expand Up @@ -152,13 +167,6 @@ export default class WorkspaceChooser extends Component<Signature> {
return this.selectedSortOption.value;
}

private get displayCatalogWorkspaces() {
return (
this.realmServer.displayedCatalogRealmIdentifiers &&
this.realmServer.displayedCatalogRealmIdentifiers.length > 0
);
}

private get communityRealmIdentifiers() {
let realmURLs = this.realmServer.displayedCatalogRealmIdentifiers ?? [];
if (config.environment !== 'production') {
Expand Down Expand Up @@ -212,9 +220,13 @@ export default class WorkspaceChooser extends Component<Signature> {
return this.filterByHosted(this.realmServer.userRealmIdentifiers);
}

// Newest first, except the Boxel catalog, which always leads the section.
private get filteredCatalogRealmIdentifiers() {
return this.filterByHosted(
this.sortByCreatedAtDesc(this.communityRealmIdentifiers),
leadWith(
this.sortByCreatedAtDesc(this.communityRealmIdentifiers),
catalogRealmURL,
),
);
}

Expand Down Expand Up @@ -262,13 +274,12 @@ export default class WorkspaceChooser extends Component<Signature> {
}

private get catalogEmptyMessage(): string | null {
if (
this.sortOrder === 'hosted-only' &&
this.filteredCatalogRealmIdentifiers.length === 0
) {
return 'No matching results';
if (this.filteredCatalogRealmIdentifiers.length > 0) {
return null;
}
return null;
return this.sortOrder === 'hosted-only'
? 'No matching results'
: 'No catalogs yet. Add public realms with the gear.';
}

private get favoritesEmptyMessage(): string | null {
Expand Down Expand Up @@ -386,7 +397,7 @@ export default class WorkspaceChooser extends Component<Signature> {
}

private get renderedCatalogCount() {
if (!this.displayCatalogWorkspaces || this.catalogEmptyMessage) {
if (this.catalogEmptyMessage) {
return 0;
}
return this.filteredCatalogRealmIdentifiers.length;
Expand Down Expand Up @@ -695,36 +706,35 @@ export default class WorkspaceChooser extends Component<Signature> {
</div>
{{/if}}
</div>
{{#if this.displayCatalogWorkspaces}}
<div class='workspace-section'>
<div class='section-header'>
<IconGlobe width='20' height='20' class='section-header-icon' />
<span class='workspace-chooser__title'>Catalogs</span>
</div>
{{#if this.catalogEmptyMessage}}
<span
class='section-empty'
data-test-catalog-empty
>{{this.catalogEmptyMessage}}</span>
{{else}}
<div class='workspace-list' data-test-catalog-list>
{{#each
this.filteredCatalogRealmIdentifiers
as |realmIdentifier i|
}}
{{#let (add this.catalogNavBase i) as |navIndex|}}
<Workspace
@realmIdentifier={{realmIdentifier}}
@navIndex={{navIndex}}
@isSelected={{eq this.currentIndex navIndex}}
@selectionActive={{this.isSelectionActive}}
/>
{{/let}}
{{/each}}
</div>
{{/if}}
<div class='workspace-section'>
<div class='section-header'>
<IconGlobe width='20' height='20' class='section-header-icon' />
<span class='workspace-chooser__title'>Catalogs</span>
<ManageCatalogs />
</div>
{{/if}}
{{#if this.catalogEmptyMessage}}
<span
class='section-empty'
data-test-catalog-empty
>{{this.catalogEmptyMessage}}</span>
{{else}}
<div class='workspace-list' data-test-catalog-list>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Empty section: the Catalogs section now always renders, but with no catalogs and no picks it's a bare heading over an empty list. It needs an empty-state line that points at the gear.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Claude Code 🤖] Fixed in da52d31. With nothing to show, the section reads "No catalogs yet. Add public realms with the gear." The hosted-only filter keeps its "No matching results". A new acceptance test covers it.

{{#each
this.filteredCatalogRealmIdentifiers
as |realmIdentifier i|
}}
{{#let (add this.catalogNavBase i) as |navIndex|}}
<Workspace
@realmIdentifier={{realmIdentifier}}
@navIndex={{navIndex}}
@isSelected={{eq this.currentIndex navIndex}}
@selectionActive={{this.isSelectionActive}}
/>
{{/let}}
{{/each}}
</div>
{{/if}}
</div>
<div class='workspace-section' data-test-archived-section>
<button
type='button'
Expand Down
Loading
Loading