From 9f1f87552c3bae8ded7eb0478098d173c3bd7e54 Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Tue, 20 Jan 2026 11:01:05 +0100 Subject: [PATCH 1/5] Add createFile param to switch submode command --- packages/base/command.gts | 6 ++ packages/host/app/commands/patch-code.ts | 32 ++------- packages/host/app/commands/switch-submode.ts | 65 +++++++++++++++---- .../matrix/room-message-command.gts | 4 +- packages/host/app/utils/file-name.ts | 29 +++++++++ .../commands/switch-submode-test.gts | 64 ++++++++++++++++++ packages/runtime-common/ai/prompt.ts | 46 +++++++++++++ 7 files changed, 205 insertions(+), 41 deletions(-) create mode 100644 packages/host/app/utils/file-name.ts diff --git a/packages/base/command.gts b/packages/base/command.gts index 538c333b97..829bd02db7 100644 --- a/packages/base/command.gts +++ b/packages/base/command.gts @@ -117,6 +117,12 @@ export class FileContents extends CardDef { export class SwitchSubmodeInput extends CardDef { @field submode = contains(StringField); @field codePath = contains(StringField); + @field createFile = contains(BooleanField); +} + +export class SwitchSubmodeResult extends CardDef { + @field codePath = contains(StringField); + @field requestedCodePath = contains(StringField); } export class WriteTextFileInput extends CardDef { diff --git a/packages/host/app/commands/patch-code.ts b/packages/host/app/commands/patch-code.ts index 8ab1015324..fdb0d8f607 100644 --- a/packages/host/app/commands/patch-code.ts +++ b/packages/host/app/commands/patch-code.ts @@ -16,6 +16,7 @@ import type CommandService from '../services/command-service'; import type MonacoService from '../services/monaco-service'; import type OperatorModeStateService from '../services/operator-mode-state-service'; import type RealmService from '../services/realm'; +import { findNonConflictingFilename } from '../utils/file-name'; interface FileInfo { exists: boolean; @@ -218,34 +219,9 @@ export default class PatchCodeCommand extends HostBaseCommand< return originalUrl; } - return await this.findNonConflictingFilename(originalUrl); - } - - private async findNonConflictingFilename(fileUrl: string): Promise { - let MAX_ATTEMPTS = 100; - let { baseName, extension } = this.parseFilename(fileUrl); - - for (let counter = 1; counter < MAX_ATTEMPTS; counter++) { - let candidateUrl = `${baseName}-${counter}${extension}`; - let exists = await this.fileExists(candidateUrl); - - if (!exists) { - return candidateUrl; - } - } - - return `${baseName}-${MAX_ATTEMPTS}${extension}`; - } - - private parseFilename(fileUrl: string): { - baseName: string; - extension: string; - } { - let extensionMatch = fileUrl.match(/\.([^.]+)$/); - let extension = extensionMatch?.[0] || ''; - let baseName = fileUrl.replace(/\.([^.]+)$/, ''); - - return { baseName, extension }; + return await findNonConflictingFilename(originalUrl, (candidateUrl) => + this.fileExists(candidateUrl), + ); } private async fileExists(fileUrl: string): Promise { diff --git a/packages/host/app/commands/switch-submode.ts b/packages/host/app/commands/switch-submode.ts index c08b78d6e5..c2671b7acc 100644 --- a/packages/host/app/commands/switch-submode.ts +++ b/packages/host/app/commands/switch-submode.ts @@ -7,12 +7,16 @@ import { Submodes } from '../components/submode-switcher'; import HostBaseCommand from '../lib/host-base-command'; import type OperatorModeStateService from '../services/operator-mode-state-service'; +import type CardService from '../services/card-service'; import type StoreService from '../services/store'; +import { findNonConflictingFilename } from '../utils/file-name'; export default class SwitchSubmodeCommand extends HostBaseCommand< - typeof BaseCommandModule.SwitchSubmodeInput + typeof BaseCommandModule.SwitchSubmodeInput, + typeof BaseCommandModule.SwitchSubmodeResult | undefined > { @service declare private operatorModeStateService: OperatorModeStateService; + @service declare private cardService: CardService; @service declare private store: StoreService; static actionVerb = 'Switch'; @@ -43,23 +47,53 @@ export default class SwitchSubmodeCommand extends HostBaseCommand< protected async run( input: BaseCommandModule.SwitchSubmodeInput, - ): Promise { + ): Promise { + let resultCard: BaseCommandModule.SwitchSubmodeResult | undefined; switch (input.submode) { case Submodes.Interact: await this.operatorModeStateService.updateCodePath(null); break; case Submodes.Code: - if (input.codePath) { - await this.operatorModeStateService.updateCodePath( - new URL(input.codePath), - ); - } else { - await this.operatorModeStateService.updateCodePath( - this.lastCardInRightMostStack - ? new URL(this.lastCardInRightMostStack + '.json') - : null, - ); + let codePath = + input.codePath ?? + (this.lastCardInRightMostStack + ? this.lastCardInRightMostStack + '.json' + : null); + let codeUrl = codePath ? new URL(codePath) : null; + let currentSubmode = this.operatorModeStateService.state.submode; + let finalCodeUrl = codeUrl; + if ( + codeUrl && + input.createFile && + currentSubmode === Submodes.Interact + ) { + let { status, content } = await this.cardService.getSource(codeUrl); + if (status === 404) { + await this.cardService.saveSource(codeUrl, '', 'create-file'); + } else if (status === 200) { + if (content.trim() !== '') { + let nonConflictingUrl = await findNonConflictingFilename( + codeUrl.href, + (candidateUrl) => this.fileExists(candidateUrl), + ); + let newCodeUrl = new URL(nonConflictingUrl); + await this.cardService.saveSource(newCodeUrl, '', 'create-file'); + finalCodeUrl = newCodeUrl; + + let commandModule = await this.loadCommandModule(); + const { SwitchSubmodeResult } = commandModule; + resultCard = new SwitchSubmodeResult({ + codePath: newCodeUrl.href, + requestedCodePath: codeUrl.href, + }); + } + } else if (status !== 200) { + throw new Error( + `Error checking if file exists at ${codeUrl}: ${status}`, + ); + } } + await this.operatorModeStateService.updateCodePath(finalCodeUrl); break; default: throw new Error(`invalid submode specified: ${input.submode}`); @@ -69,5 +103,12 @@ export default class SwitchSubmodeCommand extends HostBaseCommand< if (this.operatorModeStateService.workspaceChooserOpened) { this.operatorModeStateService.closeWorkspaceChooser(); } + + return resultCard; + } + + private async fileExists(fileUrl: string): Promise { + let getSourceResult = await this.cardService.getSource(new URL(fileUrl)); + return getSourceResult.status !== 404; } } diff --git a/packages/host/app/components/matrix/room-message-command.gts b/packages/host/app/components/matrix/room-message-command.gts index 20b71c5f1f..90564b8327 100644 --- a/packages/host/app/components/matrix/room-message-command.gts +++ b/packages/host/app/components/matrix/room-message-command.gts @@ -135,9 +135,11 @@ export default class RoomMessageCommand extends Component { } private get shouldDisplayResultCard() { + let commandName = this.args.messageCommand.name ?? ''; return ( !!this.commandResultCard.card && - this.args.messageCommand.name !== 'checkCorrectness' + commandName !== 'checkCorrectness' && + !commandName.startsWith('switch-submode') ); } diff --git a/packages/host/app/utils/file-name.ts b/packages/host/app/utils/file-name.ts new file mode 100644 index 0000000000..42db1a858b --- /dev/null +++ b/packages/host/app/utils/file-name.ts @@ -0,0 +1,29 @@ +export async function findNonConflictingFilename( + fileUrl: string, + fileExists: (fileUrl: string) => Promise, +): Promise { + let maxAttempts = 100; + let { baseName, extension } = parseFilename(fileUrl); + + for (let counter = 1; counter < maxAttempts; counter++) { + let candidateUrl = `${baseName}-${counter}${extension}`; + let exists = await fileExists(candidateUrl); + + if (!exists) { + return candidateUrl; + } + } + + return `${baseName}-${maxAttempts}${extension}`; +} + +function parseFilename(fileUrl: string): { + baseName: string; + extension: string; +} { + let extensionMatch = fileUrl.match(/\.([^.]+)$/); + let extension = extensionMatch?.[0] || ''; + let baseName = fileUrl.replace(/\.([^.]+)$/, ''); + + return { baseName, extension }; +} diff --git a/packages/host/tests/integration/commands/switch-submode-test.gts b/packages/host/tests/integration/commands/switch-submode-test.gts index 0c6f1f6f90..179ee53349 100644 --- a/packages/host/tests/integration/commands/switch-submode-test.gts +++ b/packages/host/tests/integration/commands/switch-submode-test.gts @@ -123,4 +123,68 @@ module('Integration | commands | switch-submode', function (hooks) { 'Workspace chooser should be closed after switching submode', ); }); + + test('createFile creates a blank file when it does not exist', async function (assert) { + assert.expect(5); + + let commandService = getService('command-service'); + let cardService = getService('card-service'); + let operatorModeStateService = getService('operator-mode-state-service'); + operatorModeStateService.restore({ + stacks: [[]], + submode: 'interact', + }); + let switchSubmodeCommand = new SwitchSubmodeCommand( + commandService.commandContext, + ); + let fileUrl = `${testRealmURL}new-file.gts`; + + let result = await switchSubmodeCommand.execute({ + submode: 'code', + codePath: fileUrl, + createFile: true, + }); + + assert.strictEqual(operatorModeStateService.state?.submode, 'code'); + assert.strictEqual(operatorModeStateService.state?.codePath?.href, fileUrl); + assert.notOk(result, 'no result card when using requested path'); + + let { status, content } = await cardService.getSource(new URL(fileUrl)); + assert.strictEqual(status, 200); + assert.strictEqual(content, ''); + }); + + test('createFile picks a non-conflicting filename when the target exists', async function (assert) { + assert.expect(6); + + let commandService = getService('command-service'); + let cardService = getService('card-service'); + let operatorModeStateService = getService('operator-mode-state-service'); + operatorModeStateService.restore({ + stacks: [[]], + submode: 'interact', + }); + let switchSubmodeCommand = new SwitchSubmodeCommand( + commandService.commandContext, + ); + let fileUrl = `${testRealmURL}existing-file.gts`; + let newFileUrl = `${testRealmURL}existing-file-1.gts`; + + await cardService.saveSource(new URL(fileUrl), 'existing content', 'create-file'); + + let result = await switchSubmodeCommand.execute({ + submode: 'code', + codePath: fileUrl, + createFile: true, + }); + + assert.ok(result, 'returns a result card with the new filename'); + assert.strictEqual(result?.codePath, newFileUrl); + assert.strictEqual(result?.requestedCodePath, fileUrl); + assert.strictEqual(operatorModeStateService.state?.codePath?.href, newFileUrl); + + let { status, content } = await cardService.getSource(new URL(newFileUrl)); + assert.strictEqual(status, 200); + assert.strictEqual(content, ''); + }); }); diff --git a/packages/runtime-common/ai/prompt.ts b/packages/runtime-common/ai/prompt.ts index f511d6ec11..3423a4829c 100644 --- a/packages/runtime-common/ai/prompt.ts +++ b/packages/runtime-common/ai/prompt.ts @@ -809,6 +809,13 @@ async function toResultMessages( } else { content = `Tool call ${status == 'applied' ? 'executed' : status}.\n`; } + let switchSubmodeInstruction = + getSwitchSubmodeInstruction(commandResult); + if (switchSubmodeInstruction) { + content = [content, switchSubmodeInstruction] + .filter(Boolean) + .join('\n\n'); + } let attachments = await buildAttachmentsMessagePart( client, commandResult, @@ -843,6 +850,45 @@ async function toResultMessages( return [...toolMessages, ...followUpMessages]; } +function getSwitchSubmodeInstruction( + commandResult: CommandResultEvent, +): string | undefined { + if ( + commandResult.content.msgtype !== + APP_BOXEL_COMMAND_RESULT_WITH_OUTPUT_MSGTYPE + ) { + return undefined; + } + + let cardPayload = commandResult.content.data.card; + if (!cardPayload) { + return undefined; + } + + let parsed: { data?: any } | undefined; + let cardContent = cardPayload.content ?? cardPayload; + try { + parsed = typeof cardContent === 'string' ? JSON.parse(cardContent) : cardContent; + } catch { + return undefined; + } + + let data = parsed?.data; + if (data?.meta?.adoptsFrom?.name !== 'SwitchSubmodeResult') { + return undefined; + } + + let { codePath, requestedCodePath } = data.attributes ?? {}; + if (typeof codePath !== 'string' || typeof requestedCodePath !== 'string') { + return undefined; + } + if (codePath === requestedCodePath) { + return undefined; + } + + return `Use ${codePath} for the SEARCH/REPLACE block since ${requestedCodePath} already exists.`; +} + function buildCheckCorrectnessResultContent( request?: CommandRequest, commandResult?: CommandResultEvent, From 2cbf92ef67ef63262192d10825cb4a5607aa53d7 Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Tue, 20 Jan 2026 12:29:03 +0100 Subject: [PATCH 2/5] Test + cleanup --- .../ai-bot/tests/prompt-construction-test.ts | 106 ++++++++++++++++++ packages/host/app/commands/patch-code.ts | 3 +- packages/host/app/commands/switch-submode.ts | 8 +- .../commands/switch-submode-test.gts | 11 +- packages/runtime-common/ai/prompt.ts | 3 +- 5 files changed, 124 insertions(+), 7 deletions(-) diff --git a/packages/ai-bot/tests/prompt-construction-test.ts b/packages/ai-bot/tests/prompt-construction-test.ts index b050f1bfbe..f3cef93964 100644 --- a/packages/ai-bot/tests/prompt-construction-test.ts +++ b/packages/ai-bot/tests/prompt-construction-test.ts @@ -2723,6 +2723,112 @@ Attached Files (files with newer versions don't show their content): assert.ok(alertTool, 'Should have AlertTheUser function available'); }); + test('switch-submode result appends a filename instruction to tool message', async function () { + const roomId = '!room:localhost'; + const commandEventId = 'switch-submode-event'; + const commandResultId = 'switch-submode-result'; + const commandRequestId = 'switch-submode-cmd'; + const requestedCodePath = + 'http://localhost:4201/drafts/rock-paper-scissors.gts'; + const finalCodePath = + 'http://localhost:4201/drafts/rock-paper-scissors-1.gts'; + + const history: DiscreteMatrixEvent[] = [ + { + type: 'm.room.message', + event_id: commandEventId, + room_id: roomId, + sender: '@aibot:localhost', + origin_server_ts: 1, + content: { + msgtype: APP_BOXEL_MESSAGE_MSGTYPE, + body: 'Switch to code', + format: 'org.matrix.custom.html', + isStreamingFinished: true, + [APP_BOXEL_COMMAND_REQUESTS_KEY]: [ + { + id: commandRequestId, + name: 'switch-submode_1234', + arguments: JSON.stringify({ + description: 'Switch to code submode', + attributes: { + submode: 'code', + codePath: requestedCodePath, + createFile: true, + }, + }), + }, + ], + data: { + context: { + tools: [], + functions: [], + }, + }, + }, + unsigned: { age: 0, transaction_id: commandEventId }, + status: EventStatus.SENT, + }, + { + type: APP_BOXEL_COMMAND_RESULT_EVENT_TYPE, + event_id: commandResultId, + room_id: roomId, + sender: '@command:localhost', + origin_server_ts: 2, + content: { + msgtype: APP_BOXEL_COMMAND_RESULT_WITH_OUTPUT_MSGTYPE, + commandRequestId, + 'm.relates_to': { + event_id: commandEventId, + key: 'applied', + rel_type: APP_BOXEL_COMMAND_RESULT_REL_TYPE, + }, + data: { + card: { + sourceUrl: finalCodePath, + url: finalCodePath, + name: 'switch-submode-result.json', + contentType: 'application/json', + content: JSON.stringify({ + data: { + attributes: { + codePath: finalCodePath, + requestedCodePath, + }, + meta: { + adoptsFrom: { + module: 'https://cardstack.com/base/command', + name: 'SwitchSubmodeResult', + }, + }, + }, + }), + }, + }, + }, + unsigned: { age: 0, transaction_id: commandResultId }, + status: EventStatus.SENT, + }, + ]; + + const result = await buildPromptForModel( + history, + '@aibot:localhost', + [], + [], + [], + fakeMatrixClient, + ); + const toolMessages = result.filter((message) => message.role === 'tool'); + assert.strictEqual(toolMessages.length, 1, 'should have one tool message'); + assert.ok( + (toolMessages[0].content as string).includes( + `Use ${finalCodePath} for the SEARCH/REPLACE block since ${requestedCodePath} already exists.`, + ), + 'tool message should include the filename instruction', + ); + }); + test('Tools are not required unless they are in the last message', async () => { const eventList: DiscreteMatrixEvent[] = JSON.parse( readFileSync( diff --git a/packages/host/app/commands/patch-code.ts b/packages/host/app/commands/patch-code.ts index fdb0d8f607..970f9c192d 100644 --- a/packages/host/app/commands/patch-code.ts +++ b/packages/host/app/commands/patch-code.ts @@ -8,6 +8,8 @@ import HostBaseCommand from '../lib/host-base-command'; import { parseSearchReplace } from '../lib/search-replace-block-parsing'; import { isReady } from '../resources/file'; +import { findNonConflictingFilename } from '../utils/file-name'; + import ApplySearchReplaceBlockCommand from './apply-search-replace-block'; import LintAndFixCommand from './lint-and-fix'; @@ -16,7 +18,6 @@ import type CommandService from '../services/command-service'; import type MonacoService from '../services/monaco-service'; import type OperatorModeStateService from '../services/operator-mode-state-service'; import type RealmService from '../services/realm'; -import { findNonConflictingFilename } from '../utils/file-name'; interface FileInfo { exists: boolean; diff --git a/packages/host/app/commands/switch-submode.ts b/packages/host/app/commands/switch-submode.ts index c2671b7acc..56657629e3 100644 --- a/packages/host/app/commands/switch-submode.ts +++ b/packages/host/app/commands/switch-submode.ts @@ -6,10 +6,11 @@ import { Submodes } from '../components/submode-switcher'; import HostBaseCommand from '../lib/host-base-command'; -import type OperatorModeStateService from '../services/operator-mode-state-service'; +import { findNonConflictingFilename } from '../utils/file-name'; + import type CardService from '../services/card-service'; +import type OperatorModeStateService from '../services/operator-mode-state-service'; import type StoreService from '../services/store'; -import { findNonConflictingFilename } from '../utils/file-name'; export default class SwitchSubmodeCommand extends HostBaseCommand< typeof BaseCommandModule.SwitchSubmodeInput, @@ -53,7 +54,7 @@ export default class SwitchSubmodeCommand extends HostBaseCommand< case Submodes.Interact: await this.operatorModeStateService.updateCodePath(null); break; - case Submodes.Code: + case Submodes.Code: { let codePath = input.codePath ?? (this.lastCardInRightMostStack @@ -95,6 +96,7 @@ export default class SwitchSubmodeCommand extends HostBaseCommand< } await this.operatorModeStateService.updateCodePath(finalCodeUrl); break; + } default: throw new Error(`invalid submode specified: ${input.submode}`); } diff --git a/packages/host/tests/integration/commands/switch-submode-test.gts b/packages/host/tests/integration/commands/switch-submode-test.gts index 179ee53349..9befc3e0d8 100644 --- a/packages/host/tests/integration/commands/switch-submode-test.gts +++ b/packages/host/tests/integration/commands/switch-submode-test.gts @@ -170,7 +170,11 @@ module('Integration | commands | switch-submode', function (hooks) { let fileUrl = `${testRealmURL}existing-file.gts`; let newFileUrl = `${testRealmURL}existing-file-1.gts`; - await cardService.saveSource(new URL(fileUrl), 'existing content', 'create-file'); + await cardService.saveSource( + new URL(fileUrl), + 'existing content', + 'create-file', + ); let result = await switchSubmodeCommand.execute({ submode: 'code', @@ -181,7 +185,10 @@ module('Integration | commands | switch-submode', function (hooks) { assert.ok(result, 'returns a result card with the new filename'); assert.strictEqual(result?.codePath, newFileUrl); assert.strictEqual(result?.requestedCodePath, fileUrl); - assert.strictEqual(operatorModeStateService.state?.codePath?.href, newFileUrl); + assert.strictEqual( + operatorModeStateService.state?.codePath?.href, + newFileUrl, + ); let { status, content } = await cardService.getSource(new URL(newFileUrl)); assert.strictEqual(status, 200); diff --git a/packages/runtime-common/ai/prompt.ts b/packages/runtime-common/ai/prompt.ts index 3423a4829c..e7bb992bf1 100644 --- a/packages/runtime-common/ai/prompt.ts +++ b/packages/runtime-common/ai/prompt.ts @@ -868,7 +868,8 @@ function getSwitchSubmodeInstruction( let parsed: { data?: any } | undefined; let cardContent = cardPayload.content ?? cardPayload; try { - parsed = typeof cardContent === 'string' ? JSON.parse(cardContent) : cardContent; + parsed = + typeof cardContent === 'string' ? JSON.parse(cardContent) : cardContent; } catch { return undefined; } From 500da651f1e59979efb5435c5c1bb78a214c131a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matic=20Jurgli=C4=8D?= Date: Tue, 20 Jan 2026 13:56:56 +0100 Subject: [PATCH 3/5] Simplify condition Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/host/app/commands/switch-submode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/host/app/commands/switch-submode.ts b/packages/host/app/commands/switch-submode.ts index 56657629e3..fe84928ee8 100644 --- a/packages/host/app/commands/switch-submode.ts +++ b/packages/host/app/commands/switch-submode.ts @@ -88,7 +88,7 @@ export default class SwitchSubmodeCommand extends HostBaseCommand< requestedCodePath: codeUrl.href, }); } - } else if (status !== 200) { + } else { throw new Error( `Error checking if file exists at ${codeUrl}: ${status}`, ); From 621a1dc0ff0983aa81e10d64c62fa31e599ae8ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matic=20Jurgli=C4=8D?= Date: Tue, 20 Jan 2026 13:57:43 +0100 Subject: [PATCH 4/5] Throw an error when non-conflicting file name cannot be found Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- packages/host/app/utils/file-name.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/host/app/utils/file-name.ts b/packages/host/app/utils/file-name.ts index 42db1a858b..f22bea7826 100644 --- a/packages/host/app/utils/file-name.ts +++ b/packages/host/app/utils/file-name.ts @@ -14,7 +14,16 @@ export async function findNonConflictingFilename( } } - return `${baseName}-${maxAttempts}${extension}`; + const finalCandidateUrl = `${baseName}-${maxAttempts}${extension}`; + const finalExists = await fileExists(finalCandidateUrl); + + if (!finalExists) { + return finalCandidateUrl; + } + + throw new Error( + `Unable to find non-conflicting filename for "${fileUrl}" after ${maxAttempts} attempts.`, + ); } function parseFilename(fileUrl: string): { From ad904ddc80bf7b24a1df2c776caf39f1a49bcd4d Mon Sep 17 00:00:00 2001 From: Matic Jurglic Date: Tue, 20 Jan 2026 14:04:51 +0100 Subject: [PATCH 5/5] Add test case --- .../commands/switch-submode-test.gts | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/host/tests/integration/commands/switch-submode-test.gts b/packages/host/tests/integration/commands/switch-submode-test.gts index 9befc3e0d8..39a525859d 100644 --- a/packages/host/tests/integration/commands/switch-submode-test.gts +++ b/packages/host/tests/integration/commands/switch-submode-test.gts @@ -194,4 +194,40 @@ module('Integration | commands | switch-submode', function (hooks) { assert.strictEqual(status, 200); assert.strictEqual(content, ''); }); + + test('createFile reuses an existing blank file', async function (assert) { + assert.expect(5); + + let commandService = getService('command-service'); + let cardService = getService('card-service'); + let operatorModeStateService = getService('operator-mode-state-service'); + operatorModeStateService.restore({ + stacks: [[]], + submode: 'interact', + }); + let switchSubmodeCommand = new SwitchSubmodeCommand( + commandService.commandContext, + ); + let fileUrl = `${testRealmURL}empty-file.gts`; + + await cardService.saveSource(new URL(fileUrl), '', 'create-file'); + + let result = await switchSubmodeCommand.execute({ + submode: 'code', + codePath: fileUrl, + createFile: true, + }); + + assert.strictEqual(operatorModeStateService.state?.codePath?.href, fileUrl); + assert.notOk(result, 'no result card when using existing blank file'); + + let { status, content } = await cardService.getSource(new URL(fileUrl)); + assert.strictEqual(status, 200); + assert.strictEqual(content, ''); + + let nonConflicting = await cardService.getSource( + new URL(`${testRealmURL}empty-file-1.gts`), + ); + assert.strictEqual(nonConflicting.status, 404); + }); });