Skip to content

Commit 5b600fe

Browse files
committed
Merge branch 'main' into return-of-the-skill-commands
2 parents 4309af8 + 62f68bc commit 5b600fe

25 files changed

+503
-232
lines changed

packages/base/SkillCard/code-module-editing.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
"data": {
33
"type": "card",
44
"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.",
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'; import StringField from 'https://cardstack.com/base/string'; import NumberField from 'https://cardstack.com/base/number'; import BooleanField from 'https://cardstack.com/base/boolean'; // Important, this is the tracked decorator import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { fn } from '@ember/helper'; import { on } from '@ember/modifier';\n\nexport class MyCustomCard extends CardDef {\n\nstatic displayName = 'BoxelBuddyGuestList';\n\n// linksTo and linksToMany @field linkedData = linksToMany(() => AnotherCard);\n\n// A field that is computed from other data in the card @field computedData = contains(NumberField, { computeVia: function (this: MyCustomCard) { // implementation logic here return 1; }, });\n\n// Isolated templates are used when items are viewed on their own. Default to the isolated template static isolated = class Isolated extends Component { // Use tracked and action decorators to be able to use interactivity in the templates @tracked trackedValue = []; @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\n// Embedded is when they appear in other cards static embedded = class Embedded extends Component { };\n\n// Fitted templates should be responsive to the size of the container they appear in static fitted = class Fitted extends Component { };\n\n// Edit is for the user editing the data. Use @fields let the field render itself static edit = class Edit extends Component { }; }\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: import { tracked } from '@glimmer/tracking'; import { action } from '@ember/object'; import { fn, get } from '@ember/helper'; import { 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); @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\n<style> must be inside <template>, and the style tag must be the first child of <template>, not nested. So when you use <template>, the anatomy of should look like this:\n<template>\n... any html content...\n<style scoped>\n</style>\n</template>\n\nUnless otherwise instructed, use a modern but stylish theme. \n\nIn 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. \n\nUse multiple code snippets for every code change.",
6+
"commands": [],
67
"title": "Code Module Editing",
78
"description": null,
89
"thumbnailURL": null
@@ -14,4 +15,4 @@
1415
}
1516
}
1617
}
17-
}
18+
}

packages/base/code-ref.gts

