Skip to content
5 changes: 4 additions & 1 deletion packages/base/links-to-editor.gts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ export class LinksToEditor extends GlimmerComponent<Signature> {
/>
{{/if}}
<DefaultFormatsProvider
@value={{hash cardDef='fitted' fieldDef='embedded'}}
@value={{hash
cardDef='fitted'
fieldDef='embedded'
}}
>
<this.linkedCard />
</DefaultFormatsProvider>
Expand Down
9 changes: 8 additions & 1 deletion packages/host/app/components/host-mode/breadcrumb-item.gts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { cached } from '@glimmer/tracking';

import { cardTypeIcon, isCardInstance } from '@cardstack/runtime-common';

import { inferStoreReadType } from '@cardstack/host/lib/read-type';
import { getCard } from '@cardstack/host/resources/card-resource';

import type { ComponentLike } from '@glint/template';
Expand All @@ -26,7 +27,13 @@ export default class HostModeBreadcrumbItem extends Component<Signature> {
return undefined;
}

return getCard(this, () => this.args.cardId);
return getCard(this, () => this.args.cardId, {
type: this.readType,
});
}

private get readType() {
return inferStoreReadType(this.args.cardId);
}

@cached
Expand Down
9 changes: 8 additions & 1 deletion packages/host/app/components/host-mode/card.gts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { BoxelButton, CardContainer } from '@cardstack/boxel-ui/components';

import CardRenderer from '@cardstack/host/components/card-renderer';
import CardError from '@cardstack/host/components/operator-mode/card-error';
import { inferStoreReadType } from '@cardstack/host/lib/read-type';
import { getCard } from '@cardstack/host/resources/card-resource';

interface Signature {
Expand All @@ -25,7 +26,13 @@ export default class HostModeCard extends Component<Signature> {
return undefined;
}

return getCard(this, () => this.args.cardId!);
return getCard(this, () => this.args.cardId!, {
type: this.readType,
});
}

private get readType() {
return inferStoreReadType(this.args.cardId);
}

get card() {
Expand Down
9 changes: 8 additions & 1 deletion packages/host/app/components/host-mode/content.gts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
} from '@cardstack/runtime-common';
import { meta } from '@cardstack/runtime-common/constants';

import { inferStoreReadType } from '@cardstack/host/lib/read-type';
import { getCard } from '@cardstack/host/resources/card-resource';

import type {
Expand Down Expand Up @@ -45,7 +46,13 @@ export default class HostModeContent extends Component<Signature> {
return undefined;
}

return getCard(this, () => this.args.primaryCardId!);
return getCard(this, () => this.args.primaryCardId!, {
type: this.primaryReadType,
});
}

private get primaryReadType() {
return inferStoreReadType(this.args.primaryCardId);
}

