Skip to content

Cs 10383 create listing should have a loading user experience#4190

Open
lucaslyl wants to merge 23 commits intomainfrom
CS-10383-create-listing-should-have-a-loading-user-experience
Open

Cs 10383 create listing should have a loading user experience#4190
lucaslyl wants to merge 23 commits intomainfrom
CS-10383-create-listing-should-have-a-loading-user-experience

Conversation

@lucaslyl
Copy link
Contributor

@lucaslyl lucaslyl commented Mar 15, 2026

linear: https://linear.app/cardstack/issue/CS-10383/create-listing-should-have-a-loading-user-experience

Summary

  • Add a confirmation modal before creating a listing, allowing users to review target realm, codeRef, and select example instances
  • Show a loading state in the modal while listing creation and background auto-patching run
  • Navigate to the listing in code mode (isolated preview) immediately after the empty listing is created
  • Modal auto-dismisses after all background work (name, summary, tags, specs, etc.) finishes
  • Add isListingDef / isListingInstance to @cardstack/runtime-common for reliable listing type detection
  • Change openCardId (singular) to openCardIds (plural) to support multiple example selection

Architecture

  1. createListingModalPayload lives in OperatorModeStateService
  • The modal is rendered at the top level in operator-mode/container.gts — it needs to be visible across submodes (code mode, interact mode). The state service is the central place that manages cross-submode UI state (similar to workspaceChooserOpened, aiAssistantOpen). Storing the payload there allows any part of the app to trigger the modal via showCreateListingModal(). (example: menu-items, details panel)
  1. OpenCreateListingModalCommand exists as a separate command

The "Create Listing" action can be triggered from multiple places:

  • Detail panel (card instance view)
  • Detail panel (definition view)
  • Menu items / AI assistant

Flow after clicking "Create"

  1. User clicks Create → createListing ember-concurrency task starts, button shows loading spinner + "Creating..."
  2. ListingCreateCommand.execute() → creates an empty listing card in the store, then kicks off backgroundWork (Promise.all of auto-patching name, summary, tags, categories, license, examples, specs) as fire-and-forget
  3. Immediate navigation → switches to Code submode, opens the listing .json in preview with "Isolated" format selected. The listing card is visible behind the modal and updates in real-time as background patches land.
  4. Modal stays open → shows "Setting up your {title} listing. This may take a moment..." in the footer while backgroundWork runs
  5. Auto-dismiss → once all background promises resolve, dismissCreateListingModal() is called and the modal closes. User sees the fully populated listing.

Demo:

Screen.Recording.2026-03-18.at.4.25.29.PM.mp4

@lucaslyl lucaslyl self-assigned this Mar 15, 2026
@github-actions
Copy link

Preview deployments

@github-actions
Copy link

github-actions bot commented Mar 15, 2026

Host Test Results

0 tests   0 ✅  0s ⏱️
0 suites  0 💤
0 files    0 ❌

Results for commit 7853b39.

♻️ This comment has been updated with latest results.

@lucaslyl
Copy link
Contributor Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0606903eb1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

targetRealm,
});
if (this.isCardInstance) {
const openCardId = this.args.readyFile.url;

Choose a reason for hiding this comment

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

P1 Badge Use extensionless card id for listing creation

When the action runs from an instance, openCardId is sourced from readyFile.url, and this path is guaranteed to include .json (isCardInstance checks that). ListingCreateCommand then uses that value as the canonical instance identifier (for examples.0.links.self and in linkSpecs() via _dependencies?url=...), but instance entries are indexed by extensionless ids (for example .../Pet/mango). In this path, passing the file URL causes dependency/spec resolution to miss and can produce listings without the expected linked example/specs; use the instance id (this.args.cardInstance?.id) or strip .json before executing the command.

Useful? React with 👍 / 👎.

@lucaslyl lucaslyl marked this pull request as ready for review March 15, 2026 09:00
@lucaslyl lucaslyl requested a review from tintinthong March 15, 2026 09:01
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e825a478e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +176 to +179
@action private onClose() {
this.errorMessage = undefined;
this.operatorModeStateService.closeCreateListingModal();
}

Choose a reason for hiding this comment

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

P2 Badge Reset source override when closing the listing modal

Closing the modal only clears errorMessage, so _userSelectedKey/_userSelectedValue persist across sessions. If a user opens from an instance, switches Source to the definition, cancels, and then reopens from that same instance, activeOpenCardId will reuse the stale override instead of pre-selecting the triggering instance. That breaks the instance-driven preselection flow and can cause creating a listing from the wrong source when users immediately confirm.

Useful? React with 👍 / 👎.

targetRealm: request.targetRealm,
openCardId: this.activeOpenCardId ?? undefined,
});
this.operatorModeStateService.closeCreateListingModal();

Choose a reason for hiding this comment

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

P2 Badge Prevent stale create task from closing newer modal requests

The create task always calls closeCreateListingModal() after ListingCreateCommand resolves, even if the modal request changed while the task was running. In a long-running create, a user can dismiss and reopen the modal for a different card; when the first task finishes, it clears the newer request and dismisses the new modal unexpectedly.

