Skip to content

Commit 88e77dd

Browse files
authored
Merge pull request #2192 from cardstack/cs-7852-add-skill-for-editing-code-of-modules-and-instances
Add skill for editing card definitions
2 parents 9cac01e + 37fc68e commit 88e77dd

File tree

5 files changed

+82
-9
lines changed

5 files changed

+82
-9
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"data": {
3+
"type": "card",
4+
"attributes": {
5+
"instructions": "Boxel is a platform where people can create Cards, which under the hood are built out of glimmer components and ember.\n\nCards are independent linkable items that get an ID. Fields are contained within cards, so sometimes a user wants a custom field, but usually it's creating a card (derived from CardDef).\n\nUse glimmer templating and typescript for the code. Remember the limitations of logic within glimmer templating code. Basic interaction for editing fields is handled for you by boxel, you don't need to create that (e.g. StringField has an edit template that allows a user to edit the data). Computed fields can support more complex work, and update automatically for you. Interaction (button clicks, filtering on user typed content) may require glimmer & ember functionality (see action and tracked in the example below).\n\nCards you create have three templates. If you do not specify them they are automatically created for you, but users often want custom templates. Each template is a glimmer template and can use ember functionality. These are specified as static in the card definition:\n\nimport { contains, containsMany, linksToMany, field, CardDef, Component, } from 'https://cardstack.com/base/card-api'; \nimport StringField from 'https://cardstack.com/base/string'; \nimport NumberField from 'https://cardstack.com/base/number'; \nimport BooleanField from 'https://cardstack.com/base/boolean'; \n// Important, this is the tracked decorator \nimport { tracked } from '@glimmer/tracking'; \nimport { action } from '@ember/object'; \nimport { fn } from '@ember/helper'; \nimport { on } from '@ember/modifier';\n\nexport class MyCustomCard extends CardDef { \n\nstatic displayName = 'BoxelBuddyGuestList';\n\n// linksTo and linksToMany \n@field linkedData = linksToMany(() => AnotherCard);\n\n// A field that is computed from other data in the card \n@field computedData = contains(NumberField, { computeVia: function (this: MyCustomCard) { \n// implementation logic here \nreturn 1; }, \n});\n\n// Isolated templates are used when items are viewed on their own. Default to the isolated template \nstatic isolated = class Isolated extends Component { \n// Use tracked and action decorators to be able to use interactivity in the templates \n@tracked trackedValue = []; \n@action interactivity(event: InputEvent) {} \n\n// Glimmer template goes here, make sure the style tag is at the top level inside the template tag\n<template></template>\n}; \n\n// Embedded is when they appear in other cards \nstatic embedded = class Embedded extends Component { };\n\n// Fitted templates should be responsive to the size of the container they appear in \nstatic fitted = class Fitted extends Component { };\n\n// Edit is for the user editing the data. Use @fields let the field render itself \nstatic edit = class Edit extends Component { }; \n}\n\n@fields.fieldName lets the field render itself, very useful for editable fields. @model.fieldName gets the value out of the field.\n\nImportant:\n\nIt is extremely important you use the following imports for interactivity: \nimport { tracked } from '@glimmer/tracking'; \nimport { action } from '@ember/object'; \nimport { fn, get } from '@ember/helper'; \nimport { on } from '@ember/modifier';\n\nRemember to define a field the following syntax is used:\n\n@field fieldname = contains(FieldType); @field fieldname = containsMany(FieldType);\n\nIf user asks you to make something editable, use `contains` or `containsMany` syntax for adding a field.\n\nAnd for linking to other cards:\n\n@field fieldname = linksTo(() => CardType); \n@field fieldname = linksToMany(() => CardType);\n\nYou can ask followups\n\nYou can propose new/improved data structures\n\nWhen writing the glimmer template, ensure that the style tags appear within the template tag, as the last item in them. You should use useful class names and a sensible structure as you build this. Use single quotes for the class names.\n\nWhen writing this, take care to remember ember and glimmer oddities. Accessing a list by index should use this format:\n\n{{(get this.args.model.fieldWithAList index)}}\n\nValues from the model can be directly inserted with\n\n{{this.args.model.fieldName}}\n\nand you can delegate rendering to the field with\n\n<@fields.fieldName />\n\nYou must be careful with the templates, remember glimmer rules. Do not put a dollar sign ($) directly in front of the brackets.\n\nAlways use scoped attributed when you generate a style tag, like so: <style scoped> ... CSS code ... </style.\n\nUnless otherwise instructed, use a modern but stylish theme. In responses regarding to attached files, respond with a series of code patches where you output gts code and mark it whether it's for adding or deleting, in a clear succession, so that user can quickly just copy paste and put it in the code file, or delete code. This should include the file name and line numbers where the change should happen.",
6+
"title": "Code Module Editing",
7+
"description": null,
8+
"thumbnailURL": null
9+
},
10+
"meta": {
11+
"adoptsFrom": {
12+
"module": "../skill-card",
13+
"name": "SkillCard"
14+
}
15+
}
16+
}
17+
}

