Skip to content
Draft
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 packages/base/Skill/catalog-listing.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"data": {
"type": "card",
"attributes": {
"instructions": "Before running the operation, ensure the following conditions are met:\n\n1. The create, use, install, and remix commands must exist and be callable.\n2. Except create, the user must provide all of the following:\n- A valid Realm URL\n- A Listing Card\n- An Action Type: \"use\", \"install\", or \"remix\"\n\n3. To create, user must attach at least one card instance to create the listing.\n\nIf any value is missing, prompt the user to provide the missing input(s). If all inputs are available, proceed automatically without asking for confirmation.\n\nBased on the action type:\n- If actionType === \"create\" → run the create command\n- If actionType === \"use\" → run the use command\n- If actionType === \"install\" → run the install command\n- If actionType === \"remix\" → run the remix command\n\nIf actionType === \"create\" → run create with following payload(s):\n - Open card id: [attached instance URL]\n\n Else, use the following inputs to run the command:\n- Realm URL: [user input]\n- Listing: [user input]\n\nIf actionType is remix:\n- After running remix, also run remix code to generate two example prompts.\n- Respond with:\n 1. A confirmation message summarizing the remix operation.\n 2. A follow-up message with two listing-related remix prompts.If specific prompts can't be generated, provide two general suggestions (e.g., \"Change to dark theme\", \"Convert to minimalist layout).",
"instructions": "Before running the operation, ensure the following conditions are met:\n\n1. The create, use, install, and remix commands must exist and be callable.\n2. Except create, the user must provide all of the following:\n- A valid Realm URL\n- A Listing Card\n- An Action Type: \"use\", \"install\", or \"remix\"\n\n3. To create, user must attach at least one card instance to create the listing.\n\nIf any value is missing, prompt the user to provide the missing input(s). If all inputs are available, proceed automatically without asking for confirmation.\n\nBased on the action type:\n- If actionType === \"create\" → run the create command\n- If actionType === \"use\" → run the use command\n- If actionType === \"install\" → run the install command\n- If actionType === \"remix\" → run the remix command\n\nIf actionType === \"create\" → run create with following payload(s):\n - Open card id: [attached instance URL]\n - Supporting card ids (optional): attached cards that should install with the listing but not appear on the listing detail page (e.g. data cards a query-based example depends on). Attached cards that are not instances of the listed type are treated as supporting cards automatically.\n\n Else, use the following inputs to run the command:\n- Realm URL: [user input]\n- Listing: [user input]\n\nIf actionType is remix:\n- After running remix, also run remix code to generate two example prompts.\n- Respond with:\n 1. A confirmation message summarizing the remix operation.\n 2. A follow-up message with two listing-related remix prompts. If specific prompts can't be generated, provide two general suggestions (e.g., \"Change to dark theme\", \"Convert to minimalist layout\").",
"commands": [
{
"codeRef": {
Expand Down
2 changes: 2 additions & 0 deletions packages/base/command.gts
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,8 @@ export class RetrySubmissionWorkflowInput extends CardDef {

export class ListingCreateInput extends CardDef {
@field openCardIds = containsMany(StringField);
// installed with the listing but not shown in the listing detail view
@field supportingCardIds = containsMany(StringField);
@field codeRef = contains(CodeRefField);
@field targetRealm = contains(RealmField);
}
Expand Down
190 changes: 151 additions & 39 deletions packages/host/app/components/operator-mode/create-listing-modal.gts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { IconX, IconPlus } from '@cardstack/boxel-ui/icons';

import {
baseCardRef,
chooseCard,
isResolvedCodeRef,
rri,
Expand Down Expand Up @@ -49,6 +50,7 @@ export default class CreateListingModal extends Component<Signature> {
@service declare private realm: RealmService;

@tracked private _selectedExampleURLs: string[] | null = null;
@tracked private _selectedSupportingCardURLs: string[] | null = null;

private get payload() {
return this.operatorModeStateService.createListingModalPayload;
Expand Down Expand Up @@ -91,35 +93,38 @@ export default class CreateListingModal extends Component<Signature> {
return (this.payload?.declarationKind ?? 'card') === 'card';
}

private get selectedExampleCardUrls(): string[] {
return this.selectedExampleURLs.map((url) =>
url.endsWith('.json') ? url : `${url}.json`,
private get selectedSupportingCardURLs(): string[] {
return (
this._selectedSupportingCardURLs ?? this.payload?.supportingCardIds ?? []
);
}
Comment on lines +96 to +100

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve preselected supporting card ids

When a caller provides the newly added ListingCreateInput.supportingCardIds to the create-listing modal flow, those IDs are accepted by the input schema but never seed this selection; OpenCreateListingModalTool.run still only forwards openCardIds into createListingModalPayload, and the create action reads this getter before submitting to catalog. In AI/tool-driven create flows that attach data cards as supporting cards, the modal therefore submits supportingCardIds: [] unless the user manually reselects them, so the dependent cards are omitted from the listing install set.

Useful? React with 👍 / 👎.

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 38fa58f — OpenCreateListingModalTool now forwards supportingCardIds into the modal payload, and the modal seeds its selection from it (same pattern as openCardIds). Added a tool test for the payload pass-through and a modal test that a seeded supporting card renders with its remove button.


private get selectedExampleRealms(): string[] {
let realms = this.selectedExampleURLs.flatMap((cardUrl) => {
private get hasSelectedSupportingCards(): boolean {
return this.selectedSupportingCardURLs.length > 0;
}

// The `entry` query for a row's selected cards: scoped to the chosen card
// URLs (matched by index file URL, hence the `.json` suffixing) and their
// realms, with the `atom` rendering bound through the filter's `htmlQuery`
// (the way to select a prerendered format). The engine lifts the htmlQuery
// binding out of the eq (which then dissolves). `scope: 'cards'` pins the
// instance row, dropping each card's dual-indexed `.json` file row that
// shares its `cardUrls` URL.
private atomSearchQuery(cardURLs: string[]): SearchEntryWireQuery {
let cardUrls = cardURLs.map((url) =>
url.endsWith('.json') ? url : `${url}.json`,
);
let realms = cardURLs.flatMap((cardUrl) => {
try {
let realmURL = this.realm.realmOf(rri(cardUrl));
return realmURL ? [realmURL] : [];
} catch (_error) {
return [];
}
});
return [...new Set(realms)];
}

// The `entry` query for the selected example cards: scoped to the
// chosen card URLs (matched by index file URL, hence the `.json`-suffixed
// `selectedExampleCardUrls`) and their realms, with the `atom` rendering
// bound through the filter's `htmlQuery` (the way to select a prerendered
// format). The engine lifts the htmlQuery binding out of the eq (which then
// dissolves). `scope: 'cards'` pins the instance row, dropping each example's
// dual-indexed `.json` file row that shares its `cardUrls` URL.
private get exampleSearchQuery(): SearchEntryWireQuery {
return {
cardUrls: this.selectedExampleCardUrls,
realms: this.selectedExampleRealms,
cardUrls,
realms: [...new Set(realms)],
scope: 'cards',
filter: {
eq: {
Expand All @@ -129,6 +134,14 @@ export default class CreateListingModal extends Component<Signature> {
};
}

private get exampleSearchQuery(): SearchEntryWireQuery {
return this.atomSearchQuery(this.selectedExampleURLs);
}

private get supportingCardSearchQuery(): SearchEntryWireQuery {
return this.atomSearchQuery(this.selectedSupportingCardURLs);
}

private chooseExamples = task(async () => {
let codeRef = this.codeRef;
if (!codeRef) {
Expand Down Expand Up @@ -158,6 +171,31 @@ export default class CreateListingModal extends Component<Signature> {
);
}

private chooseSupportingCards = task(async () => {
let consumingRealm = this.payload?.targetRealm
? new URL(this.payload.targetRealm)
: undefined;
let selected = await chooseCard(
{ filter: { type: baseCardRef } },
{
multiSelect: true,
consumingRealm,
preselectConsumingRealm: true,
preselectedCardUrls: this.selectedSupportingCardURLs,
},
);
if (selected) {
this._selectedSupportingCardURLs = selected;
}
});

@action private removeSelectedSupportingCard(urlToRemove: string) {
let normalizedUrlToRemove = removeCardJsonExtension(urlToRemove);
this._selectedSupportingCardURLs = this.selectedSupportingCardURLs.filter(
(url) => removeCardJsonExtension(url) !== normalizedUrlToRemove,
);
}

private createListing = task(async () => {
let payload = this.payload;
if (!payload) {
Expand All @@ -171,6 +209,7 @@ export default class CreateListingModal extends Component<Signature> {

let targetRealm = payload.targetRealm;
let openCardIds = this.selectedExampleURLs;
let supportingCardIds = this.selectedSupportingCardURLs;

// Load listing-create from the catalog realm at runtime so we pick up
// whatever version is shipped in the realm content, not a stale
Expand All @@ -191,6 +230,7 @@ export default class CreateListingModal extends Component<Signature> {
codeRef: ResolvedCodeRef;
targetRealm: string;
openCardIds: string[];
supportingCardIds: string[];
}) => Promise<{
listing?: { id?: string };
backgroundWork?: Promise<unknown>;
Expand All @@ -205,6 +245,7 @@ export default class CreateListingModal extends Component<Signature> {
codeRef,
targetRealm,
openCardIds,
supportingCardIds,
});

// Navigate to the listing in code mode with isolated preview
Expand All @@ -228,11 +269,13 @@ export default class CreateListingModal extends Component<Signature> {
}

this._selectedExampleURLs = null;
this._selectedSupportingCardURLs = null;
this.operatorModeStateService.dismissCreateListingModal();
});

@action private onClose() {
this._selectedExampleURLs = null;
this._selectedSupportingCardURLs = null;
this.operatorModeStateService.dismissCreateListingModal();
}

Expand Down Expand Up @@ -280,29 +323,26 @@ export default class CreateListingModal extends Component<Signature> {
data-test-create-listing-examples
>
{{#if this.hasSelectedExamples}}
<div
class='selected-examples-list'
data-test-selected-examples
>
<div class='selected-cards-list' data-test-selected-examples>
<SearchResults
@query={{this.exampleSearchQuery}}
@mode='none'
as |results|
>
{{#if results.isLoading}}
<div class='selected-example-loading'>
<div class='selected-card-loading'>
<LoadingIndicator />
</div>
{{else}}
{{#each results.entries key='id' as |entry|}}
<div
class='selected-example-atom'
class='selected-card-atom'
data-test-selected-example={{entry.id}}
data-test-card-format='atom'
>
<entry.component />
<IconButton
class='selected-example-remove-button'
class='selected-card-remove-button'
@icon={{IconX}}
@height='10'
@width='10'
Expand All @@ -319,10 +359,10 @@ export default class CreateListingModal extends Component<Signature> {
</SearchResults>
</div>
{{else}}
<span class='no-examples-message'>No examples selected</span>
<span class='no-cards-message'>No examples selected</span>
{{/if}}
<Button
class='add-examples-button'
class='add-cards-button'
@size='small'
@kind='muted'
@loading={{this.chooseExamples.isRunning}}
Expand All @@ -334,6 +374,73 @@ export default class CreateListingModal extends Component<Signature> {
</Button>
</div>
</FieldContainer>

<FieldContainer
@label='Supporting Cards'
class='field examples-field-container'
>
<div
class='field-contents examples-field'
data-test-create-listing-supporting-cards
>
<p class='field-hint'>
Installed with the listing but not shown on the listing page.
</p>
{{#if this.hasSelectedSupportingCards}}
<div
class='selected-cards-list'
data-test-selected-supporting-cards
>
<SearchResults
@query={{this.supportingCardSearchQuery}}
@mode='none'
as |results|
>
{{#if results.isLoading}}
<div class='selected-card-loading'>
<LoadingIndicator />
</div>
{{else}}
{{#each results.entries key='id' as |entry|}}
<div
class='selected-card-atom'
data-test-selected-supporting-card={{entry.id}}
data-test-card-format='atom'
>
<entry.component />
<IconButton
class='selected-card-remove-button'
@icon={{IconX}}
@height='10'
@width='10'
aria-label='Remove supporting card'
{{on
'click'
(fn this.removeSelectedSupportingCard entry.id)
}}
data-test-selected-supporting-card-remove={{entry.id}}
/>
</div>
{{/each}}
{{/if}}
</SearchResults>
</div>
{{else}}
<span class='no-cards-message'>No supporting cards selected</span>
{{/if}}
<Button
class='add-cards-button'
@size='small'
@kind='muted'
@loading={{this.chooseSupportingCards.isRunning}}
{{on 'click' (perform this.chooseSupportingCards)}}
data-test-choose-supporting-cards-button
>
<IconPlus width='12px' height='12px' />
Add Supporting Cards
</Button>
</div>
</FieldContainer>
{{/if}}
</:content>
<:footer>
Expand Down Expand Up @@ -395,12 +502,17 @@ export default class CreateListingModal extends Component<Signature> {
flex-direction: column;
gap: var(--boxel-sp-xs);
}
.add-examples-button {
.field-hint {
font: var(--boxel-font-xs);
color: var(--boxel-450);
margin: 0;
}
.add-cards-button {
--boxel-button-padding: var(--boxel-sp-xs) var(--boxel-sp-sm);
align-self: flex-start;
gap: var(--boxel-sp-xxxs);
}
.no-examples-message {
.no-cards-message {
font: var(--boxel-font-sm);
color: var(--boxel-500);
display: flex;
Expand All @@ -412,7 +524,7 @@ export default class CreateListingModal extends Component<Signature> {
min-height: 3rem;
width: 100%;
}
.selected-examples-list {
.selected-cards-list {
display: flex;
flex-wrap: wrap;
gap: var(--boxel-sp-xs);
Expand All @@ -423,28 +535,28 @@ export default class CreateListingModal extends Component<Signature> {
width: 100%;
align-items: start;
}
.selected-example-atom {
.selected-card-atom {
position: relative;
min-width: 0;
display: inline-flex;
align-items: center;
}
.selected-example-atom
.selected-card-atom
:deep(.field-component-card.atom-format.display-container-true) {
min-width: 0;
}
.selected-example-atom
.selected-card-atom
:deep(.field-component-card.atom-format.display-container-true) {
padding-right: calc(
var(--boxel-sp-xs) + var(--boxel-icon-sm) + var(--boxel-sp-6xs)
);
}
.selected-example-atom :deep(.card) {
.selected-card-atom :deep(.card) {
border: none;
box-shadow: none;
background: transparent;
}
.selected-example-remove-button {
.selected-card-remove-button {
--icon-color: var(--boxel-700);
--icon-bg: transparent;
--icon-border: transparent;
Expand All @@ -457,14 +569,14 @@ export default class CreateListingModal extends Component<Signature> {
border-radius: 999px;
opacity: 0.72;
}
.selected-example-remove-button:hover,
.selected-example-remove-button:focus-visible {
.selected-card-remove-button:hover,
.selected-card-remove-button:focus-visible {
--icon-bg: var(--boxel-200);
--icon-border: var(--boxel-200);
--icon-color: var(--boxel-900);
opacity: 1;
}
.selected-example-loading {
.selected-card-loading {
display: inline-flex;
align-items: center;
justify-content: center;
Expand All @@ -473,7 +585,7 @@ export default class CreateListingModal extends Component<Signature> {
width: 100%;
}
:deep(.create-listing) {
height: 30rem;
height: 34rem;
}
.footer-loading-message {
font: var(--boxel-font-sm);
Expand Down
1 change: 1 addition & 0 deletions packages/host/app/services/operator-mode-state-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export interface CreateListingModalPayload {
codeRef: CodeRef;
targetRealm: string;
openCardIds?: RealmResourceIdentifier[];
supportingCardIds?: RealmResourceIdentifier[];
declarationKind: 'card' | 'field';
}

Expand Down
Loading
Loading