Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fd1abdb
Add realm delete workflow
jurgenwerk Mar 12, 2026
e00e903
Address review feedback on realm delete flow
jurgenwerk Mar 12, 2026
b7bc93c
Stabilize workspace delete tests
jurgenwerk Mar 12, 2026
ec5b945
Remove workspace files from recent files
jurgenwerk Mar 12, 2026
8413e27
Clean up realm artifacts on delete
jurgenwerk Mar 13, 2026
ccb1dfe
Add more cleanup
jurgenwerk Mar 13, 2026
fba75a9
If card is deleted (not found), show a more exact error message
jurgenwerk Mar 13, 2026
8d5aa76
Fix test
jurgenwerk Mar 16, 2026
66d9450
Cleanup
jurgenwerk Mar 16, 2026
0e36ff8
Cleanup
jurgenwerk Mar 16, 2026
1335d48
Handle inaccessible attached-card pills safely
jurgenwerk Mar 16, 2026
f24375c
Use path.relative for realm file cleanup
jurgenwerk Mar 16, 2026
8f2efcf
Cover realm_versions cleanup in delete-realm test
jurgenwerk Mar 16, 2026
3b3b612
Reuse shared URL normalization in delete-realm handler
jurgenwerk Mar 16, 2026
5037807
Remove dead attached-card error branch
jurgenwerk Mar 16, 2026
5521f08
Tolerate unmounted published realms during delete
jurgenwerk Mar 16, 2026
87e52d7
Fix test
jurgenwerk Mar 16, 2026
e1287e8
Keep other realm sessions when deleting a workspace
jurgenwerk Mar 16, 2026
6ce38ce
Preserve other realm sessions when deleting a workspace
jurgenwerk Mar 16, 2026
c16b26f
Adjust the design of the workspace delete modal
jurgenwerk Mar 17, 2026
d98326a
Fix test to not leak localstorage
jurgenwerk Mar 17, 2026
6f5edda
Allow workspace deletion for realm owners
jurgenwerk Mar 17, 2026
e26502b
Design tweaks
jurgenwerk Mar 17, 2026
6b054a7
Merge branch 'main' into cs-10333-add-option-to-delete-a-realm
jurgenwerk Mar 18, 2026
60555fb
Cancel running indexing jobs before realm deletion
jurgenwerk Mar 18, 2026
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
1 change: 1 addition & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@ jobs:
"server-endpoints/authentication-test.ts",
"server-endpoints/bot-commands-test.ts",
"server-endpoints/bot-registration-test.ts",
"server-endpoints/delete-realm-test.ts",
"server-endpoints/download-realm-test.ts",
"server-endpoints/index-responses-test.ts",
"server-endpoints/maintenance-endpoints-test.ts",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,6 @@ export default class AttachedItems extends Component<Signature> {
this.areAllItemsDisplayed = !this.areAllItemsDisplayed;
}

private getCardErrorId(cardError: CardErrorJSONAPI) {
return cardError.id ?? '';
}

