Skip to content
Open
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
51 changes: 50 additions & 1 deletion packages/host/app/components/operator-mode/code-submode.gts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ import perform from 'ember-concurrency/helpers/perform';
import FromElseWhere from 'ember-elsewhere/components/from-elsewhere';

import { consume, provide } from 'ember-provide-consume-context';
import { use, resource } from 'ember-resources';
import window from 'ember-window-mock';

import startCase from 'lodash/startCase';
import { TrackedObject } from 'tracked-built-ins';

import {
LoadingIndicator,
Expand All @@ -30,6 +32,7 @@ import { File } from '@cardstack/boxel-ui/icons';
import type { CodeRef } from '@cardstack/runtime-common';
import {
isCardDocumentString,
isCardErrorJSONAPI,
RealmPaths,
PermissionsContextName,
GetCardContextName,
Expand Down Expand Up @@ -57,8 +60,10 @@ import type PlaygroundPanelService from '@cardstack/host/services/playground-pan
import type RealmService from '@cardstack/host/services/realm';
import type RecentFilesService from '@cardstack/host/services/recent-files-service';
import type SpecPanelService from '@cardstack/host/services/spec-panel-service';
import type StoreService from '@cardstack/host/services/store';

import type {
BaseDef,
CardDef,
Format,
CardContext,
Expand Down Expand Up @@ -143,6 +148,7 @@ export default class CodeSubmode extends Component<Signature> {
@service declare private recentFilesService: RecentFilesService;
@service declare private realm: RealmService;
@service declare private specPanelService: SpecPanelService;
@service declare private store: StoreService;

@tracked private loadFileError: string | null = null;
@tracked private userHasDismissedURLError = false;
Expand Down Expand Up @@ -594,8 +600,51 @@ export default class CodeSubmode extends Component<Signature> {
this.operatorModeStateService.updateCardPreviewFormat(format);
}

private get isNonModuleFile() {
return !this.isModule && !isCardDocumentString(this.readyFile.content);
}

@use private fileDefResource = resource(() => {
let state = new TrackedObject<{
value: BaseDef | undefined;
isLoading: boolean;
error: unknown;
}>({
value: undefined,
isLoading: false,
error: undefined,
});
if (!this.isNonModuleFile) {
return state;
}
let fileUrl = this.readyFile.url;
state.isLoading = true;
(async () => {
try {
let result = await this.store.get(fileUrl, { type: 'file-meta' });
if (isCardErrorJSONAPI(result)) {
state.error = result;
state.value = undefined;
} else {
state.value = result as unknown as BaseDef;
state.error = undefined;
}
} catch (e) {
state.error = e;
state.value = undefined;
} finally {
state.isLoading = false;
}
})();
return state;
});

private get isFileDefInstance() {
Copy link
Contributor

Choose a reason for hiding this comment

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

What about this existing function? Can it be used here?

export function isFileDefInstance<T extends FileDef>(
fileInstance: any,

return this.fileDefResource?.value !== undefined;
}

get isReadOnly() {
return !this.realm.canWrite(this.readyFile.url);
return !this.realm.canWrite(this.readyFile.url) || this.isFileDefInstance;
Comment on lines 646 to +647

Choose a reason for hiding this comment

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

P2 Badge Mark file-def files read-only while metadata is loading

For writable realms, isReadOnly remains false until the async store.get(fileUrl, { type: 'file-meta' }) call finishes and sets fileDefResource.value, so a file-def instance can briefly open as editable; on slower responses, users can type (and potentially trigger autosave) before the lock engages. This undermines the new guarantee that file-def instances are not editable in code mode.

Useful? React with 👍 / 👎.

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.

The isReadOnly getter may briefly return false while the fileDefResource is still loading, potentially allowing users to start editing before the file is marked as read-only. Consider also checking the loading state to ensure the file is read-only during the resource fetch. For example: return !this.realm.canWrite(this.readyFile.url) || this.isFileDefInstance || this.fileDefResource?.isLoading;

Suggested change
return !this.realm.canWrite(this.readyFile.url) || this.isFileDefInstance;
return (
!this.realm.canWrite(this.readyFile.url) ||
this.isFileDefInstance ||
this.fileDefResource?.isLoading
);

Copilot uses AI. Check for mistakes.
}

@provide(PermissionsContextName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,19 @@ Some markdown content.`,

assert.dom('[data-test-card-url-bar-input]').hasValue(expectedMarkdownUrl);
});

test('file def instance is read-only in code mode', async function (assert) {
await visitOperatorMode({
submode: 'code',
codePath: `${testRealmURL}FileLinkCard/notes.md`,
});

await waitFor('[data-test-editor]');
assert
.dom('[data-test-format-chooser="edit"]')
.doesNotExist('edit format option is not shown for file def instance');
assert
.dom('[data-test-realm-indicator-not-writable]')
.exists('read-only indicator is shown for file def instance');
});
});