packages/host/app/components/ai-assistant/panel.gts

+3-1
Original file line numberDiff line numberDiff line change
@@ -471,7 +471,9 @@ export default class AiAssistantPanel extends Component<Signature> {
471471
);
472472
await addSkillsToRoomCommand.execute({
473473
roomId,
474-
skills: await this.matrixService.loadDefaultSkills(),
474+
skills: await this.matrixService.loadDefaultSkills(
475+
this.operatorModeStateService.state.submode,
476+
),
475477
});
476478
window.localStorage.setItem(NewSessionIdPersistenceKey, roomId);
477479
this.enterRoom(roomId);

packages/host/app/services/matrix-service.ts

+31-3
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ import type {
7878
import type { Tool } from 'https://cardstack.com/base/matrix-event';
7979
import { SkillCard } from 'https://cardstack.com/base/skill-card';
8080

81+
import AddSkillsToRoomCommand from '../commands/add-skills-to-room';
8182
import { importResource } from '../resources/import';
8283

8384
import { RoomResource, getRoom } from '../resources/room';
@@ -100,7 +101,6 @@ import type * as MatrixSDK from 'matrix-js-sdk';
100101
const { matrixURL } = ENV;
101102
const MAX_CARD_SIZE_KB = 60;
102103
const STATE_EVENTS_OF_INTEREST = ['m.room.create', 'm.room.name'];
103-
const DefaultSkillCards = [`${baseRealm.url}SkillCard/card-editing`];
104104

105105
export type OperatorModeContext = {
106106
submode: Submode;
@@ -814,9 +814,23 @@ export default class MatrixService extends Service {
814814
}
815815
}
816816

817-
async loadDefaultSkills() {
817+
async loadDefaultSkills(submode: Submode) {
818+
let interactModeDefaultSkills = [`${baseRealm.url}SkillCard/card-editing`];
819+
820+
let codeModeDefaultSkills = [
821+
`${baseRealm.url}SkillCard/code-module-editing`,
822+
];
823+
824+
let defaultSkills;
825+
826+
if (submode === 'code') {
827+
defaultSkills = codeModeDefaultSkills;
828+
} else {
829+
defaultSkills = interactModeDefaultSkills;
830+
}
831+
818832
return await Promise.all(
819-
DefaultSkillCards.map(async (skillCardURL) => {
833+
defaultSkills.map(async (skillCardURL) => {
820834
return await this.cardService.getCard<SkillCard>(skillCardURL);
821835
}),
822836
);
@@ -1365,6 +1379,20 @@ export default class MatrixService extends Service {
13651379
}
13661380
}
13671381

1382+
async activateCodingSkill() {
1383+
if (!this.currentRoomId) {
1384+
return;
1385+
}
1386+
1387+
let addSkillsToRoomCommand = new AddSkillsToRoomCommand(
1388+
this.commandService.commandContext,
1389+
);
1390+
await addSkillsToRoomCommand.execute({
1391+
roomId: this.currentRoomId,
1392+
skills: await this.loadDefaultSkills('code'),
1393+
});
1394+
}
1395+
13681396
async setLLMForCodeMode() {
13691397
this.setLLMModel('anthropic/claude-3.5-sonnet');
13701398
}

packages/host/app/services/operator-mode-state-service.ts

+1
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ export default class OperatorModeStateService extends Service {
318318

319319
if (submode === Submodes.Code) {
320320
this.matrixService.setLLMForCodeMode();
321+
this.matrixService.activateCodingSkill();
321322
}
322323
}
323324

packages/matrix/tests/skills.spec.ts

+30-5
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ test.describe('Skills', () => {
6666
).toHaveClass('switch checked');
6767
}
6868

69-
const defaultSkillCard = `https://cardstack.com/base/SkillCard/card-editing`;
69+
const defaultSkillCardForInteractMode = `https://cardstack.com/base/SkillCard/card-editing`;
70+
const defaultSkillCardForCodeMode = `https://cardstack.com/base/SkillCard/code-module-editing`;
7071
const skillCard1 = `${appURL}/skill-pirate-speak`;
7172
const skillCard2 = `${appURL}/skill-seo`;
7273
const skillCard3 = `${appURL}/skill-card-title-editing`;
@@ -111,10 +112,14 @@ test.describe('Skills', () => {
111112
).toHaveText('Hide');
112113
await expect(page.locator('[data-test-pill-menu-item]')).toHaveCount(1);
113114
await expect(
114-
page.locator(`[data-test-pill-menu-item="${defaultSkillCard}"]`),
115+
page.locator(
116+
`[data-test-pill-menu-item="${defaultSkillCardForInteractMode}"]`,
117+
),
115118
).toHaveCount(1);
116119
await expect(
117-
page.locator(`[data-test-card-pill-toggle="${defaultSkillCard}-on"]`),
120+
page.locator(
121+
`[data-test-card-pill-toggle="${defaultSkillCardForInteractMode}-on"]`,
122+
),
118123
).toHaveCount(1);
119124
await expect(page.locator('[data-test-pill-menu-add-button]')).toHaveCount(
120125
1,
@@ -172,6 +177,22 @@ test.describe('Skills', () => {
172177
);
173178
});
174179

180+
test('it will attach code editing skill in code mode by default', async ({
181+
page,
182+
}) => {
183+
await login(page, 'user1', 'pass', { url: appURL });
184+
185+
await page.locator('[data-test-submode-switcher] button').click();
186+
await page.locator('[data-test-boxel-menu-item-text="Code"]').click();
187+
await page.locator('[data-test-skill-menu]').hover();
188+
await page.locator('[data-test-pill-menu-header-button]').click();
189+
await expect(
190+
page.locator(
191+
`[data-test-attached-card="${defaultSkillCardForCodeMode}"]`,
192+
),
193+
).toHaveCount(1);
194+
});
195+
175196
test(`room skills state does not leak when switching rooms`, async ({
176197
page,
177198
}) => {
@@ -255,7 +276,9 @@ test.describe('Skills', () => {
255276
await page.locator('[data-test-skill-menu]').hover();
256277
await page.locator('[data-test-pill-menu-header-button]').click();
257278
await page
258-
.locator(`[data-test-card-pill-toggle="${defaultSkillCard}-on"]`)
279+
.locator(
280+
`[data-test-card-pill-toggle="${defaultSkillCardForInteractMode}-on"]`,
281+
)
259282
.click(); // toggle off default skill card
260283
await page
261284
.locator(`[data-test-card-pill-toggle="${skillCard1}-on"]`)
@@ -264,7 +287,9 @@ test.describe('Skills', () => {
264287
.locator(`[data-test-card-pill-toggle="${skillCard2}-on"]`)
265288
.click(); // toggle off skill 2
266289
await expect(
267-
page.locator(`[data-test-card-pill-toggle="${defaultSkillCard}-off"]`),
290+
page.locator(
291+
`[data-test-card-pill-toggle="${defaultSkillCardForInteractMode}-off"]`,
292+
),
268293
).toHaveCount(1);
269294
await expect(
270295
page.locator(`[data-test-card-pill-toggle="${skillCard1}-off"]`),

0 commit comments

Comments
 (0)