private getCardErrorRealm(cardError: CardErrorJSONAPI) {
return cardError.realm ?? this.operatorModeStateService.realmURL;
}
Expand Down Expand Up @@ -150,45 +146,36 @@ export default class AttachedItems extends Component<Signature> {
{{#if @isLoaded}}
{{#each this.itemsToDisplay as |item|}}
{{#if (isCardErrorJSONAPI item)}}
{{#if (this.isAutoAttachedCard (this.getCardErrorId item))}}
<Tooltip @placement='top'>
<:trigger>
<CardPill
@cardId={{this.getCardErrorId item}}
@borderType='dashed'
@onClick={{fn
this.handleChooseCard
(this.getCardErrorId item)
}}
@onRemove={{fn
this.handleRemoveCard
(this.getCardErrorId item)
}}
@urlForRealmLookup={{this.getCardErrorRealm item}}
data-test-autoattached-card={{this.getCardErrorId item}}
/>
</:trigger>
{{#if item.id}}
{{#if (this.isAutoAttachedCard item.id)}}
<Tooltip @placement='top'>
<:trigger>
<CardPill
@cardId={{item.id}}
@borderType='dashed'
@onClick={{fn this.handleChooseCard item.id}}
@onRemove={{fn this.handleRemoveCard item.id}}
@urlForRealmLookup={{this.getCardErrorRealm item}}
data-test-autoattached-card={{item.id}}
/>
</:trigger>

<:content>
{{#if @autoAttachedCardTooltipMessage}}
{{@autoAttachedCardTooltipMessage}}
{{else if
(this.isAutoAttachedCard (this.getCardErrorId item))
}}
Topmost card is shared automatically
{{/if}}
</:content>
</Tooltip>
{{else}}
<CardPill
@cardId={{this.getCardErrorId item}}
@borderType='solid'
@onRemove={{fn
this.handleRemoveCard
(this.getCardErrorId item)
}}
@urlForRealmLookup={{this.getCardErrorRealm item}}
/>
<:content>
{{#if @autoAttachedCardTooltipMessage}}
{{@autoAttachedCardTooltipMessage}}
{{else if (this.isAutoAttachedCard item.id)}}
Topmost card is shared automatically
{{/if}}
</:content>
</Tooltip>
{{else}}
<CardPill
@cardId={{item.id}}
@borderType='solid'
@onRemove={{fn this.handleRemoveCard item.id}}
@urlForRealmLookup={{this.getCardErrorRealm item}}
/>
{{/if}}
{{/if}}
{{else if (this.isCard item)}}
{{#if (this.isAutoAttachedCard item.id)}}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { TemplateOnlyComponent } from '@ember/component/template-only';

import type { getCardCollection } from '@cardstack/runtime-common';
import type {
CardErrorJSONAPI,
getCardCollection,
} from '@cardstack/runtime-common';

import CardPill from '@cardstack/host/components/card-pill';
import FilePill from '@cardstack/host/components/file-pill';
Expand All @@ -26,6 +29,54 @@ function findAttachedCardAsFile(
return attachedCardsAsFiles?.find((file) => file.sourceUrl === card.id);
}

function cardErrorRealm(cardError: CardErrorJSONAPI) {
if (cardError.realm) {
return cardError.realm;
}

let id = cardError.id;
if (!id) {
return '';
}

try {
let url = new URL(id);
let lastSlashIndex = url.pathname.lastIndexOf('/');
let pathname =
lastSlashIndex >= 0 ? url.pathname.slice(0, lastSlashIndex + 1) : '/';
return `${url.origin}${pathname}`;
} catch {
return id;
}
}

function cardErrorDisplayTitle(cardError: CardErrorJSONAPI) {
if (cardError.meta.cardTitle) {
return cardError.meta.cardTitle;
}

let id = cardError.id;
if (!id) {
return 'Unavailable card';
}

let path = id;
try {
path = new URL(id).pathname;
} catch {
// ignore invalid urls
}

if (path.startsWith('/')) {
path = path.slice(1);
}

let segments = path.split('/').filter(Boolean);
let label = segments.slice(-2).join('/');

return label.replace(/\.[^.]+$/, '') || 'Unavailable card';
}

const Attachments: TemplateOnlyComponent<Signature> = <template>
<ul class='items' data-test-message-items>
{{#each @items as |item|}}
Expand All @@ -46,6 +97,21 @@ const Attachments: TemplateOnlyComponent<Signature> = <template>
{{/let}}
</li>
{{/each}}
{{#each item.cardErrors as |cardError|}}
{{#if cardError.id}}
<li>
<CardPill
@cardId={{cardError.id}}
@displayTitle={{cardErrorDisplayTitle cardError}}
@borderType='solid'
@showErrorIcon={{true}}
@urlForRealmLookup={{cardErrorRealm cardError}}
title={{cardError.id}}
data-test-attached-card-error={{cardError.id}}
/>
</li>
{{/if}}
{{/each}}
{{else}}
<li>
<FilePill
Expand Down
85 changes: 69 additions & 16 deletions packages/host/app/components/ai-assistant/message/index.gts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { cn } from '@cardstack/boxel-ui/helpers';

import {
MINIMUM_AI_CREDITS_TO_CONTINUE,
type CardErrorJSONAPI,
type getCardCollection,
} from '@cardstack/runtime-common';

Expand Down Expand Up @@ -247,8 +248,48 @@ function isPresent(val: SafeString | string | null | undefined) {
return val ? val !== '' : false;
}

function collectionResourceError(id: string | null | undefined) {
return 'Cannot render ' + id;
export function attachedCardErrorMessages(errors: CardErrorJSONAPI[]) {
let unreachableCount = 0;
let runtimeErrorCount = 0;
let genericErrorCount = 0;

for (let error of errors) {
if (isUnreachableCardError(error)) {
unreachableCount++;
} else if (error.status >= 500) {
runtimeErrorCount++;
} else {
genericErrorCount++;
}
}

return [
...(unreachableCount > 0
? [
unreachableCount === 1
? `The card is unreachable. It may have been deleted, or you don't have permission to see it.`
: `These cards are unreachable. They may have been deleted, or you don't have permission to see them.`,
]
: []),
...(runtimeErrorCount > 0
? [
runtimeErrorCount === 1
? `This card could not be displayed because it hit a runtime error.`
: `Some cards could not be displayed because they hit runtime errors.`,
]
: []),
...(genericErrorCount > 0
? [
genericErrorCount === 1
? `This card could not be displayed because it has an error.`
: `Some cards could not be displayed because they have errors.`,
]
: []),
];
}

function isUnreachableCardError(error: CardErrorJSONAPI) {
return [403, 404].includes(error.status);
}

export default class AiAssistantMessage extends Component<Signature> {
Expand Down Expand Up @@ -373,17 +414,25 @@ export default class AiAssistantMessage extends Component<Signature> {
{{else}}
<Alert @type='error' as |Alert|>
<Alert.Messages @messages={{this.errorMessages}} />
<div class='alert-action-buttons-row'>
{{#if @waitAction}}
<Alert.Action
@actionName='Wait longer'
@action={{@waitAction}}
/>
{{/if}}
{{#if @retryAction}}
<Alert.Action @actionName='Retry' @action={{@retryAction}} />
{{/if}}
</div>
{{#if this.hasAlertActions}}
<div
class='alert-action-buttons-row'
data-test-alert-action-buttons-row
>
{{#if @waitAction}}
<Alert.Action
@actionName='Wait longer'
@action={{@waitAction}}
/>
{{/if}}
{{#if @retryAction}}
<Alert.Action
@actionName='Retry'
@action={{@retryAction}}
/>
{{/if}}
</div>
{{/if}}
</Alert>
{{/if}}
{{/if}}
Expand Down Expand Up @@ -483,12 +532,16 @@ export default class AiAssistantMessage extends Component<Signature> {
private get errorMessages() {
return [
...(this.args.errorMessage ? [this.args.errorMessage] : []),
...(this.args.collectionResource?.cardErrors.map((error) =>
collectionResourceError(error.id),
) ?? []),
...attachedCardErrorMessages(
this.args.collectionResource?.cardErrors ?? [],
),
];
}

private get hasAlertActions() {
return Boolean(this.args.waitAction || this.args.retryAction);
}

private get isOutOfCreditsErrorMessage(): boolean {
return this.errorMessages.some((error) =>
/You need a minimum of \d+ credits to continue using the AI bot\. Please upgrade to a larger plan, or top up your account\./.test(
Expand Down
27 changes: 24 additions & 3 deletions packages/host/app/components/card-pill.gts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
IconButton,
} from '@cardstack/boxel-ui/components';
import { cn } from '@cardstack/boxel-ui/helpers';
import { IconX } from '@cardstack/boxel-ui/icons';
import { FailureBordered, IconX } from '@cardstack/boxel-ui/icons';

import { type getCard, GetCardContextName } from '@cardstack/runtime-common';

Expand All @@ -30,6 +30,8 @@ interface CardPillSignature {
cardId: string;
urlForRealmLookup: string;
borderType?: 'dashed' | 'solid';
displayTitle?: string;
showErrorIcon?: boolean;
onClick?: () => void;
onRemove?: () => void;
isEnabled?: boolean;
Expand All @@ -48,7 +50,11 @@ export default class CardPill extends Component<CardPillSignature> {
}

private get cardTitle() {
return this.card?.cardTitle || this.cardError?.meta.cardTitle;
return (
this.args.displayTitle ||
this.card?.cardTitle ||
this.cardError?.meta.cardTitle
);
}

private get card() {
Expand Down Expand Up @@ -103,7 +109,16 @@ export default class CardPill extends Component<CardPillSignature> {
...attributes
>
<:iconLeft>
<RealmIcon @realmInfo={{this.realm.info @urlForRealmLookup}} />
{{#if @showErrorIcon}}
<FailureBordered
class='fallback-card-icon'
width='18'
height='18'
aria-hidden='true'
/>
{{else}}
<RealmIcon @realmInfo={{this.realm.info @urlForRealmLookup}} />
{{/if}}
</:iconLeft>
<:default>
<div class='card-content' title={{this.cardTitle}}>
Expand Down Expand Up @@ -148,6 +163,12 @@ export default class CardPill extends Component<CardPillSignature> {
.border-solid {
border-style: solid;
}
.fallback-card-icon {
display: block;
flex-shrink: 0;
--icon-background-color: var(--boxel-error-500);
--icon-color: var(--boxel-light);
}
.card-content {
max-width: 100px;
max-height: 100%;
Expand Down
5 changes: 1 addition & 4 deletions packages/host/app/components/matrix/room-message.gts
Original file line number Diff line number Diff line change
Expand Up @@ -253,9 +253,6 @@ export default class RoomMessage extends Component<Signature> {
if (this.streamingTimeout) {
return `This message has been processing for a long time (more than ${STREAMING_TIMEOUT_MINUTES} minutes), possibly due to a delay in response time, or due to a system error.`; // Will show a "Wait longer" and "Retry" button
}
if (this.attachedCardCollection?.cardErrors.length === 0) {
return undefined;
}
return 'Error rendering attached cards.';
return undefined;
}
}
Loading
Loading