Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display AI panel in code mode #2076

Merged
merged 11 commits into from
Jan 23, 2025
15 changes: 14 additions & 1 deletion packages/host/app/components/matrix/room-message-command.gts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,6 @@ export default class RoomMessageCommand extends Component<Signature> {
monacoSDK=@monacoSDK
language='json'
readOnly=true
darkTheme=true
editorDisplayOptions=this.editorDisplayOptions
}}
data-test-editor
Expand Down Expand Up @@ -241,6 +240,9 @@ export default class RoomMessageCommand extends Component<Signature> {
</CardContainer>
{{/if}}
</div>

{{! template-lint-disable no-whitespace-for-layout }}
{{! ignore the above error because ember-template-lint complains about the whitespace in the multi-line comment below }}
<style scoped>
.is-pending .view-code-button,
.is-error .view-code-button {
Expand Down Expand Up @@ -319,6 +321,17 @@ export default class RoomMessageCommand extends Component<Signature> {
.options-menu :deep(.check-icon) {
display: none;
}
/*
This filter is a best-effort approximation of a good looking dark theme that is a function of the white theme that
we use for code previews in the AI panel. While Monaco editor does support multiple themes, it does not support
monaco instances with different themes *on the same page*. This is why we are using a filter to approximate the
dark theme. More details here: https://github.com/Microsoft/monaco-editor/issues/338 (monaco uses global style tags
with hardcoded colors; any instance will override the global style tag, making all code editors look the same,
effectively disabling multiple themes to be used on the same page)
*/
:global(.preview-code .monaco-editor) {
filter: invert(1) hue-rotate(151deg) brightness(0.8) grayscale(0.1);
}
</style>
</template>
}
4 changes: 4 additions & 0 deletions packages/host/app/components/matrix/room.gts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isDestroyed } from '@ember/destroyable';
import { fn } from '@ember/helper';
import { on } from '@ember/modifier';
import { action } from '@ember/object';
Expand Down Expand Up @@ -250,6 +251,9 @@ export default class Room extends Component<Signature> {
messageElemements: new WeakMap(),
messageScrollers: new Map(),
messageVisibilityObserver: new IntersectionObserver((entries) => {
if (isDestroyed(this)) {
Copy link
Contributor Author

@jurgenwerk jurgenwerk Jan 22, 2025

Choose a reason for hiding this comment

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

I was getting the You attempted to get the value of a helper after the helper was destroyed, which is not allowed"
error in the Commands tests: a command sent via SendAiAssistantMessageCommand without autoExecute flag is not automatically executed by the bot test.

My guess is that when the AI panel is opened in interactive mode and a switch to code mode happens, the room gets rerendered (due to operator mode state change) and the intersection observer wants to work on the previous room rendering but it can't since it was destroyed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Found a better approach: 47c35a1

return;
}
entries.forEach((entry) => {
let index = this.messageElements.get(entry.target as HTMLElement);
if (index != null) {
Expand Down
10 changes: 9 additions & 1 deletion packages/host/app/components/operator-mode/code-editor.gts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,15 @@ export default class CodeEditor extends Component<Signature> {
padding: var(--boxel-sp) 0;
}

.monaco-container.readonly {
:global(.monaco-container.readonly) {
background-color: #ebeaed;
}

:global(.monaco-container.readonly .margin) {
background-color: #ebeaed;
}

:global(.monaco-container.readonly .monaco-editor-background) {
background-color: #ebeaed;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,6 @@ export default class CodeSubmode extends Component<Signature> {
{{/let}}
<SubmodeLayout
@onCardSelectFromSearch={{this.openSearchResultInEditor}}
@hideAiAssistant={{true}}
as |search|
>
<div
Expand Down
29 changes: 11 additions & 18 deletions packages/host/app/components/operator-mode/submode-layout.gts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { TrackedObject } from 'tracked-built-ins';

import { ResizablePanelGroup } from '@cardstack/boxel-ui/components';
import { Avatar, IconButton } from '@cardstack/boxel-ui/components';
import { and, cn, not } from '@cardstack/boxel-ui/helpers';
import { cn, not } from '@cardstack/boxel-ui/helpers';

import { BoxelIcon } from '@cardstack/boxel-ui/icons';

Expand Down Expand Up @@ -47,7 +47,6 @@ import type OperatorModeStateService from '../../services/operator-mode-state-se
interface Signature {
Element: HTMLDivElement;
Args: {
hideAiAssistant?: boolean;
onSearchSheetOpened?: () => void;
onSearchSheetClosed?: () => void;
onCardSelectFromSearch: (cardId: string) => void;
Expand Down Expand Up @@ -309,23 +308,17 @@ export default class SubmodeLayout extends Component<Signature> {
@onCardSelect={{this.handleCardSelectFromSearch}}
@onInputInsertion={{this.storeSearchElement}}
/>
{{#if (not @hideAiAssistant)}}
<AiAssistantToast
@hide={{this.operatorModeStateService.aiAssistantOpen}}
@onViewInChatClick={{this.operatorModeStateService.toggleAiAssistant}}
/>
<AiAssistantButton
class='chat-btn'
@isActive={{this.operatorModeStateService.aiAssistantOpen}}
{{on 'click' this.operatorModeStateService.toggleAiAssistant}}
/>
{{/if}}
<AiAssistantToast
@hide={{this.operatorModeStateService.aiAssistantOpen}}
@onViewInChatClick={{this.operatorModeStateService.toggleAiAssistant}}
/>
<AiAssistantButton
class='chat-btn'
@isActive={{this.operatorModeStateService.aiAssistantOpen}}
{{on 'click' this.operatorModeStateService.toggleAiAssistant}}
/>
</ResizablePanel>
{{#if
(and
(not @hideAiAssistant) this.operatorModeStateService.aiAssistantOpen
)
}}
{{#if this.operatorModeStateService.aiAssistantOpen}}
<ResizablePanel
class='ai-assistant-resizable-panel'
@defaultSize={{this.aiPanelWidths.defaultWidth}}
Expand Down
22 changes: 8 additions & 14 deletions packages/host/app/modifiers/monaco.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ interface Signature {
language?: string;
readOnly?: boolean;
monacoSDK: typeof MonacoSDK;
darkTheme?: boolean;
editorDisplayOptions?: MonacoEditorOptions;
};
};
Expand Down Expand Up @@ -54,7 +53,6 @@ export default class Monaco extends Modifier<Signature> {
onSetup,
readOnly,
monacoSDK,
darkTheme,
editorDisplayOptions,
}: Signature['Args']['Named'],
) {
Expand All @@ -73,21 +71,17 @@ export default class Monaco extends Modifier<Signature> {
this.model.setValue(content);
}
} else {
monacoSDK.editor.defineTheme('boxel-theme', {
// The light theme editor is used for the main editor in code mode,
// but we also have a dark themed editor for the preview editor in AI panel.
// The latter is themed using a CSS filter as opposed to defining a new monaco theme
// because monaco does not support multiple themes on the same page (check the comment in
// room-message-command.gts for more details)
monacoSDK.editor.defineTheme('boxel-monaco-light-theme', {
base: 'vs',
inherit: true,
rules: [],
colors: {
'editor.background': readOnly ? '#EBEAED' : '#FFFFFF',
},
});

monacoSDK.editor.defineTheme('boxel-dark-theme', {
base: 'vs-dark',
inherit: true,
rules: [],
colors: {
'editor.background': '#000000',
'editor.background': '#FFFFFF',
},
});

Expand All @@ -100,7 +94,7 @@ export default class Monaco extends Modifier<Signature> {
minimap: {
enabled: false,
},
theme: darkTheme ? 'boxel-dark-theme' : 'boxel-theme',
theme: 'boxel-monaco-light-theme',
...editorDisplayOptions,
};

Expand Down
2 changes: 1 addition & 1 deletion packages/host/tests/acceptance/code-submode/editor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,7 @@ module('Acceptance | code submode | editor tests', function (hooks) {
assert.dom('[data-test-realm-indicator-not-writable]').exists();
assert.strictEqual(
window
.getComputedStyle(find('.monaco-editor')!)
.getComputedStyle(find('.monaco-editor-background')!)
.getPropertyValue('background-color')!,
'rgb(235, 234, 237)', // equivalent to #ebeaed
'monaco editor is greyed out when read-only',
Expand Down
21 changes: 21 additions & 0 deletions packages/host/tests/acceptance/operator-mode-acceptance-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -1186,5 +1186,26 @@ module('Acceptance | operator mode tests', function (hooks) {
.hasNoClass('out-of-credit');
assert.dom('[data-test-buy-more-credits]').hasNoClass('out-of-credit');
});

test(`ai panel continues being open when switching to code submode`, async function (assert) {
await visitOperatorMode({
stacks: [
[
{
id: `${testRealmURL}Person/fadhlan`,
format: 'isolated',
},
],
],
});

await click('[data-test-open-ai-assistant]');
assert.dom('[data-test-ai-assistant-panel]').exists();
await click('[data-test-submode-switcher] button');
await click('[data-test-boxel-menu-item-text="Code"]');
assert.dom('[data-test-ai-assistant-panel]').exists();
await click('[data-test-open-ai-assistant]');
assert.dom('[data-test-ai-assistant-panel]').doesNotExist();
});
});
});
Loading