get cardIds() {
Expand Down
9 changes: 8 additions & 1 deletion packages/host/app/components/host-mode/stack-item.gts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { cached, tracked } from '@glimmer/tracking';
import { ContextButton } from '@cardstack/boxel-ui/components';
import { and, bool } from '@cardstack/boxel-ui/helpers';

import { inferStoreReadType } from '@cardstack/host/lib/read-type';
import { getCard } from '@cardstack/host/resources/card-resource';

import HostModeCard from './card';
Expand All @@ -37,7 +38,13 @@ export default class HostModeStackItem extends Component<Signature> {
if (!this.args.cardId) {
return undefined;
}
return getCard(this, () => this.args.cardId);
return getCard(this, () => this.args.cardId, {
type: this.readType,
});
}

private get readType() {
return inferStoreReadType(this.args.cardId);
}

@cached
Expand Down
28 changes: 27 additions & 1 deletion packages/host/app/components/operator-mode/interact-submode.gts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
codeRefWithAbsoluteURL,
identifyCard,
isCardInstance,
isFileDefInstance,
isResolvedCodeRef,
CardError,
loadCardDef,
Expand All @@ -53,7 +54,7 @@ import {

import CopyCardToStackCommand from '@cardstack/host/commands/copy-card-to-stack';

import { StackItem } from '@cardstack/host/lib/stack-item';
import { StackItem, type StackItemType } from '@cardstack/host/lib/stack-item';

import { stackBackgroundsResource } from '@cardstack/host/resources/stack-backgrounds';

Expand Down Expand Up @@ -186,6 +187,7 @@ export default class InteractSubmode extends Component {
request: new Deferred(),
closeAfterSaving: opts?.closeAfterCreating,
stackIndex,
type: 'card',
});
this.addToStack(newItem);
return localId;
Expand All @@ -212,6 +214,9 @@ export default class InteractSubmode extends Component {
: cardOrURL instanceof URL
? cardOrURL.href
: cardOrURL.id;
if (!cardId) {
return;
}
if (opts?.openCardInRightMostStack) {
stackIndex = this.stacks.length;
} else if (typeof opts?.stackIndex === 'number') {
Expand All @@ -229,10 +234,12 @@ export default class InteractSubmode extends Component {
}
stackIndex = opts.stackIndex;
}
let stackItemType = this.getStackItemType(cardOrURL, cardId);
let newItem = new StackItem({
id: cardId,
format,
stackIndex,
type: stackItemType,
relationshipContext: opts?.fieldName
? {
fieldName: opts.fieldName,
Expand All @@ -251,6 +258,23 @@ export default class InteractSubmode extends Component {
this.operatorModeStateService.editCardOnStack(stackIndex, card);
};

private getStackItemType(
cardOrURL: CardDef | URL | string,
cardId: string,
): StackItemType {
if (
cardOrURL &&
typeof cardOrURL === 'object' &&
!(cardOrURL instanceof URL)
) {
return isFileDefInstance(cardOrURL as CardDef) ? 'file-meta' : 'card';
}
let fileMetaInstanceOrError =
this.store.peek(cardId, { type: 'file-meta' }) ??
this.store.peekError(cardId, { type: 'file-meta' });
return fileMetaInstanceOrError ? 'file-meta' : 'card';
}
Comment on lines +261 to +276
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

When cardOrURL is a URL string that points to a file (has extension, not .json), but the file-meta instance is not yet loaded in the store, this method will return 'card' instead of 'file-meta'. This could cause the wrong type to be used when opening a file via direct navigation. Consider using inferStoreReadType as a fallback when no instance is found in the store to correctly detect file URLs based on their extension pattern.

Copilot uses AI. Check for mistakes.

private saveCard = (id: string): void => {
this.store.save(id);
};
Expand Down Expand Up @@ -500,6 +524,7 @@ export default class InteractSubmode extends Component {
id: url.href,
format: 'isolated',
stackIndex: 0,
type: this.getStackItemType(url, url.href),
});
// it's important that we await the stack item readiness _before_
// we mutate the stack, otherwise there are very odd visual artifacts
Expand Down Expand Up @@ -542,6 +567,7 @@ export default class InteractSubmode extends Component {
id: url.href,
format: 'isolated',
stackIndex,
type: this.getStackItemType(url, url.href),
});
// await stackItem.ready();
this.operatorModeStateService.clearStackAndAdd(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,16 @@ import {
} from '@cardstack/boxel-ui/icons';

import type { CommandContext } from '@cardstack/runtime-common';
import { isFileDefInstance } from '@cardstack/runtime-common';

import {
CardCrudFunctionsContextName,
CommandContextName,
getMenuItems,
} from '@cardstack/runtime-common';

import { isFileMetaId } from '@cardstack/host/lib/read-type';

import type {
CardCrudFunctions,
CardDef,
Expand Down Expand Up @@ -345,6 +349,9 @@ export default class OperatorModeOverlays extends Overlays {
case 'select':
return !this.isField(renderedCard) && !!this.args.toggleSelect;
case 'edit':
if (this.isFileMetaTarget(renderedCard)) {
return false;
}
return this.realm.canWrite(this.getCardId(renderedCard.cardDefOrId));
case 'more-options':
return true;
Expand All @@ -353,6 +360,16 @@ export default class OperatorModeOverlays extends Overlays {
}
}

private isFileMetaTarget(
renderedCard: StackItemRenderedCardForOverlayActions,
): boolean {
let cardDefOrId = renderedCard.cardDefOrId;
if (typeof cardDefOrId === 'string') {
return isFileMetaId(cardDefOrId);
}
return isFileDefInstance(cardDefOrId);
}

@action
private registerDropdownAPI(
renderedCard: StackItemRenderedCardForOverlayActions,
Expand Down Expand Up @@ -398,6 +415,9 @@ export default class OperatorModeOverlays extends Overlays {
protected override getFormatForCard(
renderedCard: StackItemRenderedCardForOverlayActions,
): Format {
if (this.isFileMetaTarget(renderedCard)) {
return 'isolated';
}
return renderedCard.stackItem.format as Format;
}

Expand Down
4 changes: 3 additions & 1 deletion packages/host/app/components/operator-mode/overlays.gts
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,9 @@ export default class Overlays extends Component<OverlaySignature> {
let canWrite = this.realm.canWrite(cardId);
format = canWrite ? format : 'isolated';
if (this.args.viewCard) {
await this.args.viewCard(new URL(cardId), format, {
let target =
typeof cardDefOrId === 'string' ? new URL(cardId) : cardDefOrId;
await this.args.viewCard(target, format, {
fieldType,
fieldName,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
getMenuItems,
identifyCard,
isCardInstance,
isFileDefInstance,
isResolvedCodeRef,
} from '@cardstack/runtime-common';

Expand Down Expand Up @@ -94,7 +95,11 @@ export default class PreviewPanel extends Component<Signature> {

private openInInteractMode = () => {
if (this.cardId) {
this.operatorModeStateService.openCardInInteractMode(this.cardId);
this.operatorModeStateService.openCardInInteractMode(
this.cardId,
'isolated',
isFileDefInstance(this.args.card) ? 'file-meta' : 'card',
);
}
};

Expand Down
23 changes: 20 additions & 3 deletions packages/host/app/components/operator-mode/stack-item.gts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
type getCard,
type getCards,
type getCardCollection,
isFileDefInstance,
cardTypeDisplayName,
PermissionsContextName,
RealmURLContextName,
Expand Down Expand Up @@ -178,7 +179,9 @@ export default class OperatorModeStackItem extends Component<Signature> {
}

private makeCardResource = () => {
this.cardResource = this.getCard(this, () => this.args.item.id);
this.cardResource = this.getCard(this, () => this.args.item.id, {
type: this.args.item.type,
});
};

private get url() {
Expand Down Expand Up @@ -654,12 +657,26 @@ export default class OperatorModeStackItem extends Component<Signature> {
this.card[realmURL] &&
!this.isBuried &&
!this.isEditing &&
!this.isFileCard &&
this.realm.canWrite(this.card[realmURL].href)
);
}

private get isEditing() {
return !this.isBuried && this.args.item.format === 'edit';
return (
!this.isBuried && !this.isFileCard && this.args.item.format === 'edit'
);
}

private get isFileCard() {
return (
this.args.item.type === 'file-meta' ||
(this.card ? isFileDefInstance(this.card) : false)
);
}

private get cardFormat() {
return this.isFileCard ? 'isolated' : this.args.item.format;
}

private get showError() {
Expand Down Expand Up @@ -831,7 +848,7 @@ export default class OperatorModeStackItem extends Component<Signature> {
<CardRenderer
class='stack-item-preview'
@card={{this.card}}
@format={{@item.format}}
@format={{this.cardFormat}}
/>
<OperatorModeOverlays
@renderedCardsForOverlayActions={{this.renderedCardsForOverlayActions}}
Expand Down
15 changes: 15 additions & 0 deletions packages/host/app/lib/read-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { StoreReadType } from '@cardstack/runtime-common';
import { hasExtension } from '@cardstack/runtime-common/url';

export function isFileMetaId(id: string | null | undefined): boolean {
if (!id) {
return false;
}
return hasExtension(id) && !id.endsWith('.json');
}

export function inferStoreReadType(
id: string | null | undefined,
): StoreReadType {
return isFileMetaId(id) ? 'file-meta' : 'card';
}
Comment on lines +4 to +15
Copy link
Contributor

Choose a reason for hiding this comment

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

This is an AI anti-pattern that I have had to fight . We should be using more explicit type specification, not inferring stuff from URLs

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note from our call:

  • consider adding a subprotocol or url annotation/prefix in urls in operator mode state that differentiate between card or file
  • default should be card

Loading