From 2b2a6c49d5b03f4a38051ca1191e563f38bc2733 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Mon, 9 Dec 2024 17:59:30 -0500 Subject: [PATCH 1/9] WIP Extract commands for CreateAiAssistantRoom and AddSkillsToRoom --- packages/base/command.gts | 15 +++ .../create-product-requirements-command.ts | 29 ++++- .../host/app/commands/add-skills-to-room.ts | 58 ++++++++++ .../app/commands/create-ai-assistant-room.ts | 56 ++++++++++ packages/host/app/commands/index.ts | 11 ++ .../app/components/ai-assistant/panel.gts | 33 +++--- packages/host/app/components/matrix/room.gts | 15 ++- packages/host/app/services/matrix-service.ts | 101 ++---------------- .../host/tests/acceptance/commands-test.gts | 23 +++- packages/host/tests/cards/person.gts | 12 ++- packages/runtime-common/commands.ts | 4 +- 11 files changed, 240 insertions(+), 117 deletions(-) create mode 100644 packages/host/app/commands/add-skills-to-room.ts create mode 100644 packages/host/app/commands/create-ai-assistant-room.ts diff --git a/packages/base/command.gts b/packages/base/command.gts index 38cbcf1bde..ca578415d2 100644 --- a/packages/base/command.gts +++ b/packages/base/command.gts @@ -6,11 +6,13 @@ import { contains, field, linksTo, + linksToMany, primitive, queryableValue, } from './card-api'; import CodeRefField from './code-ref'; import BooleanField from './boolean'; +import { SkillCard } from './skill-card'; export type CommandStatus = 'applied' | 'ready' | 'applying'; @@ -75,3 +77,16 @@ export class CreateInstanceInput extends CardDef { @field module = contains(CodeRefField); @field realm = contains(StringField); } + +export class CreateAIAssistantRoomInput extends CardDef { + @field name = contains(StringField); +} + +export class CreateAIAssistantRoomResult extends CardDef { + @field roomId = contains(StringField); +} + +export class AddSkillsToRoomInput extends CardDef { + @field roomId = contains(StringField); + @field skills = linksToMany(SkillCard); +} diff --git a/packages/catalog-realm/AiAppGenerator/create-product-requirements-command.ts b/packages/catalog-realm/AiAppGenerator/create-product-requirements-command.ts index d87ce8340b..fa9c3cc9da 100644 --- a/packages/catalog-realm/AiAppGenerator/create-product-requirements-command.ts +++ b/packages/catalog-realm/AiAppGenerator/create-product-requirements-command.ts @@ -11,6 +11,8 @@ import { SkillCard } from 'https://cardstack.com/base/skill-card'; import SaveCardCommand from '@cardstack/boxel-host/commands/save-card'; import PatchCardCommand from '@cardstack/boxel-host/commands/patch-card'; import ReloadCardCommand from '@cardstack/boxel-host/commands/reload-card'; +import CreateAIAssistantRoomCommand from '@cardstack/boxel-host/commands/create-ai-assistant-room'; +import AddSkillsToRoomCommand from '../../host/app/commands/add-skills-to-room'; export class CreateProductRequirementsInput extends CardDef { @field targetAudience = contains(StringField); @@ -40,10 +42,10 @@ export default class CreateProductRequirementsInstance extends Command< Update the appTitle. Update the prompt to be grammatically accurate. Description should be 1 or 2 short sentences. - In overview, provide 1 or 2 paragraph summary of the most important ways this app will meet the needs of the target audience. The capabilites of the platform allow creating types that can be linked to other types, and creating fields. - + In overview, provide 1 or 2 paragraph summary of the most important ways this app will meet the needs of the target audience. The capabilites of the platform allow creating types that can be linked to other types, and creating fields. + For the schema, consider the types required. Write out the schema as a mermaid class diagram. - + NEVER offer to update the card, you MUST call patchCard in your response.`, }); } @@ -71,11 +73,28 @@ export default class CreateProductRequirementsInstance extends Command< cardType: ProductRequirementDocument, }); - let { roomId } = await this.commandContext.sendAiAssistantMessage({ + let createRoomCommand = new CreateAIAssistantRoomCommand( + this.commandContext, + ); + let { roomId } = await createRoomCommand.execute( + new (await createRoomCommand.getInputType())({ + name: 'Product Requirements Doc Creation', + }), + ); + let addSkillsToRoomCommand = new AddSkillsToRoomCommand( + this.commandContext, + ); + await addSkillsToRoomCommand.execute( + new (await addSkillsToRoomCommand.getInputType())({ + roomId, + skills: [this.skillCard], + }), + ); + await this.commandContext.sendAiAssistantMessage({ + roomId, show: false, // maybe? open the side panel prompt: this.createPrompt(input), attachedCards: [prdCard], - skillCards: [this.skillCard], commands: [{ command: patchPRDCommand, autoExecute: true }], // this should persist over multiple messages, matrix service is responsible to tracking whic }); diff --git a/packages/host/app/commands/add-skills-to-room.ts b/packages/host/app/commands/add-skills-to-room.ts new file mode 100644 index 0000000000..c88e2d1647 --- /dev/null +++ b/packages/host/app/commands/add-skills-to-room.ts @@ -0,0 +1,58 @@ +import { service } from '@ember/service'; + +import type * as BaseCommandModule from 'https://cardstack.com/base/command'; + +import HostBaseCommand from '../lib/host-base-command'; + +import { SKILLS_STATE_EVENT_TYPE } from '../services/matrix-service'; + +import type MatrixService from '../services/matrix-service'; + +export default class AddSkillsToRoomCommand extends HostBaseCommand< + BaseCommandModule.AddSkillsToRoomInput, + undefined +> { + @service private declare matrixService: MatrixService; + + async getInputType() { + let commandModule = await this.loadCommandModule(); + const { AddSkillsToRoomInput } = commandModule; + return AddSkillsToRoomInput; + } + + protected async run( + input: BaseCommandModule.AddSkillsToRoomInput, + ): Promise { + let { client } = this.matrixService; + let { roomId, skills } = input; + let roomSkillEventIds = await this.matrixService.addCardsToRoom( + skills, + roomId, + this.matrixService.skillCardHashes, + { includeComputeds: true, maybeRelativeURL: null }, + ); + let skillEventIdsStateEvent: Record = {}; + try { + skillEventIdsStateEvent = await client.getStateEvent( + roomId, + SKILLS_STATE_EVENT_TYPE, + '', + ); + } catch (e: unknown) { + if (e instanceof Error && 'errcode' in e && e.errcode === 'M_NOT_FOUND') { + // this is fine, it just means the state event doesn't exist yet + } else { + throw e; + } + } + client.sendStateEvent(roomId, SKILLS_STATE_EVENT_TYPE, { + enabledEventIds: [ + ...new Set([ + ...(skillEventIdsStateEvent?.enabledEventIds || []), + ...roomSkillEventIds, + ]), + ], + disabledEventIds: [...(skillEventIdsStateEvent?.disabledEventIds || [])], + }); + } +} diff --git a/packages/host/app/commands/create-ai-assistant-room.ts b/packages/host/app/commands/create-ai-assistant-room.ts new file mode 100644 index 0000000000..202857da80 --- /dev/null +++ b/packages/host/app/commands/create-ai-assistant-room.ts @@ -0,0 +1,56 @@ +import { service } from '@ember/service'; + +import format from 'date-fns/format'; + +import { aiBotUsername } from '@cardstack/runtime-common'; + +import type * as BaseCommandModule from 'https://cardstack.com/base/command'; + +import HostBaseCommand from '../lib/host-base-command'; + +import { AI_BOT_POWER_LEVEL } from '../services/matrix-service'; + +import type MatrixService from '../services/matrix-service'; + +export default class CreateAIAssistantRoomCommand extends HostBaseCommand< + BaseCommandModule.CreateAIAssistantRoomInput, + BaseCommandModule.CreateAIAssistantRoomResult +> { + @service private declare matrixService: MatrixService; + + async getInputType() { + let commandModule = await this.loadCommandModule(); + const { CreateAIAssistantRoomInput } = commandModule; + return CreateAIAssistantRoomInput; + } + + protected async run( + input: BaseCommandModule.CreateAIAssistantRoomInput, + ): Promise { + let { client, matrixSDK } = this.matrixService; + let userId = client.getUserId(); + if (!userId) { + throw new Error( + `bug: there is no userId associated with the matrix client`, + ); + } + let server = userId!.split(':')[1]; + let aiBotFullId = `@${aiBotUsername}:${server}`; + let { room_id: roomId } = await client.createRoom({ + preset: matrixSDK.Preset.PrivateChat, + invite: [aiBotFullId], + name: input.name, + topic: undefined, + room_alias_name: encodeURIComponent( + `${input.name} - ${format( + new Date(), + "yyyy-MM-dd'T'HH:mm:ss.SSSxxx", + )} - ${userId}`, + ), + }); + client.setPowerLevel(roomId, aiBotFullId, AI_BOT_POWER_LEVEL, null); + let commandModule = await this.loadCommandModule(); + const { CreateAIAssistantRoomResult } = commandModule; + return new CreateAIAssistantRoomResult({ roomId }); + } +} diff --git a/packages/host/app/commands/index.ts b/packages/host/app/commands/index.ts index 92929b77e0..09b2d08fc5 100644 --- a/packages/host/app/commands/index.ts +++ b/packages/host/app/commands/index.ts @@ -1,12 +1,23 @@ import { VirtualNetwork } from '@cardstack/runtime-common'; +import * as AddSkillsToRoomCommandModule from './add-skills-to-room'; +import * as CreateAIAssistantRoomCommandModule from './create-ai-assistant-room'; import * as PatchCardCommandModule from './patch-card'; import * as ReloadCardCommandModule from './reload-card'; import * as SaveCardCommandModule from './save-card'; import * as ShowCardCommandModule from './show-card'; import * as SwitchSubmodeCommandModule from './switch-submode'; import * as WriteTextFileCommandModule from './write-text-file'; + export function shimHostCommands(virtualNetwork: VirtualNetwork) { + virtualNetwork.shimModule( + '@cardstack/boxel-host/commands/add-skills-to-room', + AddSkillsToRoomCommandModule, + ); + virtualNetwork.shimModule( + '@cardstack/boxel-host/commands/create-ai-assistant-room', + CreateAIAssistantRoomCommandModule, + ); virtualNetwork.shimModule( '@cardstack/boxel-host/commands/patch-card', PatchCardCommandModule, diff --git a/packages/host/app/components/ai-assistant/panel.gts b/packages/host/app/components/ai-assistant/panel.gts index f300510721..a57c00bfdf 100644 --- a/packages/host/app/components/ai-assistant/panel.gts +++ b/packages/host/app/components/ai-assistant/panel.gts @@ -22,6 +22,7 @@ import { DropdownArrowFilled, IconX } from '@cardstack/boxel-ui/icons'; import { aiBotUsername } from '@cardstack/runtime-common'; +import CreateAIAssistantRoomCommand from '@cardstack/host/commands/create-ai-assistant-room'; import NewSession from '@cardstack/host/components/ai-assistant/new-session'; import AiAssistantPastSessionsList from '@cardstack/host/components/ai-assistant/past-sessions'; import RenameSession from '@cardstack/host/components/ai-assistant/rename-session'; @@ -35,6 +36,7 @@ import { eventDebounceMs, } from '@cardstack/host/lib/matrix-utils'; +import CommandService from '@cardstack/host/services/command-service'; import type MatrixService from '@cardstack/host/services/matrix-service'; import type MonacoService from '@cardstack/host/services/monaco-service'; import { type MonacoSDK } from '@cardstack/host/services/monaco-service'; @@ -360,6 +362,7 @@ export default class AiAssistantPanel extends Component { @service private declare matrixService: MatrixService; @service private declare monacoService: MonacoService; @service private declare router: RouterService; + @service private declare commandService: CommandService; @tracked private currentRoomId: string | undefined; @tracked private isShowingPastSessions = false; @@ -420,22 +423,24 @@ export default class AiAssistantPanel extends Component { return; } let newRoomName = 'New AI Assistant Chat'; - this.doCreateRoom.perform(newRoomName, [aiBotUsername]); + this.doCreateRoom.perform(newRoomName); } - private doCreateRoom = restartableTask( - async (name: string, invites: string[]) => { - try { - let newRoomId = await this.matrixService.createRoom(name, invites); - window.localStorage.setItem(newSessionIdPersistenceKey, newRoomId); - this.enterRoom(newRoomId); - } catch (e) { - console.log(e); - this.displayRoomError = true; - this.currentRoomId = undefined; - } - }, - ); + private doCreateRoom = restartableTask(async (name: string) => { + try { + let command = new CreateAIAssistantRoomCommand( + this.commandService.commandContext, + ); + let InputType = await command.getInputType(); + let { roomId } = await command.execute(new InputType({ name })); + window.localStorage.setItem(newSessionIdPersistenceKey, roomId); + this.enterRoom(roomId); + } catch (e) { + console.log(e); + this.displayRoomError = true; + this.currentRoomId = undefined; + } + }); private get newSessionId() { let id = window.localStorage.getItem(newSessionIdPersistenceKey); diff --git a/packages/host/app/components/matrix/room.gts b/packages/host/app/components/matrix/room.gts index dffea9b10e..2b5403b663 100644 --- a/packages/host/app/components/matrix/room.gts +++ b/packages/host/app/components/matrix/room.gts @@ -24,12 +24,14 @@ import { not } from '@cardstack/boxel-ui/helpers'; import { unixTime } from '@cardstack/runtime-common'; +import AddSkillsToRoomCommand from '@cardstack/host/commands/add-skills-to-room'; import { Message } from '@cardstack/host/lib/matrix-classes/message'; import type { StackItem } from '@cardstack/host/lib/stack-item'; import { getAutoAttachment } from '@cardstack/host/resources/auto-attached-card'; import { getRoom } from '@cardstack/host/resources/room'; import type CardService from '@cardstack/host/services/card-service'; +import type CommandService from '@cardstack/host/services/command-service'; import type MatrixService from '@cardstack/host/services/matrix-service'; import { type MonacoSDK } from '@cardstack/host/services/monaco-service'; import type OperatorModeStateService from '@cardstack/host/services/operator-mode-state-service'; @@ -169,6 +171,7 @@ export default class Room extends Component { @service private declare cardService: CardService; + @service private declare commandService: CommandService; @service private declare matrixService: MatrixService; @service private declare operatorModeStateService: OperatorModeStateService; @@ -623,8 +626,16 @@ export default class Room extends Component { return message.status === 'sending' || message.status === 'queued'; } - private attachSkill = (card: SkillCard) => { - this.matrixService.addSkillCardsToRoom(this.args.roomId, [card]); + private attachSkill = async (card: SkillCard) => { + let addSkillsToRoomCommand = new AddSkillsToRoomCommand( + this.commandService.commandContext, + ); + await addSkillsToRoomCommand.execute( + new (await addSkillsToRoomCommand.getInputType())({ + roomId: this.args.roomId, + skills: [card], + }), + ); }; } diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index ad99cdc7be..b448ac99c5 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -4,8 +4,6 @@ import { debounce } from '@ember/runloop'; import Service, { service } from '@ember/service'; import { cached, tracked } from '@glimmer/tracking'; -import format from 'date-fns/format'; - import { task } from 'ember-concurrency'; import window from 'ember-window-mock'; import { @@ -23,7 +21,6 @@ import { v4 as uuidv4 } from 'uuid'; import { type LooseSingleCardDocument, markdownToHtml, - aiBotUsername, splitStringIntoChunks, baseRealm, loaderFor, @@ -86,11 +83,11 @@ import type ResetService from './reset'; import type * as MatrixSDK from 'matrix-js-sdk'; const { matrixURL } = ENV; -const AI_BOT_POWER_LEVEL = 50; // this is required to set the room name +export const AI_BOT_POWER_LEVEL = 50; // this is required to set the room name const MAX_CARD_SIZE_KB = 60; const STATE_EVENTS_OF_INTEREST = ['m.room.create', 'm.room.name']; const DefaultSkillCards = [`${baseRealm.url}SkillCard/card-editing`]; -const SKILLS_STATE_EVENT_TYPE = 'com.cardstack.boxel.room.skills'; +export const SKILLS_STATE_EVENT_TYPE = 'com.cardstack.boxel.room.skills'; export type OperatorModeContext = { submode: Submode; @@ -132,7 +129,7 @@ export default class MatrixService extends Service { currentUserEventReadReceipts: TrackedMap = new TrackedMap(); private cardHashes: Map = new Map(); // hashes <> event id - private skillCardHashes: Map = new Map(); // hashes <> event id + skillCardHashes: Map = new Map(); // hashes <> event id private defaultSkills: SkillCard[] = []; constructor(owner: Owner) { @@ -222,7 +219,7 @@ export default class MatrixService extends Service { return this.cardAPIModule.module as typeof CardAPI; } - private get matrixSDK() { + get matrixSDK() { if (!this.#matrixSDK) { throw new Error(`cannot use matrix SDK before it has loaded`); } @@ -425,41 +422,6 @@ export default class MatrixService extends Service { return this.client.createRealmSession(realmURL); } - async createRoom( - name: string, - invites: string[], // these can be local names - topic?: string, - ): Promise { - let userId = this.client.getUserId(); - if (!userId) { - throw new Error( - `bug: there is no userId associated with the matrix client`, - ); - } - let invite = invites.map((i) => - i.startsWith('@') ? i : `@${i}:${userId!.split(':')[1]}`, - ); - let { room_id: roomId } = await this.client.createRoom({ - preset: this.matrixSDK.Preset.PrivateChat, - invite, - name, - topic, - room_alias_name: encodeURIComponent( - `${name} - ${format(new Date(), "yyyy-MM-dd'T'HH:mm:ss.SSSxxx")} - ${ - this.userId - }`, - ), - }); - invites.map((i) => { - let fullId = i.startsWith('@') ? i : `@${i}:${userId!.split(':')[1]}`; - if (i === aiBotUsername) { - this.client.setPowerLevel(roomId, fullId, AI_BOT_POWER_LEVEL, null); - } - }); - this.addSkillCardsToRoom(roomId, await this.loadDefaultSkills()); - return roomId; - } - private async sendEvent( roomId: string, eventType: string, @@ -529,12 +491,12 @@ export default class MatrixService extends Service { } } - private async getCardEventIds( + async addCardsToRoom( cards: CardDef[], roomId: string, cardHashes: Map, opts?: CardAPI.SerializeOpts, - ) { + ): Promise { if (!cards.length) { return []; } @@ -598,7 +560,7 @@ export default class MatrixService extends Service { } } - let attachedCardsEventIds = await this.getCardEventIds( + let attachedCardsEventIds = await this.addCardsToRoom( attachedCards, roomId, this.cardHashes, @@ -622,41 +584,6 @@ export default class MatrixService extends Service { } as CardMessageContent); } - public async addSkillCardsToRoom( - roomId: string, - skillCards: SkillCard[], - ): Promise { - let attachedSkillEventIds = await this.getCardEventIds( - skillCards, - roomId, - this.skillCardHashes, - { includeComputeds: true, maybeRelativeURL: null }, - ); - let skillEventIdsStateEvent: Record = {}; - try { - skillEventIdsStateEvent = await this.client.getStateEvent( - roomId, - SKILLS_STATE_EVENT_TYPE, - '', - ); - } catch (e: unknown) { - if (e instanceof Error && 'errcode' in e && e.errcode === 'M_NOT_FOUND') { - // this is fine, it just means the state event doesn't exist yet - } else { - throw e; - } - } - this.client.sendStateEvent(roomId, SKILLS_STATE_EVENT_TYPE, { - enabledEventIds: [ - ...new Set([ - ...(skillEventIdsStateEvent?.enabledEventIds || []), - ...attachedSkillEventIds, - ]), - ], - disabledEventIds: [...(skillEventIdsStateEvent?.disabledEventIds || [])], - }); - } - public updateSkillIsActive = async ( roomId: string, skillEventId: string, @@ -689,21 +616,13 @@ export default class MatrixService extends Service { }; public async sendAiAssistantMessage(params: { - roomId?: string; // if falsy we create a new room - show?: boolean; // if truthy, ensure the side panel to the room + roomId: string; + show?: boolean; // if truthy, ensure the side panel is open to the room prompt: string; attachedCards?: CardDef[]; - skillCards?: SkillCard[]; commands?: { command: Command; autoExecute: boolean }[]; }): Promise<{ roomId: string }> { let roomId = params.roomId; - if (!roomId) { - roomId = await this.createRoom('AI Assistant', [aiBotUsername]); - } - if (params.skillCards?.length) { - this.addSkillCardsToRoom(roomId, params.skillCards); - } - let html = markdownToHtml(params.prompt); let mappings = await basicMappings(this.loaderService.loader); let tools = []; @@ -729,7 +648,7 @@ export default class MatrixService extends Service { }); } - let attachedCardsEventIds = await this.getCardEventIds( + let attachedCardsEventIds = await this.addCardsToRoom( params.attachedCards ?? [], roomId, this.cardHashes, diff --git a/packages/host/tests/acceptance/commands-test.gts b/packages/host/tests/acceptance/commands-test.gts index a4d8502630..ee9e44c8bb 100644 --- a/packages/host/tests/acceptance/commands-test.gts +++ b/packages/host/tests/acceptance/commands-test.gts @@ -16,6 +16,7 @@ import { GridContainer } from '@cardstack/boxel-ui/components'; import { baseRealm, Command } from '@cardstack/runtime-common'; +import CreateAIAssistantRoomCommand from '@cardstack/host/commands/create-ai-assistant-room'; import PatchCardCommand from '@cardstack/host/commands/patch-card'; import SaveCardCommand from '@cardstack/host/commands/save-card'; import ShowCardCommand from '@cardstack/host/commands/show-card'; @@ -148,7 +149,16 @@ module('Acceptance | Commands tests', function (hooks) { cardType: Meeting, }); + let createAIAssistantRoomCommand = new CreateAIAssistantRoomCommand( + this.commandContext, + ); + let { roomId } = await createAIAssistantRoomCommand.execute( + new (await createAIAssistantRoomCommand.getInputType())({ + name: 'AI Assistant Room', + }), + ); await this.commandContext.sendAiAssistantMessage({ + roomId, prompt: `Change the topic of the meeting to "${input.topic}"`, attachedCards: [meeting], commands: [{ command: patchCardCommand, autoExecute: true }], @@ -201,14 +211,25 @@ module('Acceptance | Commands tests', function (hooks) { }); static isolated = class Isolated extends Component { - runSwitchToCodeModeCommandViaAiAssistant = (autoExecute: boolean) => { + runSwitchToCodeModeCommandViaAiAssistant = async ( + autoExecute: boolean, + ) => { let commandContext = this.args.context?.commandContext; if (!commandContext) { console.error('No command context found'); return; } + let createAIAssistantRoomCommand = new CreateAIAssistantRoomCommand( + commandContext, + ); + let { roomId } = await createAIAssistantRoomCommand.execute( + new (await createAIAssistantRoomCommand.getInputType())({ + name: 'AI Assistant Room', + }), + ); let switchSubmodeCommand = new SwitchSubmodeCommand(commandContext); commandContext.sendAiAssistantMessage({ + roomId, prompt: 'Switch to code mode', commands: [{ command: switchSubmodeCommand, autoExecute }], }); diff --git a/packages/host/tests/cards/person.gts b/packages/host/tests/cards/person.gts index 5966d1a1c5..5ef198e599 100644 --- a/packages/host/tests/cards/person.gts +++ b/packages/host/tests/cards/person.gts @@ -1,5 +1,6 @@ import { on } from '@ember/modifier'; +import CreateAIAssistantRoomCommand from '@cardstack/boxel-host/commands/create-ai-assistant-room'; import SwitchSubmodeCommand from '@cardstack/boxel-host/commands/switch-submode'; import { @@ -30,14 +31,23 @@ export class Person extends CardDef { @field description = contains(StringCard, { computeVia: () => 'Person' }); static isolated = class Isolated extends Component { - runSwitchToCodeModeCommandViaAiAssistant = () => { + runSwitchToCodeModeCommandViaAiAssistant = async () => { let commandContext = this.args.context?.commandContext; if (!commandContext) { console.error('No command context found'); return; } + let createAIAssistantRoomCommand = new CreateAIAssistantRoomCommand( + commandContext, + ); + let { roomId } = await createAIAssistantRoomCommand.execute( + new (await createAIAssistantRoomCommand.getInputType())({ + name: 'AI Assistant Room', + }), + ); let switchSubmodeCommand = new SwitchSubmodeCommand(commandContext); commandContext.sendAiAssistantMessage({ + roomId, prompt: 'Switch to code mode', commands: [{ command: switchSubmodeCommand, autoExecute: true }], }); diff --git a/packages/runtime-common/commands.ts b/packages/runtime-common/commands.ts index 2a8cdd3e64..210168f5a0 100644 --- a/packages/runtime-common/commands.ts +++ b/packages/runtime-common/commands.ts @@ -1,7 +1,6 @@ import { Deferred } from './deferred'; import type * as CardAPI from 'https://cardstack.com/base/card-api'; import { CardDef } from 'https://cardstack.com/base/card-api'; -import { SkillCard } from 'https://cardstack.com/base/skill-card'; import { AttributesSchema, CardSchema, @@ -10,11 +9,10 @@ import { export interface CommandContext { sendAiAssistantMessage: (params: { - roomId?: string; // if falsy we create a new room + roomId: string; show?: boolean; // if truthy, ensure the side panel to the room prompt: string; attachedCards?: CardDef[]; - skillCards?: SkillCard[]; commands?: { command: Command; autoExecute: boolean }[]; }) => Promise<{ roomId: string }>; } From 95d0d2d594b00289828bd0c4470657c95be907ac Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 10 Dec 2024 12:23:25 -0500 Subject: [PATCH 2/9] Clean up command/service layer boundaries --- .../host/app/commands/add-skills-to-room.ts | 28 +++++------ .../app/commands/create-ai-assistant-room.ts | 15 +++--- .../app/components/ai-assistant/panel.gts | 1 + packages/host/app/services/matrix-service.ts | 47 +++++++++++++++++-- 4 files changed, 63 insertions(+), 28 deletions(-) diff --git a/packages/host/app/commands/add-skills-to-room.ts b/packages/host/app/commands/add-skills-to-room.ts index c370304be0..b121968b5f 100644 --- a/packages/host/app/commands/add-skills-to-room.ts +++ b/packages/host/app/commands/add-skills-to-room.ts @@ -23,17 +23,16 @@ export default class AddSkillsToRoomCommand extends HostBaseCommand< protected async run( input: BaseCommandModule.AddSkillsToRoomInput, ): Promise { - let { client } = this.matrixService; + let { matrixService } = this; let { roomId, skills } = input; - let roomSkillEventIds = await this.matrixService.addCardsToRoom( + let roomSkillEventIds = await matrixService.addSkillCardsToRoomHistory( skills, roomId, - this.matrixService.skillCardHashes, { includeComputeds: true, maybeRelativeURL: null }, ); let skillEventIdsStateEvent: Record = {}; try { - skillEventIdsStateEvent = await client.getStateEvent( + skillEventIdsStateEvent = await matrixService.getStateEvent( roomId, SKILLS_STATE_EVENT_TYPE, '', @@ -45,19 +44,14 @@ export default class AddSkillsToRoomCommand extends HostBaseCommand< throw e; } } - let roomData = this.matrixService.ensureRoomData(roomId); - await roomData.mutex.dispatch(async () => { - client.sendStateEvent(roomId, SKILLS_STATE_EVENT_TYPE, { - enabledEventIds: [ - ...new Set([ - ...(skillEventIdsStateEvent?.enabledEventIds || []), - ...roomSkillEventIds, - ]), - ], - disabledEventIds: [ - ...(skillEventIdsStateEvent?.disabledEventIds || []), - ], - }); + await matrixService.sendStateEvent(roomId, SKILLS_STATE_EVENT_TYPE, { + enabledEventIds: [ + ...new Set([ + ...(skillEventIdsStateEvent?.enabledEventIds || []), + ...roomSkillEventIds, + ]), + ], + disabledEventIds: [...(skillEventIdsStateEvent?.disabledEventIds || [])], }); } } diff --git a/packages/host/app/commands/create-ai-assistant-room.ts b/packages/host/app/commands/create-ai-assistant-room.ts index 0f911622fa..980f7256eb 100644 --- a/packages/host/app/commands/create-ai-assistant-room.ts +++ b/packages/host/app/commands/create-ai-assistant-room.ts @@ -27,8 +27,8 @@ export default class CreateAIAssistantRoomCommand extends HostBaseCommand< protected async run( input: BaseCommandModule.CreateAIAssistantRoomInput, ): Promise { - let { client, matrixSDK } = this.matrixService; - let userId = client.getUserId(); + let { matrixService } = this; + let { matrixSDK, userId } = matrixService; if (!userId) { throw new Error( `bug: there is no userId associated with the matrix client`, @@ -36,7 +36,7 @@ export default class CreateAIAssistantRoomCommand extends HostBaseCommand< } let server = userId!.split(':')[1]; let aiBotFullId = `@${aiBotUsername}:${server}`; - let { room_id: roomId } = await client.createRoom({ + let { room_id: roomId } = await matrixService.createRoom({ preset: matrixSDK.Preset.PrivateChat, invite: [aiBotFullId], name: input.name, @@ -48,10 +48,11 @@ export default class CreateAIAssistantRoomCommand extends HostBaseCommand< )} - ${userId}`, ), }); - let roomData = this.matrixService.ensureRoomData(roomId); - roomData.mutex.dispatch(async () => { - client.setPowerLevel(roomId, aiBotFullId, AI_BOT_POWER_LEVEL, null); - }); + await this.matrixService.setPowerLevel( + roomId, + aiBotFullId, + AI_BOT_POWER_LEVEL, + ); let commandModule = await this.loadCommandModule(); const { CreateAIAssistantRoomResult } = commandModule; return new CreateAIAssistantRoomResult({ roomId }); diff --git a/packages/host/app/components/ai-assistant/panel.gts b/packages/host/app/components/ai-assistant/panel.gts index a57c00bfdf..38e41e275f 100644 --- a/packages/host/app/components/ai-assistant/panel.gts +++ b/packages/host/app/components/ai-assistant/panel.gts @@ -411,6 +411,7 @@ export default class AiAssistantPanel extends Component { private loadRoomsTask = restartableTask(async () => { await this.matrixService.flushMembership; await this.matrixService.flushTimeline; + await this.matrixService.flushRoomState; await Promise.all([...this.roomResources.values()].map((r) => r.loading)); this.enterRoomInitially(); }); diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index eb31318bde..8775b998a0 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -108,7 +108,7 @@ export default class MatrixService extends Service { @tracked private _isNewUser = false; @tracked private postLoginCompleted = false; - profile = getMatrixProfile(this, () => this.client.getUserId()); + profile = getMatrixProfile(this, () => this.userId); private roomDataMap: TrackedMap = new TrackedMap(); @@ -129,7 +129,7 @@ export default class MatrixService extends Service { currentUserEventReadReceipts: TrackedMap = new TrackedMap(); private cardHashes: Map = new Map(); // hashes <> event id - skillCardHashes: Map = new Map(); // hashes <> event id + private skillCardHashes: Map = new Map(); // hashes <> event id private defaultSkills: SkillCard[] = []; constructor(owner: Owner) { @@ -190,7 +190,7 @@ export default class MatrixService extends Service { return this.client.isLoggedIn() && this.postLoginCompleted; } - get client() { + private get client() { if (!this._client) { throw new Error(`cannot use matrix client before matrix SDK has loaded`); } @@ -507,6 +507,14 @@ export default class MatrixService extends Service { } } + async addSkillCardsToRoomHistory( + skills: SkillCard[], + roomId: string, + opts?: CardAPI.SerializeOpts, + ): Promise { + return this.addCardsToRoom(skills, roomId, this.skillCardHashes, opts); + } + async addCardsToRoom( cards: CardDef[], roomId: string, @@ -858,6 +866,10 @@ export default class MatrixService extends Service { } } + async createRoom(opts: MatrixSDK.ICreateRoomOpts) { + return this.client.createRoom(opts); + } + async createCard( codeRef: ResolvedCodeRef, attr: Record, @@ -878,6 +890,33 @@ export default class MatrixService extends Service { return card; } + async setPowerLevel(roomId: string, userId: string, powerLevel: number) { + let roomData = this.ensureRoomData(roomId); + await roomData.mutex.dispatch(async () => { + return this.client.setPowerLevel(roomId, userId, powerLevel); + }); + } + + async getStateEvent( + roomId: string, + eventType: string, + stateKey: string = '', + ) { + return this.client.getStateEvent(roomId, eventType, stateKey); + } + + async sendStateEvent( + roomId: string, + eventType: string, + content: Record, + stateKey: string = '', + ) { + let roomData = this.ensureRoomData(roomId); + await roomData.mutex.dispatch(async () => { + return this.client.sendStateEvent(roomId, eventType, content, stateKey); + }); + } + private addRoomEvent(event: TempEvent, oldEventId?: string) { let { room_id: roomId } = event; @@ -890,7 +929,7 @@ export default class MatrixService extends Service { roomData.addEvent(event, oldEventId); } - ensureRoomData(roomId: string) { + private ensureRoomData(roomId: string) { let roomData = this.getRoomData(roomId); if (!roomData) { roomData = new Room(); From 1dd15dab1a80710f98eeee58a7d536f80e664ba7 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 10 Dec 2024 13:53:44 -0500 Subject: [PATCH 3/9] Test fixes --- .../AiAppGenerator/generate-code-command.ts | 14 +++++++++++--- .../catalog-realm/product-requirement-document.gts | 10 +++++++++- .../components/ai-module-creation-test.gts | 12 ++++++++---- .../realm-indexing-and-querying-test.gts | 1 + 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/packages/catalog-realm/AiAppGenerator/generate-code-command.ts b/packages/catalog-realm/AiAppGenerator/generate-code-command.ts index 279935d88a..e692bcfbbc 100644 --- a/packages/catalog-realm/AiAppGenerator/generate-code-command.ts +++ b/packages/catalog-realm/AiAppGenerator/generate-code-command.ts @@ -2,13 +2,13 @@ import { CardDef, field, linksTo, - containsMany, contains, } from 'https://cardstack.com/base/card-api'; import { Command } from '@cardstack/runtime-common'; import { SkillCard } from 'https://cardstack.com/base/skill-card'; import StringField from 'https://cardstack.com/base/string'; import { ProductRequirementDocument } from '../product-requirement-document'; +import AddSkillsToRoomCommand from '@cardstack/boxel-host/commands/add-skills-to-room'; export class GenerateCodeInput extends CardDef { @field productRequirements = linksTo(() => ProductRequirementDocument); @@ -91,6 +91,7 @@ import { and, bool, cn } from '@cardstack/boxel-ui/helpers'; import { baseRealm, getCard } from '@cardstack/runtime-common'; import { hash } from '@ember/helper'; import { on } from '@ember/modifier'; +import AddSkillsToRoomCommand from '../../host/app/commands/add-skills-to-room'; import { action } from '@ember/object'; import type Owner from '@ember/owner'; import GlimmerComponent from '@glimmer/component'; @@ -254,14 +255,21 @@ import { on } from '@ember/modifier'; let constructApplicationCodeCommand = new ConstructApplicationCodeCommand( this.commandContext, ); - + let addSkillsToRoomCommand = new AddSkillsToRoomCommand( + this.commandContext, + ); + await addSkillsToRoomCommand.execute( + new (await addSkillsToRoomCommand.getInputType())({ + roomId: input.roomId, + skills: [this.skillCard], + }), + ); await this.commandContext.sendAiAssistantMessage({ roomId: input.roomId, show: false, // maybe? open the side panel prompt: 'Generate code for the application given the product requirements, you do not need to strictly follow the schema if it does not seem appropriate for the application.', attachedCards: [input.productRequirements], - skillCards: [this.skillCard], commands: [ { command: constructApplicationCodeCommand, autoExecute: true }, ], diff --git a/packages/catalog-realm/product-requirement-document.gts b/packages/catalog-realm/product-requirement-document.gts index 835a81648b..9c4cdb126f 100644 --- a/packages/catalog-realm/product-requirement-document.gts +++ b/packages/catalog-realm/product-requirement-document.gts @@ -17,9 +17,10 @@ import { tracked } from '@glimmer/tracking'; import { AppCard } from './app-card'; import ClipboardListIcon from '@cardstack/boxel-icons/clipboard-list'; -import WriteTextFileCommand from '@cardstack/boxel-host/commands/write-text-file'; +import CreateAIAssistantRoomCommand from '@cardstack/boxel-host/commands/create-ai-assistant-room'; import ShowCardCommand from '@cardstack/boxel-host/commands/show-card'; import SaveCardCommand from '@cardstack/boxel-host/commands/save-card'; +import WriteTextFileCommand from '@cardstack/boxel-host/commands/write-text-file'; import GenerateCodeCommand from './AiAppGenerator/generate-code-command'; import { GenerateCodeInput } from './AiAppGenerator/generate-code-command'; @@ -252,9 +253,16 @@ class Isolated extends Component { } this.errorMessage = ''; try { + let createRoomCommand = new CreateAIAssistantRoomCommand(commandContext); + let { roomId } = await createRoomCommand.execute( + new (await createRoomCommand.getInputType())({ + name: 'AI Assistant Room', + }), + ); let generateCodeCommand = new GenerateCodeCommand(commandContext); let generateCodeInput = new GenerateCodeInput({ productRequirements: this.args.model, + roomId, }); let { code, appName } = diff --git a/packages/host/tests/integration/components/ai-module-creation-test.gts b/packages/host/tests/integration/components/ai-module-creation-test.gts index d3a64878d5..8fdd59694e 100644 --- a/packages/host/tests/integration/components/ai-module-creation-test.gts +++ b/packages/host/tests/integration/components/ai-module-creation-test.gts @@ -163,7 +163,7 @@ module('Integration | create app module via ai-assistant', function (hooks) { await click('[data-test-generate-app]'); await click('[data-test-past-sessions-button]'); let newRoomButton = findAll('[data-test-enter-room]').filter((el) => - el.textContent?.includes('AI Assistant'), + el.textContent?.includes('AI Assistant Room'), )[0]; assert.ok(newRoomButton, 'new room button exists'); @@ -173,21 +173,25 @@ module('Integration | create app module via ai-assistant', function (hooks) { await click(`[data-test-enter-room="${roomId}"]`); assert - .dom(`[data-test-room-name="AI Assistant"] [data-test-message-idx="0"]`) + .dom( + `[data-test-room-name="AI Assistant Room"] [data-test-message-idx="0"]`, + ) .containsText('Generate code'); let events = getRoomEvents(roomId); let lastEvContent = events[events.length - 1].content as CardMessageContent; assert.strictEqual( lastEvContent.body, 'Generate code for the application given the product requirements, you do not need to strictly follow the schema if it does not seem appropriate for the application.', + 'Event content is correct', ); assert.strictEqual( getRoomState(roomId, 'com.cardstack.boxel.room.skills').enabledEventIds .length, - 2, + 1, + 'Only added skill is present', ); let skillEventId = getRoomState(roomId, 'com.cardstack.boxel.room.skills') - .enabledEventIds[1]; + .enabledEventIds[0]; let skillEventData = JSON.parse( ( events.find((e) => e.event_id === skillEventId) diff --git a/packages/host/tests/integration/realm-indexing-and-querying-test.gts b/packages/host/tests/integration/realm-indexing-and-querying-test.gts index b8e1f2f8ae..3039834d8c 100644 --- a/packages/host/tests/integration/realm-indexing-and-querying-test.gts +++ b/packages/host/tests/integration/realm-indexing-and-querying-test.gts @@ -3482,6 +3482,7 @@ module(`Integration | realm indexing and querying`, function (hooks) { 'https://cardstack.com/base/string', 'https://cardstack.com/base/text-input-validator', 'https://cardstack.com/base/watched-array', + 'https://packages/@cardstack/boxel-host/commands/create-ai-assistant-room', 'https://packages/@cardstack/boxel-host/commands/switch-submode', 'https://packages/@cardstack/boxel-ui/components', 'https://packages/@cardstack/boxel-ui/helpers', From deae052e049542faff2021f5b1795a80be965eb9 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 10 Dec 2024 14:22:44 -0500 Subject: [PATCH 4/9] Fix test with intentional error --- .../components/ai-assistant-panel-test.gts | 77 +++++++++++-------- 1 file changed, 44 insertions(+), 33 deletions(-) diff --git a/packages/host/tests/integration/components/ai-assistant-panel-test.gts b/packages/host/tests/integration/components/ai-assistant-panel-test.gts index 0473b0ea0e..a93fedbd92 100644 --- a/packages/host/tests/integration/components/ai-assistant-panel-test.gts +++ b/packages/host/tests/integration/components/ai-assistant-panel-test.gts @@ -970,41 +970,52 @@ module('Integration | ai-assistant-panel', function (hooks) { await percySnapshot(assert); }); - test('it can handle an error during room creation', async function (assert) { - await setCardInOperatorModeState(); - await renderComponent( - class TestDriver extends GlimmerComponent { - - }, - ); + module('suspending global error hook', (hooks) => { + let tmp: any; + hooks.before(() => { + tmp = QUnit.onUncaughtException; + QUnit.onUncaughtException = () => {}; + }); - await waitFor('[data-test-open-ai-assistant]'); - await click('[data-test-open-ai-assistant]'); - await waitFor('[data-test-new-session]'); - assert.dom('[data-test-room-error]').exists(); - assert.dom('[data-test-room]').doesNotExist(); - assert.dom('[data-test-past-sessions-button]').isDisabled(); - await percySnapshot( - 'Integration | ai-assistant-panel | it can handle an error during room creation | error state', - ); + hooks.after(() => { + QUnit.onUncaughtException = tmp; + }); + test('it can handle an error during room creation', async function (assert) { + await setCardInOperatorModeState(); + await renderComponent( + class TestDriver extends GlimmerComponent { + + }, + ); - document.querySelector('[data-test-throw-room-error]')?.remove(); - await click('[data-test-room-error] > button'); - await waitFor('[data-test-room]'); - assert.dom('[data-test-room-error]').doesNotExist(); - assert.dom('[data-test-past-sessions-button]').isEnabled(); - await percySnapshot( - 'Integration | ai-assistant-panel | it can handle an error during room creation | new room state', - ); + await waitFor('[data-test-open-ai-assistant]'); + await click('[data-test-open-ai-assistant]'); + await waitFor('[data-test-new-session]'); + assert.dom('[data-test-room-error]').exists(); + assert.dom('[data-test-room]').doesNotExist(); + assert.dom('[data-test-past-sessions-button]').isDisabled(); + await percySnapshot( + 'Integration | ai-assistant-panel | it can handle an error during room creation | error state', + ); + + document.querySelector('[data-test-throw-room-error]')?.remove(); + await click('[data-test-room-error] > button'); + await waitFor('[data-test-room]'); + assert.dom('[data-test-room-error]').doesNotExist(); + assert.dom('[data-test-past-sessions-button]').isEnabled(); + await percySnapshot( + 'Integration | ai-assistant-panel | it can handle an error during room creation | new room state', + ); + }); }); test('when opening ai panel it opens the most recent room', async function (assert) { From af93d1130b55022979fdd55f4f9a57cef39cfe8d Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 10 Dec 2024 14:26:45 -0500 Subject: [PATCH 5/9] Fix import paths --- .../AiAppGenerator/create-product-requirements-command.ts | 2 +- packages/catalog-realm/AiAppGenerator/generate-code-command.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/catalog-realm/AiAppGenerator/create-product-requirements-command.ts b/packages/catalog-realm/AiAppGenerator/create-product-requirements-command.ts index fa9c3cc9da..3b55592489 100644 --- a/packages/catalog-realm/AiAppGenerator/create-product-requirements-command.ts +++ b/packages/catalog-realm/AiAppGenerator/create-product-requirements-command.ts @@ -12,7 +12,7 @@ import SaveCardCommand from '@cardstack/boxel-host/commands/save-card'; import PatchCardCommand from '@cardstack/boxel-host/commands/patch-card'; import ReloadCardCommand from '@cardstack/boxel-host/commands/reload-card'; import CreateAIAssistantRoomCommand from '@cardstack/boxel-host/commands/create-ai-assistant-room'; -import AddSkillsToRoomCommand from '../../host/app/commands/add-skills-to-room'; +import AddSkillsToRoomCommand from '@cardstack/boxel-host/commands/add-skills-to-room'; export class CreateProductRequirementsInput extends CardDef { @field targetAudience = contains(StringField); diff --git a/packages/catalog-realm/AiAppGenerator/generate-code-command.ts b/packages/catalog-realm/AiAppGenerator/generate-code-command.ts index e692bcfbbc..a8dd37e3fa 100644 --- a/packages/catalog-realm/AiAppGenerator/generate-code-command.ts +++ b/packages/catalog-realm/AiAppGenerator/generate-code-command.ts @@ -91,7 +91,7 @@ import { and, bool, cn } from '@cardstack/boxel-ui/helpers'; import { baseRealm, getCard } from '@cardstack/runtime-common'; import { hash } from '@ember/helper'; import { on } from '@ember/modifier'; -import AddSkillsToRoomCommand from '../../host/app/commands/add-skills-to-room'; +import AddSkillsToRoomCommand from '@cardstack/boxel-host/commands/add-skills-to-room'; import { action } from '@ember/object'; import type Owner from '@ember/owner'; import GlimmerComponent from '@glimmer/component'; From 5c200d105bcfe06b93121f6158ad16f8edaa90c7 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 10 Dec 2024 17:03:58 -0500 Subject: [PATCH 6/9] Eliminate a dependency from matrix-service to ai-assistant-panel component, which was causing indexing problems - Also add forwarding methods to matrix-service to make client private --- .../app/components/ai-assistant/panel.gts | 13 ++-- .../ai-assistant/rename-session.gts | 2 +- .../app/components/ai-assistant/toast.gts | 4 +- .../app/components/matrix/forgot-password.gts | 4 +- .../app/components/matrix/register-user.gts | 7 +- packages/host/app/components/matrix/room.gts | 2 +- .../app/components/matrix/user-profile.gts | 4 +- .../components/operator-mode/code-submode.gts | 2 +- .../operator-mode/profile/profile-email.gts | 4 +- .../profile/profile-settings-modal.gts | 2 +- packages/host/app/resources/matrix-profile.ts | 4 +- packages/host/app/services/matrix-service.ts | 74 ++++++++++++++++++- packages/host/app/services/message-service.ts | 2 +- packages/host/app/services/realm.ts | 3 +- packages/host/app/utils/local-storage-keys.ts | 4 + .../operator-mode-acceptance-test.gts | 7 +- .../components/ai-assistant-panel-test.gts | 3 +- 17 files changed, 110 insertions(+), 31 deletions(-) create mode 100644 packages/host/app/utils/local-storage-keys.ts diff --git a/packages/host/app/components/ai-assistant/panel.gts b/packages/host/app/components/ai-assistant/panel.gts index 38e41e275f..54733293ff 100644 --- a/packages/host/app/components/ai-assistant/panel.gts +++ b/packages/host/app/components/ai-assistant/panel.gts @@ -41,6 +41,11 @@ import type MatrixService from '@cardstack/host/services/matrix-service'; import type MonacoService from '@cardstack/host/services/monaco-service'; import { type MonacoSDK } from '@cardstack/host/services/monaco-service'; +import { + currentRoomIdPersistenceKey, + newSessionIdPersistenceKey, +} from '@cardstack/host/utils/local-storage-keys'; + import assistantIcon from './ai-assist-icon.webp'; const { matrixServerName } = ENV; @@ -62,10 +67,6 @@ export interface SessionRoomData { lastActiveTimestamp: number; } -// Local storage keys -export const currentRoomIdPersistenceKey = 'aiPanelCurrentRoomId'; -let newSessionIdPersistenceKey = 'aiPanelNewSessionId'; - export default class AiAssistantPanel extends Component { get hasOtherActiveSessions() { let oneMinuteAgo = new Date(Date.now() - 60 * 1000).getTime(); @@ -557,8 +558,8 @@ export default class AiAssistantPanel extends Component { private doLeaveRoom = restartableTask(async (roomId: string) => { try { - await this.matrixService.client.leave(roomId); - await this.matrixService.client.forget(roomId); + await this.matrixService.leave(roomId); + await this.matrixService.forget(roomId); await timeout(eventDebounceMs); // this makes it feel a bit more responsive this.matrixService.roomResourcesCache.delete(roomId); diff --git a/packages/host/app/components/ai-assistant/rename-session.gts b/packages/host/app/components/ai-assistant/rename-session.gts index 05c779148b..70182e3ae6 100644 --- a/packages/host/app/components/ai-assistant/rename-session.gts +++ b/packages/host/app/components/ai-assistant/rename-session.gts @@ -122,7 +122,7 @@ export default class RenameSession extends Component { throw new Error(`bug: should never get here`); } try { - await this.matrixService.client.setRoomName( + await this.matrixService.setRoomName( this.args.room.roomId, this.newRoomName, ); diff --git a/packages/host/app/components/ai-assistant/toast.gts b/packages/host/app/components/ai-assistant/toast.gts index 84eea06127..7bd36b741f 100644 --- a/packages/host/app/components/ai-assistant/toast.gts +++ b/packages/host/app/components/ai-assistant/toast.gts @@ -20,9 +20,9 @@ import { markdownToHtml } from '@cardstack/runtime-common'; import { Message } from '@cardstack/host/lib/matrix-classes/message'; import MatrixService from '@cardstack/host/services/matrix-service'; -import assistantIcon from './ai-assist-icon.webp'; +import { currentRoomIdPersistenceKey } from '@cardstack/host/utils/local-storage-keys'; -import { currentRoomIdPersistenceKey } from './panel'; +import assistantIcon from './ai-assist-icon.webp'; interface Signature { Element: HTMLDivElement; diff --git a/packages/host/app/components/matrix/forgot-password.gts b/packages/host/app/components/matrix/forgot-password.gts index a909cd0b0d..b0fad64cbd 100644 --- a/packages/host/app/components/matrix/forgot-password.gts +++ b/packages/host/app/components/matrix/forgot-password.gts @@ -391,7 +391,7 @@ export default class ForgotPassword extends Component { try { let clientSecret = uuidv4(); - let { sid } = await this.matrixService.client.requestPasswordEmailToken( + let { sid } = await this.matrixService.requestPasswordEmailToken( this.state.email, clientSecret, this.state.sendAttempt, @@ -433,7 +433,7 @@ export default class ForgotPassword extends Component { } try { - await this.matrixService.client.setPassword( + await this.matrixService.setPassword( { threepid_creds: { sid: this.args.resetPasswordParams.sid, diff --git a/packages/host/app/components/matrix/register-user.gts b/packages/host/app/components/matrix/register-user.gts index 9aeb459073..55d82d7b11 100644 --- a/packages/host/app/components/matrix/register-user.gts +++ b/packages/host/app/components/matrix/register-user.gts @@ -513,8 +513,9 @@ export default class RegisterUser extends Component { return; } - this.isUsernameAvailable = - await this.matrixService.client.isUsernameAvailable(this.username); + this.isUsernameAvailable = await this.matrixService.isUsernameAvailable( + this.username, + ); if (!this.isUsernameAvailable) { this.usernameError = 'Username is already taken'; } @@ -675,7 +676,7 @@ export default class RegisterUser extends Component { let auth: RegisterResponse; try { - auth = await this.matrixService.client.registerRequest({ + auth = await this.matrixService.registerRequest({ username: this.state.username, password: this.state.password, auth: { diff --git a/packages/host/app/components/matrix/room.gts b/packages/host/app/components/matrix/room.gts index c993b32c72..ad9dca79ba 100644 --- a/packages/host/app/components/matrix/room.gts +++ b/packages/host/app/components/matrix/room.gts @@ -400,7 +400,7 @@ export default class Room extends Component { // update value, but it had already been used previously in the same // computation" error schedule('afterRender', () => { - this.matrixService.client.sendReadReceipt(matrixEvent as MatrixEvent); + this.matrixService.sendReadReceipt(matrixEvent as MatrixEvent); }); } diff --git a/packages/host/app/components/matrix/user-profile.gts b/packages/host/app/components/matrix/user-profile.gts index d6ca9ce370..e534c15639 100644 --- a/packages/host/app/components/matrix/user-profile.gts +++ b/packages/host/app/components/matrix/user-profile.gts @@ -88,8 +88,8 @@ export default class UserProfile extends Component { private loadProfile = restartableTask(async () => { let [profile, threePid] = await all([ - this.matrixService.client.getProfileInfo(this.userId), - this.matrixService.client.getThreePids(), + this.matrixService.getProfileInfo(this.userId), + this.matrixService.getThreePids(), ]); let { displayname: displayName } = profile; let { threepids } = threePid; diff --git a/packages/host/app/components/operator-mode/code-submode.gts b/packages/host/app/components/operator-mode/code-submode.gts index 18c98a5b6d..2f969c5268 100644 --- a/packages/host/app/components/operator-mode/code-submode.gts +++ b/packages/host/app/components/operator-mode/code-submode.gts @@ -58,6 +58,7 @@ import type RecentFilesService from '@cardstack/host/services/recent-files-servi import type { CardDef, Format } from 'https://cardstack.com/base/card-api'; import { htmlComponent } from '../../lib/html-component'; +import { CodeModePanelWidths } from '../../utils/local-storage-keys'; import FileTree from '../editor/file-tree'; import CardError from './card-error'; @@ -95,7 +96,6 @@ type PanelHeights = { type SelectedAccordionItem = 'schema-editor' | null; -const CodeModePanelWidths = 'code-mode-panel-widths'; const defaultLeftPanelWidth = (14.0 * parseFloat(getComputedStyle(document.documentElement).fontSize)) / (document.documentElement.clientWidth - 40 - 36); diff --git a/packages/host/app/components/operator-mode/profile/profile-email.gts b/packages/host/app/components/operator-mode/profile/profile-email.gts index 12a40abdd7..2b1c4b0692 100644 --- a/packages/host/app/components/operator-mode/profile/profile-email.gts +++ b/packages/host/app/components/operator-mode/profile/profile-email.gts @@ -632,7 +632,7 @@ export default class ProfileEmail extends Component { let emailAdded = false; try { - await this.matrixService.client.addThreePidOnly({ + await this.matrixService.addThreePidOnly({ auth, client_secret: this.emailState.clientSecret, sid: this.emailState.sid, @@ -671,7 +671,7 @@ export default class ProfileEmail extends Component { let oldEmails = this.matrixService.profile.threePids; await Promise.all( oldEmails.map((email) => - this.matrixService.client.deleteThreePid('email', email), + this.matrixService.deleteThreePid('email', email), ), ); this.emailState = { type: 'initial' }; diff --git a/packages/host/app/components/operator-mode/profile/profile-settings-modal.gts b/packages/host/app/components/operator-mode/profile/profile-settings-modal.gts index 56c07a37a4..c27e07f8d8 100644 --- a/packages/host/app/components/operator-mode/profile/profile-settings-modal.gts +++ b/packages/host/app/components/operator-mode/profile/profile-settings-modal.gts @@ -420,7 +420,7 @@ export default class ProfileSettingsModal extends Component { user: this.matrixService.userId, }, } as IAuthData & { type: string }; - await this.matrixService.client.setPassword(auth, this.newPassword); + await this.matrixService.setPassword(auth, this.newPassword); this.resetPasswordFields(); this.submode = undefined; } catch (e: any) { diff --git a/packages/host/app/resources/matrix-profile.ts b/packages/host/app/resources/matrix-profile.ts index 0dc7e906dc..4a6e4557ed 100644 --- a/packages/host/app/resources/matrix-profile.ts +++ b/packages/host/app/resources/matrix-profile.ts @@ -32,8 +32,8 @@ export class MatrixProfileResource extends Resource { load = restartableTask(async () => { if (this.userId) { let [rawProfile, threePid] = await all([ - this.matrixService.client.getProfileInfo(this.userId), - this.matrixService.client.getThreePids(), + this.matrixService.getProfileInfo(this.userId), + this.matrixService.getThreePids(), ]); if (rawProfile) { diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index 8775b998a0..f5e33e8a88 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -37,7 +37,6 @@ import { import { getPatchTool } from '@cardstack/runtime-common/helpers/ai'; import { getMatrixUsername } from '@cardstack/runtime-common/matrix-client'; -import { currentRoomIdPersistenceKey } from '@cardstack/host/components/ai-assistant/panel'; import { type Submode, Submodes, @@ -69,6 +68,8 @@ import { importResource } from '../resources/import'; import { RoomResource, getRoom } from '../resources/room'; +import { currentRoomIdPersistenceKey } from '../utils/local-storage-keys'; + import { type SerializedState as OperatorModeSerializedState } from './operator-mode-state-service'; import type CardService from './card-service'; @@ -890,6 +891,22 @@ export default class MatrixService extends Service { return card; } + async getProfileInfo(userId: string) { + return await this.client.getProfileInfo(userId); + } + + async getThreePids() { + return await this.client.getThreePids(); + } + + async addThreePidOnly(data: MatrixSDK.IAddThreePidOnlyBody) { + return await this.client.addThreePidOnly(data); + } + + async deleteThreePid(medium: string, address: string) { + return await this.client.deleteThreePid(medium, address); + } + async setPowerLevel(roomId: string, userId: string, powerLevel: number) { let roomData = this.ensureRoomData(roomId); await roomData.mutex.dispatch(async () => { @@ -917,6 +934,61 @@ export default class MatrixService extends Service { }); } + async leave(roomId: string) { + let roomData = this.ensureRoomData(roomId); + await roomData.mutex.dispatch(async () => { + return this.client.leave(roomId); + }); + } + + async forget(roomId: string) { + let roomData = this.ensureRoomData(roomId); + await roomData.mutex.dispatch(async () => { + return this.client.forget(roomId); + }); + } + + async setRoomName(roomId: string, name: string) { + let roomData = this.ensureRoomData(roomId); + await roomData.mutex.dispatch(async () => { + return this.client.setRoomName(roomId, name); + }); + } + + async requestPasswordEmailToken( + email: string, + clientSecret: string, + sendAttempt: number, + nextLink?: string, + ) { + return await this.client.requestPasswordEmailToken( + email, + clientSecret, + sendAttempt, + nextLink, + ); + } + + async setPassword( + authDict: MatrixSDK.AuthDict, + newPassword: string, + logoutDevices?: boolean, + ) { + return await this.client.setPassword(authDict, newPassword, logoutDevices); + } + + async registerRequest(data: MatrixSDK.RegisterRequest, kind?: string) { + return await this.client.registerRequest(data, kind); + } + + async sendReadReceipt(matrixEvent: MatrixEvent) { + return await this.client.sendReadReceipt(matrixEvent); + } + + async isUsernameAvailable(username: string) { + return await this.client.isUsernameAvailable(username); + } + private addRoomEvent(event: TempEvent, oldEventId?: string) { let { room_id: roomId } = event; diff --git a/packages/host/app/services/message-service.ts b/packages/host/app/services/message-service.ts index 6bd34cb856..156a8eb700 100644 --- a/packages/host/app/services/message-service.ts +++ b/packages/host/app/services/message-service.ts @@ -8,7 +8,7 @@ import window from 'ember-window-mock'; import qs from 'qs'; -import { sessionLocalStorageKey } from './realm'; +import { sessionLocalStorageKey } from '../utils/local-storage-keys'; import type NetworkService from './network'; diff --git a/packages/host/app/services/realm.ts b/packages/host/app/services/realm.ts index 4dd57e8014..e259e4ef45 100644 --- a/packages/host/app/services/realm.ts +++ b/packages/host/app/services/realm.ts @@ -34,6 +34,8 @@ import ENV from '@cardstack/host/config/environment'; import { assertNever } from '@cardstack/host/utils/assert-never'; +import { sessionLocalStorageKey } from '../utils/local-storage-keys'; + import type MatrixService from './matrix-service'; import type MessageService from './message-service'; import type NetworkService from './network'; @@ -649,7 +651,6 @@ export default class RealmService extends Service { } export const tokenRefreshPeriodSec = 5 * 60; // 5 minutes -export const sessionLocalStorageKey = 'boxel-session'; export function claimsFromRawToken(rawToken: string): JWTPayload { let [_header, payload] = rawToken.split('.'); diff --git a/packages/host/app/utils/local-storage-keys.ts b/packages/host/app/utils/local-storage-keys.ts new file mode 100644 index 0000000000..86eee8784b --- /dev/null +++ b/packages/host/app/utils/local-storage-keys.ts @@ -0,0 +1,4 @@ +export const currentRoomIdPersistenceKey = 'aiPanelCurrentRoomId'; +export const newSessionIdPersistenceKey = 'aiPanelNewSessionId'; +export const CodeModePanelWidths = 'code-mode-panel-widths'; +export const sessionLocalStorageKey = 'boxel-session'; diff --git a/packages/host/tests/acceptance/operator-mode-acceptance-test.gts b/packages/host/tests/acceptance/operator-mode-acceptance-test.gts index e01cae71c6..0773dcf808 100644 --- a/packages/host/tests/acceptance/operator-mode-acceptance-test.gts +++ b/packages/host/tests/acceptance/operator-mode-acceptance-test.gts @@ -22,10 +22,9 @@ import { } from '@cardstack/runtime-common'; import { Submodes } from '@cardstack/host/components/submode-switcher'; -import { - tokenRefreshPeriodSec, - sessionLocalStorageKey, -} from '@cardstack/host/services/realm'; +import { tokenRefreshPeriodSec } from '@cardstack/host/services/realm'; + +import { sessionLocalStorageKey } from '@cardstack/host/utils/local-storage-keys'; import { percySnapshot, diff --git a/packages/host/tests/integration/components/ai-assistant-panel-test.gts b/packages/host/tests/integration/components/ai-assistant-panel-test.gts index a93fedbd92..57fc329803 100644 --- a/packages/host/tests/integration/components/ai-assistant-panel-test.gts +++ b/packages/host/tests/integration/components/ai-assistant-panel-test.gts @@ -16,13 +16,14 @@ import { module, test } from 'qunit'; import { baseRealm } from '@cardstack/runtime-common'; import { Loader } from '@cardstack/runtime-common/loader'; -import { currentRoomIdPersistenceKey } from '@cardstack/host/components/ai-assistant/panel'; import CardPrerender from '@cardstack/host/components/card-prerender'; import OperatorMode from '@cardstack/host/components/operator-mode/container'; import MatrixService from '@cardstack/host/services/matrix-service'; import OperatorModeStateService from '@cardstack/host/services/operator-mode-state-service'; +import { currentRoomIdPersistenceKey } from '@cardstack/host/utils/local-storage-keys'; + import type { CommandResultEvent } from 'https://cardstack.com/base/matrix-event'; import { From 65fdf05bfa12d329b65c583234e9376cfcaebb86 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Tue, 10 Dec 2024 18:20:41 -0500 Subject: [PATCH 7/9] Add default skills to room when creating new room via panel --- .../host/app/components/ai-assistant/panel.gts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/packages/host/app/components/ai-assistant/panel.gts b/packages/host/app/components/ai-assistant/panel.gts index 54733293ff..4e89ed41cd 100644 --- a/packages/host/app/components/ai-assistant/panel.gts +++ b/packages/host/app/components/ai-assistant/panel.gts @@ -22,6 +22,7 @@ import { DropdownArrowFilled, IconX } from '@cardstack/boxel-ui/icons'; import { aiBotUsername } from '@cardstack/runtime-common'; +import AddSkillsToRoomCommand from '@cardstack/host/commands/add-skills-to-room'; import CreateAIAssistantRoomCommand from '@cardstack/host/commands/create-ai-assistant-room'; import NewSession from '@cardstack/host/components/ai-assistant/new-session'; import AiAssistantPastSessionsList from '@cardstack/host/components/ai-assistant/past-sessions'; @@ -430,11 +431,21 @@ export default class AiAssistantPanel extends Component { private doCreateRoom = restartableTask(async (name: string) => { try { - let command = new CreateAIAssistantRoomCommand( + let createRoomCommand = new CreateAIAssistantRoomCommand( this.commandService.commandContext, ); - let InputType = await command.getInputType(); - let { roomId } = await command.execute(new InputType({ name })); + let InputType = await createRoomCommand.getInputType(); + let { roomId } = await createRoomCommand.execute(new InputType({ name })); + + let addSkillsToRoomCommand = new AddSkillsToRoomCommand( + this.commandService.commandContext, + ); + await addSkillsToRoomCommand.execute( + new (await addSkillsToRoomCommand.getInputType())({ + roomId, + skills: await this.matrixService.loadDefaultSkills(), + }), + ); window.localStorage.setItem(newSessionIdPersistenceKey, roomId); this.enterRoom(roomId); } catch (e) { From 78fd931ddf16d4a2d42a615e361e60aad1b78019 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Wed, 11 Dec 2024 14:41:50 -0500 Subject: [PATCH 8/9] Apply code review feedback --- .../host/app/commands/add-skills-to-room.ts | 39 +++++------ .../app/commands/create-ai-assistant-room.ts | 8 +-- .../app/components/ai-assistant/panel.gts | 20 +++--- .../app/components/ai-assistant/toast.gts | 4 +- packages/host/app/components/matrix/room.gts | 35 ++++++---- packages/host/app/services/matrix-service.ts | 70 +++++++++++++++++-- packages/host/app/services/message-service.ts | 4 +- packages/host/app/services/realm.ts | 10 +-- packages/host/app/utils/local-storage-keys.ts | 6 +- .../operator-mode-acceptance-test.gts | 8 +-- .../components/ai-assistant-panel-test.gts | 12 ++-- 11 files changed, 137 insertions(+), 79 deletions(-) diff --git a/packages/host/app/commands/add-skills-to-room.ts b/packages/host/app/commands/add-skills-to-room.ts index b121968b5f..7a48726cf7 100644 --- a/packages/host/app/commands/add-skills-to-room.ts +++ b/packages/host/app/commands/add-skills-to-room.ts @@ -30,28 +30,21 @@ export default class AddSkillsToRoomCommand extends HostBaseCommand< roomId, { includeComputeds: true, maybeRelativeURL: null }, ); - let skillEventIdsStateEvent: Record = {}; - try { - skillEventIdsStateEvent = await matrixService.getStateEvent( - roomId, - SKILLS_STATE_EVENT_TYPE, - '', - ); - } catch (e: unknown) { - if (e instanceof Error && 'errcode' in e && e.errcode === 'M_NOT_FOUND') { - // this is fine, it just means the state event doesn't exist yet - } else { - throw e; - } - } - await matrixService.sendStateEvent(roomId, SKILLS_STATE_EVENT_TYPE, { - enabledEventIds: [ - ...new Set([ - ...(skillEventIdsStateEvent?.enabledEventIds || []), - ...roomSkillEventIds, - ]), - ], - disabledEventIds: [...(skillEventIdsStateEvent?.disabledEventIds || [])], - }); + await matrixService.updateStateEvent( + roomId, + SKILLS_STATE_EVENT_TYPE, + '', + async (oldContent: Record) => { + return { + enabledEventIds: [ + ...new Set([ + ...(oldContent.enabledEventIds || []), + ...roomSkillEventIds, + ]), + ], + disabledEventIds: [...(oldContent.disabledEventIds || [])], + }; + }, + ); } } diff --git a/packages/host/app/commands/create-ai-assistant-room.ts b/packages/host/app/commands/create-ai-assistant-room.ts index 980f7256eb..e97fea321d 100644 --- a/packages/host/app/commands/create-ai-assistant-room.ts +++ b/packages/host/app/commands/create-ai-assistant-room.ts @@ -8,8 +8,6 @@ import type * as BaseCommandModule from 'https://cardstack.com/base/command'; import HostBaseCommand from '../lib/host-base-command'; -import { AI_BOT_POWER_LEVEL } from '../services/matrix-service'; - import type MatrixService from '../services/matrix-service'; export default class CreateAIAssistantRoomCommand extends HostBaseCommand< @@ -28,7 +26,7 @@ export default class CreateAIAssistantRoomCommand extends HostBaseCommand< input: BaseCommandModule.CreateAIAssistantRoomInput, ): Promise { let { matrixService } = this; - let { matrixSDK, userId } = matrixService; + let { userId } = matrixService; if (!userId) { throw new Error( `bug: there is no userId associated with the matrix client`, @@ -37,7 +35,7 @@ export default class CreateAIAssistantRoomCommand extends HostBaseCommand< let server = userId!.split(':')[1]; let aiBotFullId = `@${aiBotUsername}:${server}`; let { room_id: roomId } = await matrixService.createRoom({ - preset: matrixSDK.Preset.PrivateChat, + preset: matrixService.privateChatPreset, invite: [aiBotFullId], name: input.name, topic: undefined, @@ -51,7 +49,7 @@ export default class CreateAIAssistantRoomCommand extends HostBaseCommand< await this.matrixService.setPowerLevel( roomId, aiBotFullId, - AI_BOT_POWER_LEVEL, + matrixService.aiBotPowerLevel, ); let commandModule = await this.loadCommandModule(); const { CreateAIAssistantRoomResult } = commandModule; diff --git a/packages/host/app/components/ai-assistant/panel.gts b/packages/host/app/components/ai-assistant/panel.gts index 4e89ed41cd..b6c6c83205 100644 --- a/packages/host/app/components/ai-assistant/panel.gts +++ b/packages/host/app/components/ai-assistant/panel.gts @@ -43,8 +43,8 @@ import type MonacoService from '@cardstack/host/services/monaco-service'; import { type MonacoSDK } from '@cardstack/host/services/monaco-service'; import { - currentRoomIdPersistenceKey, - newSessionIdPersistenceKey, + CurrentRoomIdPersistenceKey, + NewSessionIdPersistenceKey, } from '@cardstack/host/utils/local-storage-keys'; import assistantIcon from './ai-assist-icon.webp'; @@ -382,7 +382,7 @@ export default class AiAssistantPanel extends Component { private enterRoomInitially() { let persistedRoomId = window.localStorage.getItem( - currentRoomIdPersistenceKey, + CurrentRoomIdPersistenceKey, ); if ( persistedRoomId && @@ -411,9 +411,7 @@ export default class AiAssistantPanel extends Component { } private loadRoomsTask = restartableTask(async () => { - await this.matrixService.flushMembership; - await this.matrixService.flushTimeline; - await this.matrixService.flushRoomState; + await this.matrixService.flushAll; await Promise.all([...this.roomResources.values()].map((r) => r.loading)); this.enterRoomInitially(); }); @@ -446,7 +444,7 @@ export default class AiAssistantPanel extends Component { skills: await this.matrixService.loadDefaultSkills(), }), ); - window.localStorage.setItem(newSessionIdPersistenceKey, roomId); + window.localStorage.setItem(NewSessionIdPersistenceKey, roomId); this.enterRoom(roomId); } catch (e) { console.log(e); @@ -456,7 +454,7 @@ export default class AiAssistantPanel extends Component { }); private get newSessionId() { - let id = window.localStorage.getItem(newSessionIdPersistenceKey); + let id = window.localStorage.getItem(NewSessionIdPersistenceKey); if ( id && this.roomResources.has(id) && @@ -529,7 +527,7 @@ export default class AiAssistantPanel extends Component { if (hidePastSessionsList) { this.hidePastSessions(); } - window.localStorage.setItem(currentRoomIdPersistenceKey, roomId); + window.localStorage.setItem(CurrentRoomIdPersistenceKey, roomId); } @action private setRoomToRename(room: SessionRoomData) { @@ -575,11 +573,11 @@ export default class AiAssistantPanel extends Component { this.matrixService.roomResourcesCache.delete(roomId); if (this.newSessionId === roomId) { - window.localStorage.removeItem(newSessionIdPersistenceKey); + window.localStorage.removeItem(NewSessionIdPersistenceKey); } if (this.currentRoomId === roomId) { - window.localStorage.removeItem(currentRoomIdPersistenceKey); + window.localStorage.removeItem(CurrentRoomIdPersistenceKey); if (this.latestRoom) { this.enterRoom(this.latestRoom.roomId, false); } else { diff --git a/packages/host/app/components/ai-assistant/toast.gts b/packages/host/app/components/ai-assistant/toast.gts index 7bd36b741f..360dd7f008 100644 --- a/packages/host/app/components/ai-assistant/toast.gts +++ b/packages/host/app/components/ai-assistant/toast.gts @@ -20,7 +20,7 @@ import { markdownToHtml } from '@cardstack/runtime-common'; import { Message } from '@cardstack/host/lib/matrix-classes/message'; import MatrixService from '@cardstack/host/services/matrix-service'; -import { currentRoomIdPersistenceKey } from '@cardstack/host/utils/local-storage-keys'; +import { CurrentRoomIdPersistenceKey } from '@cardstack/host/utils/local-storage-keys'; import assistantIcon from './ai-assist-icon.webp'; @@ -222,7 +222,7 @@ export default class AiAssistantToast extends Component { @action private viewInChat() { - window.localStorage.setItem(currentRoomIdPersistenceKey, this.roomId); + window.localStorage.setItem(CurrentRoomIdPersistenceKey, this.roomId); this.args.onViewInChatClick(); } } diff --git a/packages/host/app/components/matrix/room.gts b/packages/host/app/components/matrix/room.gts index ad9dca79ba..f304202fa0 100644 --- a/packages/host/app/components/matrix/room.gts +++ b/packages/host/app/components/matrix/room.gts @@ -6,8 +6,15 @@ import { service } from '@ember/service'; import Component from '@glimmer/component'; import { tracked, cached } from '@glimmer/tracking'; -import { enqueueTask, restartableTask, timeout, all } from 'ember-concurrency'; - +import { + enqueueTask, + restartableTask, + timeout, + all, + task, +} from 'ember-concurrency'; + +import perform from 'ember-concurrency/helpers/perform'; import max from 'lodash/max'; import { MatrixEvent } from 'matrix-js-sdk'; @@ -102,8 +109,8 @@ export default class Room extends Component { {{/if}} @@ -588,13 +595,15 @@ export default class Room extends Component { return this.autoAttachmentResource.cards; } - updateSkillIsActiveTask = async (skillEventId: string, isActive: boolean) => { - await this.matrixService.updateSkillIsActive( - this.args.roomId, - skillEventId, - isActive, - ); - }; + updateSkillIsActiveTask = task( + async (skillEventId: string, isActive: boolean) => { + await this.matrixService.updateSkillIsActive( + this.args.roomId, + skillEventId, + isActive, + ); + }, + ); private get canSend() { return ( @@ -622,7 +631,7 @@ export default class Room extends Component { return message.status === 'sending' || message.status === 'queued'; } - private attachSkill = async (card: SkillCard) => { + private attachSkillTask = task(async (card: SkillCard) => { let addSkillsToRoomCommand = new AddSkillsToRoomCommand( this.commandService.commandContext, ); @@ -632,7 +641,7 @@ export default class Room extends Component { skills: [card], }), ); - }; + }); } declare module '@glint/environment-ember-loose/registry' { diff --git a/packages/host/app/services/matrix-service.ts b/packages/host/app/services/matrix-service.ts index f5e33e8a88..dc67a4537b 100644 --- a/packages/host/app/services/matrix-service.ts +++ b/packages/host/app/services/matrix-service.ts @@ -68,7 +68,7 @@ import { importResource } from '../resources/import'; import { RoomResource, getRoom } from '../resources/room'; -import { currentRoomIdPersistenceKey } from '../utils/local-storage-keys'; +import { CurrentRoomIdPersistenceKey } from '../utils/local-storage-keys'; import { type SerializedState as OperatorModeSerializedState } from './operator-mode-state-service'; @@ -84,7 +84,6 @@ import type ResetService from './reset'; import type * as MatrixSDK from 'matrix-js-sdk'; const { matrixURL } = ENV; -export const AI_BOT_POWER_LEVEL = 50; // this is required to set the room name const MAX_CARD_SIZE_KB = 60; const STATE_EVENTS_OF_INTEREST = ['m.room.create', 'm.room.name']; const DefaultSkillCards = [`${baseRealm.url}SkillCard/card-editing`]; @@ -220,17 +219,32 @@ export default class MatrixService extends Service { return this.cardAPIModule.module as typeof CardAPI; } - get matrixSDK() { + private get matrixSDK() { if (!this.#matrixSDK) { throw new Error(`cannot use matrix SDK before it has loaded`); } return this.#matrixSDK; } + get privateChatPreset() { + return this.matrixSDK.Preset.PrivateChat; + } + + get aiBotPowerLevel() { + return 50; // this is required to set the room name + } + + get flushAll() { + return Promise.all([ + this.flushMembership ?? Promise.resolve(), + this.flushTimeline ?? Promise.resolve(), + this.flushRoomState ?? Promise.resolve(), + ]); + } + async logout() { try { - await this.flushMembership; - await this.flushTimeline; + await this.flushAll; clearAuth(); this.postLoginCompleted = false; this.reset.resetAll(); @@ -616,7 +630,7 @@ export default class MatrixService extends Service { ) => { let roomData = this.ensureRoomData(roomId); await roomData.mutex.dispatch(async () => { - let currentSkillsConfig = await this.client.getStateEvent( + let currentSkillsConfig = await this.getStateEvent( roomId, SKILLS_STATE_EVENT_TYPE, '', @@ -922,6 +936,23 @@ export default class MatrixService extends Service { return this.client.getStateEvent(roomId, eventType, stateKey); } + async getStateEventSafe( + roomId: string, + eventType: string, + stateKey: string = '', + ) { + try { + return await this.client.getStateEvent(roomId, eventType, stateKey); + } catch (e: unknown) { + if (e instanceof Error && 'errcode' in e && e.errcode === 'M_NOT_FOUND') { + // this is fine, it just means the state event doesn't exist yet + return undefined; + } else { + throw e; + } + } + } + async sendStateEvent( roomId: string, eventType: string, @@ -934,6 +965,31 @@ export default class MatrixService extends Service { }); } + async updateStateEvent( + roomId: string, + eventType: string, + stateKey: string = '', + transformContent: ( + content: Record, + ) => Promise>, + ) { + let roomData = this.ensureRoomData(roomId); + await roomData.mutex.dispatch(async () => { + let currentContent = await this.getStateEventSafe( + roomId, + eventType, + stateKey, + ); + let newContent = await transformContent(currentContent ?? {}); + return this.client.sendStateEvent( + roomId, + eventType, + newContent, + stateKey, + ); + }); + } + async leave(roomId: string) { let roomData = this.ensureRoomData(roomId); await roomData.mutex.dispatch(async () => { @@ -1294,7 +1350,7 @@ function saveAuth(auth: LoginResponse) { function clearAuth() { window.localStorage.removeItem('auth'); - window.localStorage.removeItem(currentRoomIdPersistenceKey); + window.localStorage.removeItem(CurrentRoomIdPersistenceKey); } function getAuth(): LoginResponse | undefined { diff --git a/packages/host/app/services/message-service.ts b/packages/host/app/services/message-service.ts index 156a8eb700..46ea4b40dd 100644 --- a/packages/host/app/services/message-service.ts +++ b/packages/host/app/services/message-service.ts @@ -8,7 +8,7 @@ import window from 'ember-window-mock'; import qs from 'qs'; -import { sessionLocalStorageKey } from '../utils/local-storage-keys'; +import { SessionLocalStorageKey } from '../utils/local-storage-keys'; import type NetworkService from './network'; @@ -62,7 +62,7 @@ function getPersistedTokenForRealm(realmURL: string) { return 'TEST_TOKEN'; } - let sessionStr = window.localStorage.getItem(sessionLocalStorageKey) ?? '{}'; + let sessionStr = window.localStorage.getItem(SessionLocalStorageKey) ?? '{}'; let session = JSON.parse(sessionStr); return session[realmURL] as string | undefined; } diff --git a/packages/host/app/services/realm.ts b/packages/host/app/services/realm.ts index e259e4ef45..0bc049a637 100644 --- a/packages/host/app/services/realm.ts +++ b/packages/host/app/services/realm.ts @@ -34,7 +34,7 @@ import ENV from '@cardstack/host/config/environment'; import { assertNever } from '@cardstack/host/utils/assert-never'; -import { sessionLocalStorageKey } from '../utils/local-storage-keys'; +import { SessionLocalStorageKey } from '../utils/local-storage-keys'; import type MatrixService from './matrix-service'; import type MessageService from './message-service'; @@ -202,7 +202,7 @@ class RealmResource { this.fetchInfoTask.cancelAll(); this.fetchingInfo = undefined; this.fetchRealmPermissionsTask.cancelAll(); - window.localStorage.removeItem(sessionLocalStorageKey); + window.localStorage.removeItem(SessionLocalStorageKey); } private fetchingInfo: Promise | undefined; @@ -659,7 +659,7 @@ export function claimsFromRawToken(rawToken: string): JWTPayload { let SessionStorage = { getAll(): Record | undefined { - let sessionsString = window.localStorage.getItem(sessionLocalStorageKey); + let sessionsString = window.localStorage.getItem(SessionLocalStorageKey); if (sessionsString) { return JSON.parse(sessionsString); } @@ -667,12 +667,12 @@ let SessionStorage = { }, persist(realmURL: string, token: string | undefined) { let sessionStr = - window.localStorage.getItem(sessionLocalStorageKey) ?? '{}'; + window.localStorage.getItem(SessionLocalStorageKey) ?? '{}'; let session = JSON.parse(sessionStr); if (session[realmURL] !== token) { session[realmURL] = token; window.localStorage.setItem( - sessionLocalStorageKey, + SessionLocalStorageKey, JSON.stringify(session), ); } diff --git a/packages/host/app/utils/local-storage-keys.ts b/packages/host/app/utils/local-storage-keys.ts index 86eee8784b..da1969e8ab 100644 --- a/packages/host/app/utils/local-storage-keys.ts +++ b/packages/host/app/utils/local-storage-keys.ts @@ -1,4 +1,4 @@ -export const currentRoomIdPersistenceKey = 'aiPanelCurrentRoomId'; -export const newSessionIdPersistenceKey = 'aiPanelNewSessionId'; +export const CurrentRoomIdPersistenceKey = 'aiPanelCurrentRoomId'; +export const NewSessionIdPersistenceKey = 'aiPanelNewSessionId'; export const CodeModePanelWidths = 'code-mode-panel-widths'; -export const sessionLocalStorageKey = 'boxel-session'; +export const SessionLocalStorageKey = 'boxel-session'; diff --git a/packages/host/tests/acceptance/operator-mode-acceptance-test.gts b/packages/host/tests/acceptance/operator-mode-acceptance-test.gts index 0773dcf808..77632caf32 100644 --- a/packages/host/tests/acceptance/operator-mode-acceptance-test.gts +++ b/packages/host/tests/acceptance/operator-mode-acceptance-test.gts @@ -24,7 +24,7 @@ import { import { Submodes } from '@cardstack/host/components/submode-switcher'; import { tokenRefreshPeriodSec } from '@cardstack/host/services/realm'; -import { sessionLocalStorageKey } from '@cardstack/host/utils/local-storage-keys'; +import { SessionLocalStorageKey } from '@cardstack/host/utils/local-storage-keys'; import { percySnapshot, @@ -807,14 +807,14 @@ module('Acceptance | operator mode tests', function (hooks) { test('realm session refreshes within 5 minute window of expiration', async function (assert) { await visit('/'); - let originalToken = window.localStorage.getItem(sessionLocalStorageKey); + let originalToken = window.localStorage.getItem(SessionLocalStorageKey); await waitUntil( () => - window.localStorage.getItem(sessionLocalStorageKey) !== originalToken, + window.localStorage.getItem(SessionLocalStorageKey) !== originalToken, { timeout: refreshInSec * 3 * 1000 }, ); - let newToken = window.localStorage.getItem(sessionLocalStorageKey); + let newToken = window.localStorage.getItem(SessionLocalStorageKey); assert.ok(newToken, 'new session token obtained'); assert.notEqual( originalToken, diff --git a/packages/host/tests/integration/components/ai-assistant-panel-test.gts b/packages/host/tests/integration/components/ai-assistant-panel-test.gts index 57fc329803..820ae97187 100644 --- a/packages/host/tests/integration/components/ai-assistant-panel-test.gts +++ b/packages/host/tests/integration/components/ai-assistant-panel-test.gts @@ -22,7 +22,7 @@ import OperatorMode from '@cardstack/host/components/operator-mode/container'; import MatrixService from '@cardstack/host/services/matrix-service'; import OperatorModeStateService from '@cardstack/host/services/operator-mode-state-service'; -import { currentRoomIdPersistenceKey } from '@cardstack/host/utils/local-storage-keys'; +import { CurrentRoomIdPersistenceKey } from '@cardstack/host/utils/local-storage-keys'; import type { CommandResultEvent } from 'https://cardstack.com/base/matrix-event'; @@ -973,9 +973,12 @@ module('Integration | ai-assistant-panel', function (hooks) { module('suspending global error hook', (hooks) => { let tmp: any; + let uncaughtException: any; hooks.before(() => { tmp = QUnit.onUncaughtException; - QUnit.onUncaughtException = () => {}; + QUnit.onUncaughtException = (err) => { + uncaughtException = err; + }; }); hooks.after(() => { @@ -1004,6 +1007,7 @@ module('Integration | ai-assistant-panel', function (hooks) { assert.dom('[data-test-room-error]').exists(); assert.dom('[data-test-room]').doesNotExist(); assert.dom('[data-test-past-sessions-button]').isDisabled(); + assert.strictEqual(uncaughtException.message, 'Intentional error thrown'); await percySnapshot( 'Integration | ai-assistant-panel | it can handle an error during room creation | error state', ); @@ -1059,7 +1063,7 @@ module('Integration | ai-assistant-panel', function (hooks) { await click('[data-test-close-ai-assistant]'); window.localStorage.setItem( - currentRoomIdPersistenceKey, + CurrentRoomIdPersistenceKey, "room-id-that-doesn't-exist-and-should-not-break-the-implementation", ); await click('[data-test-open-ai-assistant]'); @@ -1070,7 +1074,7 @@ module('Integration | ai-assistant-panel', function (hooks) { "test room 2 is the most recently created room and it's opened initially", ); } finally { - window.localStorage.removeItem(currentRoomIdPersistenceKey); // Cleanup + window.localStorage.removeItem(CurrentRoomIdPersistenceKey); // Cleanup } }); From a8f2c9706f74dbd46bf362efed5fcf0ce2823a44 Mon Sep 17 00:00:00 2001 From: Luke Melia Date: Wed, 11 Dec 2024 17:34:31 -0500 Subject: [PATCH 9/9] Take advantage of updated Command#execute API --- .../create-boxel-app-command.ts | 26 ++-------- .../create-product-requirements-command.ts | 18 +++---- .../AiAppGenerator/generate-code-command.ts | 10 ++-- .../product-requirement-document.gts | 35 +++++--------- .../app/components/ai-assistant/panel.gts | 13 ++--- packages/host/app/components/matrix/room.gts | 10 ++-- .../operator-mode/card-error-detail.gts | 4 +- .../host/tests/acceptance/commands-test.gts | 47 +++++++------------ packages/host/tests/cards/person.gts | 8 ++-- .../commands/switch-submode-test.gts | 6 +-- .../commands/write-text-file-test.gts | 28 ++++------- 11 files changed, 68 insertions(+), 137 deletions(-) diff --git a/packages/catalog-realm/AiAppGenerator/create-boxel-app-command.ts b/packages/catalog-realm/AiAppGenerator/create-boxel-app-command.ts index 58da9d62b2..58f088301e 100644 --- a/packages/catalog-realm/AiAppGenerator/create-boxel-app-command.ts +++ b/packages/catalog-realm/AiAppGenerator/create-boxel-app-command.ts @@ -6,7 +6,6 @@ import CreateProductRequirementsInstance, { import ShowCardCommand from '@cardstack/boxel-host/commands/show-card'; import WriteTextFileCommand from '@cardstack/boxel-host/commands/write-text-file'; import GenerateCodeCommand from './generate-code-command'; -import { GenerateCodeInput } from './generate-code-command'; import { AppCard } from '../app-card'; import SaveCardCommand from '@cardstack/boxel-host/commands/save-card'; @@ -27,36 +26,26 @@ export default class CreateBoxelApp extends Command< await createPRDCommand.execute(input); let showCardCommand = new ShowCardCommand(this.commandContext); - let ShowCardInput = await showCardCommand.getInputType(); - - let showPRDCardInput = new ShowCardInput(); - showPRDCardInput.cardToShow = prdCard; - await showCardCommand.execute(showPRDCardInput); + await showCardCommand.execute({ cardToShow: prdCard }); let generateCodeCommand = new GenerateCodeCommand(this.commandContext); - let generateCodeInput = new GenerateCodeInput({ + let { code, appName } = await generateCodeCommand.execute({ roomId, productRequirements: prdCard, }); - let { code, appName } = await generateCodeCommand.execute( - generateCodeInput, - ); - // Generate a unique name for the module using timestamp let timestamp = Date.now(); let moduleName = `generated-apps/${timestamp}/${appName}`; let filePath = `${moduleName}.gts`; let moduleId = new URL(moduleName, input.realm).href; let writeFileCommand = new WriteTextFileCommand(this.commandContext); - let writeFileInput = new (await writeFileCommand.getInputType())({ + await writeFileCommand.execute({ path: filePath, content: code, realm: input.realm, }); - await writeFileCommand.execute(writeFileInput); - // get the app card def from the module let loader = (import.meta as any).loader; let module = await loader.import(moduleId + '.gts'); @@ -77,18 +66,13 @@ export default class CreateBoxelApp extends Command< // save card let saveCardCommand = new SaveCardCommand(this.commandContext); - let SaveCardInputType = await saveCardCommand.getInputType(); - - let saveCardInput = new SaveCardInputType({ + await saveCardCommand.execute({ realm: input.realm, card: myAppCard, }); - await saveCardCommand.execute(saveCardInput); // show the app card - let showAppCardInput = new ShowCardInput(); - showAppCardInput.cardToShow = myAppCard; - await showCardCommand.execute(showAppCardInput); + await showCardCommand.execute({ cardToShow: myAppCard }); return myAppCard; } diff --git a/packages/catalog-realm/AiAppGenerator/create-product-requirements-command.ts b/packages/catalog-realm/AiAppGenerator/create-product-requirements-command.ts index 7647c32660..50c1062450 100644 --- a/packages/catalog-realm/AiAppGenerator/create-product-requirements-command.ts +++ b/packages/catalog-realm/AiAppGenerator/create-product-requirements-command.ts @@ -73,20 +73,16 @@ export default class CreateProductRequirementsInstance extends Command< let createRoomCommand = new CreateAIAssistantRoomCommand( this.commandContext, ); - let { roomId } = await createRoomCommand.execute( - new (await createRoomCommand.getInputType())({ - name: 'Product Requirements Doc Creation', - }), - ); + let { roomId } = await createRoomCommand.execute({ + name: 'Product Requirements Doc Creation', + }); let addSkillsToRoomCommand = new AddSkillsToRoomCommand( this.commandContext, ); - await addSkillsToRoomCommand.execute( - new (await addSkillsToRoomCommand.getInputType())({ - roomId, - skills: [this.skillCard], - }), - ); + await addSkillsToRoomCommand.execute({ + roomId, + skills: [this.skillCard], + }); await this.commandContext.sendAiAssistantMessage({ roomId, show: false, // maybe? open the side panel diff --git a/packages/catalog-realm/AiAppGenerator/generate-code-command.ts b/packages/catalog-realm/AiAppGenerator/generate-code-command.ts index a8dd37e3fa..99a04e6785 100644 --- a/packages/catalog-realm/AiAppGenerator/generate-code-command.ts +++ b/packages/catalog-realm/AiAppGenerator/generate-code-command.ts @@ -258,12 +258,10 @@ import { on } from '@ember/modifier'; let addSkillsToRoomCommand = new AddSkillsToRoomCommand( this.commandContext, ); - await addSkillsToRoomCommand.execute( - new (await addSkillsToRoomCommand.getInputType())({ - roomId: input.roomId, - skills: [this.skillCard], - }), - ); + await addSkillsToRoomCommand.execute({ + roomId: input.roomId, + skills: [this.skillCard], + }); await this.commandContext.sendAiAssistantMessage({ roomId: input.roomId, show: false, // maybe? open the side panel diff --git a/packages/catalog-realm/product-requirement-document.gts b/packages/catalog-realm/product-requirement-document.gts index 9c4cdb126f..81858af57c 100644 --- a/packages/catalog-realm/product-requirement-document.gts +++ b/packages/catalog-realm/product-requirement-document.gts @@ -23,7 +23,6 @@ import SaveCardCommand from '@cardstack/boxel-host/commands/save-card'; import WriteTextFileCommand from '@cardstack/boxel-host/commands/write-text-file'; import GenerateCodeCommand from './AiAppGenerator/generate-code-command'; -import { GenerateCodeInput } from './AiAppGenerator/generate-code-command'; import { restartableTask } from 'ember-concurrency'; class Isolated extends Component { @@ -254,33 +253,27 @@ class Isolated extends Component { this.errorMessage = ''; try { let createRoomCommand = new CreateAIAssistantRoomCommand(commandContext); - let { roomId } = await createRoomCommand.execute( - new (await createRoomCommand.getInputType())({ - name: 'AI Assistant Room', - }), - ); + let { roomId } = await createRoomCommand.execute({ + name: 'AI Assistant Room', + }); let generateCodeCommand = new GenerateCodeCommand(commandContext); - let generateCodeInput = new GenerateCodeInput({ - productRequirements: this.args.model, + let { code, appName } = await generateCodeCommand.execute({ + productRequirements: this.args.model as ProductRequirementDocument, roomId, }); - let { code, appName } = - await generateCodeCommand.execute(generateCodeInput); - // Generate a unique name for the module using timestamp let timestamp = Date.now(); let moduleName = `generated-apps/${timestamp}/${appName}`; let filePath = `${moduleName}.gts`; let moduleId = new URL(moduleName, this.currentRealm).href; let writeFileCommand = new WriteTextFileCommand(commandContext); - let writeFileInput = new (await writeFileCommand.getInputType())({ + + await writeFileCommand.execute({ path: filePath, content: code, - realm: this.currentRealm, + realm: this.currentRealm?.href, }); - - await writeFileCommand.execute(writeFileInput); this.args.model.moduleURL = moduleId; } catch (e) { console.error(e); @@ -325,23 +318,17 @@ class Isolated extends Component { }); let saveCardCommand = new SaveCardCommand(commandContext); - let SaveCardInputType = await saveCardCommand.getInputType(); - - let saveCardInput = new SaveCardInputType({ - realm: this.currentRealm, + await saveCardCommand.execute({ + realm: this.currentRealm.href, card: myAppCard, }); - await saveCardCommand.execute(saveCardInput); // show the app card let showCardCommand = new ShowCardCommand(commandContext); - let ShowCardInput = await showCardCommand.getInputType(); - - let showAppCardInput = new ShowCardInput({ + await showCardCommand.execute({ cardToShow: myAppCard, }); - await showCardCommand.execute(showAppCardInput); if (!myAppCard) { throw new Error('Could not create card'); diff --git a/packages/host/app/components/ai-assistant/panel.gts b/packages/host/app/components/ai-assistant/panel.gts index b6c6c83205..f58c18561c 100644 --- a/packages/host/app/components/ai-assistant/panel.gts +++ b/packages/host/app/components/ai-assistant/panel.gts @@ -432,18 +432,15 @@ export default class AiAssistantPanel extends Component { let createRoomCommand = new CreateAIAssistantRoomCommand( this.commandService.commandContext, ); - let InputType = await createRoomCommand.getInputType(); - let { roomId } = await createRoomCommand.execute(new InputType({ name })); + let { roomId } = await createRoomCommand.execute({ name }); let addSkillsToRoomCommand = new AddSkillsToRoomCommand( this.commandService.commandContext, ); - await addSkillsToRoomCommand.execute( - new (await addSkillsToRoomCommand.getInputType())({ - roomId, - skills: await this.matrixService.loadDefaultSkills(), - }), - ); + await addSkillsToRoomCommand.execute({ + roomId, + skills: await this.matrixService.loadDefaultSkills(), + }); window.localStorage.setItem(NewSessionIdPersistenceKey, roomId); this.enterRoom(roomId); } catch (e) { diff --git a/packages/host/app/components/matrix/room.gts b/packages/host/app/components/matrix/room.gts index f304202fa0..4c75505966 100644 --- a/packages/host/app/components/matrix/room.gts +++ b/packages/host/app/components/matrix/room.gts @@ -635,12 +635,10 @@ export default class Room extends Component { let addSkillsToRoomCommand = new AddSkillsToRoomCommand( this.commandService.commandContext, ); - await addSkillsToRoomCommand.execute( - new (await addSkillsToRoomCommand.getInputType())({ - roomId: this.args.roomId, - skills: [card], - }), - ); + await addSkillsToRoomCommand.execute({ + roomId: this.args.roomId, + skills: [card], + }); }); } diff --git a/packages/host/app/components/operator-mode/card-error-detail.gts b/packages/host/app/components/operator-mode/card-error-detail.gts index 9f6ef9485f..2d876a36bb 100644 --- a/packages/host/app/components/operator-mode/card-error-detail.gts +++ b/packages/host/app/components/operator-mode/card-error-detail.gts @@ -35,12 +35,10 @@ export default class CardErrorDetail extends Component { let switchSubmodeCommand = new SwitchSubmodeCommand( this.commandService.commandContext, ); - const InputType = await switchSubmodeCommand.getInputType(); - let input = new InputType({ + await switchSubmodeCommand.execute({ submode: 'code', codePath: `${this.args.error.id}.json`, }); - await switchSubmodeCommand.execute(input); });