+76-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type Owner from '@ember/owner';
2+
import { tracked } from '@glimmer/tracking';
13
import {
24
Component,
35
primitive,
@@ -13,7 +15,14 @@ import {
1315
type SerializeOpts,
1416
type JSONAPISingleResourceDocument,
1517
} from './card-api';
16-
import { ResolvedCodeRef } from '@cardstack/runtime-common';
18+
import { restartableTask } from 'ember-concurrency';
19+
import { consume } from 'ember-provide-consume-context';
20+
import {
21+
type ResolvedCodeRef,
22+
CardURLContextName,
23+
} from '@cardstack/runtime-common';
24+
import { not } from '@cardstack/boxel-ui/helpers';
25+
import { BoxelInput } from '@cardstack/boxel-ui/components';
1726
import CodeIcon from '@cardstack/boxel-icons/code';
1827

1928
function moduleIsUrlLike(module: string) {
@@ -35,6 +44,70 @@ class BaseView extends Component<typeof CodeRefField> {
3544
</template>
3645
}
3746

47+
class EditView extends Component<typeof CodeRefField> {
48+
@consume(CardURLContextName) declare cardURL: string | undefined;
49+
@tracked validationState: 'initial' | 'valid' | 'invalid' = 'initial';
50+
@tracked private maybeCodeRef: string | undefined = maybeSerializeCodeRef(
51+
this.args.model ?? undefined,
52+
);
53+
54+
<template>
55+
<BoxelInput
56+
data-test-hasValidated={{this.setIfValid.isIdle}}
57+
@value={{this.maybeCodeRef}}
58+
@state={{this.validationState}}
59+
@onInput={{this.onInput}}
60+
@disabled={{not @canEdit}}
61+
/>
62+
</template>
63+
64+
constructor(owner: Owner, args: any) {
65+
super(owner, args);
66+
if (this.maybeCodeRef != null) {
67+
this.setIfValid.perform(this.maybeCodeRef, { checkOnly: true });
68+
}
69+
}
70+
71+
private onInput = (inputVal: string) => {
72+
this.maybeCodeRef = inputVal;
73+
this.setIfValid.perform(this.maybeCodeRef);
74+
};
75+
76+
private setIfValid = restartableTask(
77+
async (maybeCodeRef: string, opts?: { checkOnly?: true }) => {
78+
this.validationState = 'initial';
79+
if (maybeCodeRef.length === 0) {
80+
if (!opts?.checkOnly) {
81+
this.args.set(undefined);
82+
}
83+
return;
84+
}
85+
86+
let parts = maybeCodeRef.split('/');
87+
if (parts.length < 2) {
88+
this.validationState = 'invalid';
89+
return;
90+
}
91+
92+
let name = parts.pop()!;
93+
let module = parts.join('/');
94+
try {
95+
let code = (await import(module))[name];
96+
if (code) {
97+
this.validationState = 'valid';
98+
if (!opts?.checkOnly) {
99+
this.args.set({ module, name });
100+
}
101+
} else {
102+
this.validationState = 'invalid';
103+
}
104+
} catch (err) {
105+
this.validationState = 'invalid';
106+
}
107+
},
108+
);
109+
}
110+
38111
export default class CodeRefField extends FieldDef {
39112
static icon = CodeIcon;
40113
static [primitive]: ResolvedCodeRef;
@@ -71,8 +144,8 @@ export default class CodeRefField extends FieldDef {
71144
}
72145

73146
static embedded = class Embedded extends BaseView {};
74-
// The edit template is meant to be read-only, this field card is not mutable
75-
static edit = class Edit extends BaseView {};
147+
148+
static edit = EditView;
76149
}
77150

78151
function maybeSerializeCodeRef(

packages/experiments-realm/CrmApp/4e73712d-2a31-4ffe-9c22-d3de277257a6.json

-46
Original file line numberDiff line numberDiff line change
@@ -2,52 +2,6 @@
22
"data": {
33
"type": "card",
44
"attributes": {
5-
"tabs": [
6-
{
7-
"displayName": "Contacts",
8-
"tabId": "Contact",
9-
"ref": {
10-
"name": "Contact",
11-
"module": "crm/contact"
12-
},
13-
"isTable": false
14-
},
15-
{
16-
"displayName": "Accounts",
17-
"tabId": "Account",
18-
"ref": {
19-
"name": "Account",
20-
"module": "crm/account"
21-
},
22-
"isTable": false
23-
},
24-
{
25-
"displayName": "Deals",
26-
"tabId": "Deal",
27-
"ref": {
28-
"name": "Deal",
29-
"module": "crm/deal"
30-
},
31-
"isTable": false
32-
},
33-
{
34-
"displayName": "Tasks",
35-
"tabId": "Task",
36-
"ref": {
37-
"name": "Task",
38-
"module": "crm/task"
39-
},
40-
"isTable": false
41-
}
42-
],
43-
"headerIcon": {
44-
"altText": null,
45-
"size": "actual",
46-
"height": null,
47-
"width": null,
48-
"base64": null
49-
},
50-
"moduleId": null,
515
"title": "Vibeverse",
526
"description": "Feel the moment",
537
"thumbnailURL": "https://boxel-images.boxel.ai/icons/vibeverse.jpg"

packages/experiments-realm/crm-app.gts

+6-6
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,10 @@ const TASK_FILTERS: LayoutFilter[] = [
187187
];
188188

189189
const TABS = [
190+
{
191+
tabId: 'Account',
192+
displayName: 'Accounts',
193+
},
190194
{
191195
tabId: 'Contact',
192196
displayName: 'Contacts',
@@ -195,10 +199,6 @@ const TABS = [
195199
tabId: 'Deal',
196200
displayName: 'Deals',
197201
},
198-
{
199-
tabId: 'Account',
200-
displayName: 'Accounts',
201-
},
202202
{
203203
tabId: 'Task',
204204
displayName: 'Tasks',
@@ -209,12 +209,12 @@ const TABS = [
209209
class CrmAppTemplate extends Component<typeof CrmApp> {
210210
//filters
211211
filterMap: TrackedMap<string, LayoutFilter[]> = new TrackedMap([
212+
['Account', ACCOUNT_FILTERS],
212213
['Contact', CONTACT_FILTERS],
213214
['Deal', DEAL_FILTERS],
214-
['Account', ACCOUNT_FILTERS],
215215
['Task', TASK_FILTERS],
216216
]);
217-
@tracked private activeFilter: LayoutFilter = CONTACT_FILTERS[0];
217+
@tracked private activeFilter: LayoutFilter = ACCOUNT_FILTERS[0];
218218
@action private onFilterChange(filter: LayoutFilter) {
219219
this.activeFilter = filter;
220220
if (this.activeTabId === 'Task') {

packages/host/app/components/ai-assistant/formatted-message.gts

+1-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ export default class FormattedMessage extends Component<FormattedMessageSignatur
112112
:deep(.code-copy-button .copy-text) {
113113
color: transparent;
114114
}
115-
:deep(.code-copy-button .copy-text:hover) {
115+
:deep(.code-copy-button:hover .copy-text) {
116116
color: var(--boxel-highlight);
117117
}
118118
</style>

packages/host/app/components/ai-assistant/message/index.gts

+8-6
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ export default class AiAssistantMessage extends Component<Signature> {
261261
{{/if}}
262262

263263
{{#if @resources.errors.length}}
264-
<div class='error-container'>
264+
<div class='error-container error-footer'>
265265
{{#each @resources.errors as |resourceError|}}
266266
<FailureBordered class='error-icon' />
267267
<div class='error-message' data-test-card-error>
@@ -348,12 +348,7 @@ export default class AiAssistantMessage extends Component<Signature> {
348348
padding: var(--ai-assistant-message-padding, var(--boxel-sp));
349349
}
350350
351-
.is-from-assistant .content :deep(.message) {
352-
padding: var(--ai-assistant-message-padding, var(--boxel-sp));
353-
}
354-
355351
.is-from-assistant .content {
356-
padding: 0;
357352
background-color: var(--ai-bot-message-background-color);
358353
color: var(--boxel-light);
359354
/* the below font-smoothing options are only recommended for light-colored
@@ -422,6 +417,13 @@ export default class AiAssistantMessage extends Component<Signature> {
422417
font: 600 var(--boxel-font-sm);
423418
letter-spacing: var(--boxel-lsp);
424419
}
420+
.error-footer {
421+
--fill-container-spacing: calc(
422+
-1 * var(--ai-assistant-message-padding)
423+
);
424+
margin-inline: var(--fill-container-spacing);
425+
margin-bottom: var(--fill-container-spacing);
426+
}
425427
.error-icon {
426428
--icon-background-color: var(--boxel-light);
427429
--icon-color: var(--boxel-danger);

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,9 @@ export default class AiAssistantPanel extends Component<Signature> {
444444

445445
private loadRoomsTask = restartableTask(async () => {
446446
await this.matrixService.flushAll;
447-
await Promise.all([...this.roomResources.values()].map((r) => r.loading));
447+
await Promise.allSettled(
448+
[...this.roomResources.values()].map((r) => r.loading),
449+
);
448450
await this.enterRoomInitially();
449451
});
450452

packages/host/app/components/matrix/room-message-command.gts

+23-6
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ export default class RoomMessageCommand extends Component<Signature> {
198198
{{#if @isDisplayingCode}}
199199
<div class='preview-code'>
200200
<Button
201-
class='copy-to-clipboard-button'
201+
class='code-copy-button'
202202
@kind='text-only'
203203
@size='extra-small'
204204
{{on 'click' this.copyToClipboard}}
@@ -210,7 +210,7 @@ export default class RoomMessageCommand extends Component<Signature> {
210210
role='presentation'
211211
aria-hidden='true'
212212
/>
213-
Copy to clipboard
213+
<span class='copy-text'>Copy to clipboard</span>
214214
</Button>
215215
<div
216216
class='monaco-container'
@@ -279,7 +279,17 @@ export default class RoomMessageCommand extends Component<Signature> {
279279
.command-result-card-preview {
280280
margin-top: var(--boxel-sp);
281281
}
282-
.copy-to-clipboard-button {
282+
.preview-code {
283+
--spacing: var(--boxel-sp-sm);
284+
--fill-container-spacing: calc(
285+
-1 * var(--ai-assistant-message-padding)
286+
);
287+
margin: var(--boxel-sp) var(--fill-container-spacing)
288+
var(--fill-container-spacing) var(--fill-container-spacing);
289+
padding: var(--spacing) 0;
290+
background-color: var(--boxel-dark);
291+
}
292+
.code-copy-button {
283293
--boxel-button-font: 600 var(--boxel-font-xs);
284294
--boxel-button-padding: 0 var(--boxel-sp-xs);
285295
--icon-color: var(--boxel-highlight);
@@ -290,9 +300,16 @@ export default class RoomMessageCommand extends Component<Signature> {
290300
grid-template-columns: auto 1fr;
291301
gap: var(--spacing);
292302
}
293-
.copy-to-clipboard-button:hover:not(:disabled) {
294-
--boxel-button-text-color: var(--boxel-highlight);
295-
filter: brightness(1.1);
303+
.code-copy-button > .copy-text {
304+
color: transparent;
305+
}
306+
.code-copy-button:hover:not(:disabled) > .copy-text {
307+
color: var(--boxel-highlight);
308+
}
309+
.monaco-container {
310+
height: var(--monaco-container-height);
311+
min-height: 10rem;
312+
max-height: 30vh;
296313
}
297314
.header {
298315
--boxel-label-color: var(--boxel-450);

0 commit comments

Comments
 (0)