Useful? React with 👍 / 👎.

@lucaslyl lucaslyl requested a review from a team March 16, 2026 02:07
@lucaslyl lucaslyl marked this pull request as draft March 16, 2026 04:46
@lucaslyl
Copy link
Contributor Author

@codex review

@lucaslyl lucaslyl marked this pull request as ready for review March 18, 2026 09:19
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 87624ce0e0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +98 to +102
if (backgroundWork) {
await backgroundWork;
}

this.operatorModeStateService.dismissCreateListingModal();

Choose a reason for hiding this comment

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

P1 Badge Reset selected examples before auto-dismissing the modal

After a successful create, this path dismisses the modal without clearing selectedExampleIds. Because CreateListingModal stays mounted in operator-mode/container.gts, reopening it later reuses the previous run's manual selection; if the next listing is for a different type, openCardIds can still contain stale ids from the earlier listing and attach unrelated example cards. The success path should clear the selection state the same way onClose() does.

Useful? React with 👍 / 👎.

Comment on lines +68 to +72
this.selectedExampleIds === null
? this.instances.map((i) => i.id)
: this.selectedExampleIds.size > 0
? [...this.selectedExampleIds]
: (payload.openCardIds ?? []);

Choose a reason for hiding this comment

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

P2 Badge Preserve an intentionally empty example selection

When the modal is opened from an instance or playground card, payload.openCardIds is pre-populated. If the user clicks Clear All or deselects the last remaining example, selectedExampleIds becomes an empty set, but this expression falls back to payload.openCardIds and still sends the original card ids to ListingCreateCommand. In that flow the listing is created with example links even though the UI shows nothing selected.

Useful? React with 👍 / 👎.

menuItems = [...menuItems, ...getSampleDataMenuItems(card, params)];
menuItems.push({
label: `Create Listing with AI`,
label: `Create Listing`,
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@tintinthong
Copy link
Contributor

Is there a recursive call somehow? I can't create-listing without it running async forever

@lucaslyl
Copy link
Contributor Author

Is there a recursive call somehow? I can't create-listing without it running async forever

No, there is no recursive call. The flow is linear — create listing, then await background work, then dismiss modal.

The issue is background work fires multiple LLM requests in parallel (autoPatchName, autoPatchSummary, etc) plus N spec creations, all with no timeout. If any one of them is slow or hangs, the modal waits forever.

The original await backgroundWork was intentionally designed based on the request —it keeps the modal on screen until all the AI auto-patching finishes writing to the listing card.

Alternative way, I think

  1. either we can remove await — dismiss the modal immediately, listing fields fill in gradually,
  2. Or add a timeout so the modal waits up to a limit then dismisses.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR improves the “Create Listing” flow in operator mode by introducing a confirmation modal with a loading experience, supporting multi-example selection, and navigating to the new listing immediately while background auto-patching completes.

Changes:

  • Adds an operator-mode “Create Listing” confirmation modal (with examples selection + loading state).
  • Introduces OpenCreateListingModalCommand and updates menu items/indexing expectations to open the modal instead of directly creating a listing.
  • Updates listing creation to accept openCardIds (plural) and exposes background work completion so the modal can auto-dismiss when patching finishes.

Reviewed changes

Copilot reviewed 16 out of 16 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/runtime-common/code-ref.ts Adds isListingDef / isListingInstance helpers for reliable listing detection.
packages/host/app/services/operator-mode-state-service.ts Stores modal payload in cross-submode state service.
packages/host/app/components/operator-mode/container.gts Renders the new modal at operator-mode top level.
packages/host/app/components/operator-mode/detail-panel.gts Switches “Create Listing” action to open modal; hides action for listings.
packages/host/app/components/operator-mode/create-listing-modal.gts New confirmation modal UI + loading state + navigation behavior.
packages/host/app/commands/open-create-listing-modal.ts New command that sets modal payload in operator-mode state.
packages/host/app/commands/listing-create.ts Renames openCardIdopenCardIds, links multiple examples, runs autopatching as background work.
packages/host/app/commands/index.ts Shims/registers the new open-create-listing-modal command.
packages/catalog-realm/catalog-app/listing/listing.gts Marks listing definition with static isListingDef = true and updates menu item filtering.
packages/base/menu-items.ts Updates default menu item to “Create Listing” and opens the modal command.
packages/base/command.gts Updates ListingCreateInput to openCardIds (containsMany).
packages/host/tests/* Adds/updates tests for modal behavior, command behavior, and indexing deps.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +50 to +51
private instancesSearch = getSearch<CardDef>(this, getOwner(this)!, () =>
this.codeRef ? { filter: { type: this.codeRef } } : undefined,
Comment on lines +65 to +69
let targetRealm = payload.targetRealm;
let openCardIds =
this.selectedExampleIds === null
? this.instances.map((i) => i.id)
: [...this.selectedExampleIds];
Comment on lines +201 to +205
@title='Create Listing'
@size='small'
@isOpen={{this.isModalOpen}}
@onClose={{this.onClose}}
data-test-create-listing-modal
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants