Skip to content

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

Open
lucaslyl wants to merge 29 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 29 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

New Added :

  • New CardInstancePicker component (mirrors RealmPicker/TypePicker pattern)
  • CreateListingModal refactored to own selection as PickerOption[]
  • autoLinkExample fix to respect user's explicit 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
  • Menu items (packages/base )

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-19.at.4.57.55.PM.mov

@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

    1 files  ± 0      1 suites  ±0   2h 25m 53s ⏱️ + 2m 8s
2 043 tests +13  2 028 ✅ +13  15 💤 ±0  0 ❌ ±0 
2 058 runs  +13  2 043 ✅ +13  15 💤 ±0  0 ❌ ±0 

Results for commit 45e145f. ± Comparison against base commit 5ba2167.

♻️ 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
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree with this. But follow the request, we should allow users to close the modal instead of forcing them to wait for the listing creation to finish, as the background processing can take quite a long time.

<template>
<div class='operator-mode'>
<ChooseFileModal />
<CreateListingModal />
Copy link
Contributor

Choose a reason for hiding this comment

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

seems like a very similar modal to ChooseFileModal

Copy link
Contributor Author

Choose a reason for hiding this comment

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

From my understanding, the main reason we can't reuse ChooseFileModal is that it's a host-only component that lives in code submode. But the listing creation gets triggered from a menu item in packages/base, which runs in the realm's headless Chrome process — it has no way to reach any host UI component directly. The only bridge i can see is using the command pattern (openlistingmodal command).

The LHS detail panel "create-lisitng" dont have this issue because it was under host packages

private moduleInspectorHistory: Record<string, ModuleInspectorView>;

@tracked profileSettingsOpen = false;
@tracked createListingModalPayload?: CreateListingModalPayload;
Copy link
Contributor

Choose a reason for hiding this comment

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

why was this state not INSIDE of the modal

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As for why createListingModalPayload is stored in OperatorModeStateService — the modal doesn't exist until the payload is set, so the command needs somewhere that's always alive to drop off the data. For now i can see the OperatorModeStateService fits because it persists across all modes, unlike CreateFileModal which only lives in code submode. The payload also carries the context (targetRealm, codeRef, openCardIds) that the modal needs to know what to display, since that info has to travel from the base package through the command pattern to reach the host side. Not sure if my understanding is precise or not

lucaslyl and others added 2 commits March 19, 2026 11:05
- Extract CardInstancePicker as a thin Picker wrapper (mirrors
  RealmPicker/TypePicker pattern: selectAll option, passthrough onChange)
- Refactor CreateListingModal to own selection state as PickerOption[]
  instead of string[], with effectiveSelected derived from user
  interaction or initial payload (openCardIds / first instance)
- Fix autoLinkExample in ListingCreateCommand to respect user's explicit
  selection instead of auto-filling additional examples via LLM
- Update create-listing-modal tests for new picker UI and add coverage
  for auto-select behavior (from instance vs from module)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lucaslyl
Copy link
Contributor Author

@codex feedback

@chatgpt-codex-connector
Copy link

Summary

Testing

  • ⚠️ mise install (failed because the environment could not reach the pinned toolchain download sources)
  • ⚠️ mise trust .mise.toml && mise install (trust succeeded, but Node/pnpm downloads were blocked by network/proxy errors)
  • ⚠️ pnpm lint (blocked because the pinned Node 24 toolchain could not be fetched, so package scripts could not run)
  • git commit -m "Fix create listing modal state handling"

View task →

@lucaslyl lucaslyl requested a review from tintinthong March 19, 2026 14:57
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