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

Add codeblock rendering support using monaco for AI messages #2172

Conversation

habdelra
Copy link
Contributor

@habdelra habdelra commented Feb 19, 2025

This PR adds codeblock rendering support using monaco for AI messages. As part of this there was a lot of cleanup that i had to do for RoomResource and RoomMessageComponent. Specifically there were a couple problems:

  1. The main problem is that every RoomMessageComponent on the screen was re-rendering for every matrix event received, regardless of whether the matrix event had anything to do with the RoomMessageComponent. This was due to the fact that the RoomMessageComponent use an array of Message objects as one of it's args. The thing is that for every access of RoomResource.message the array was rebuilt (it was not cached), moreover, even when it is cached, it's still creating a brand new array for each new message received in a room. We need to stabilize our RoomMessageComponent on the RoomResource so that it is not rebuilding on every single matrix event.
  2. The RoomResource has caches that span rooms. This is fine--as you jump between your rooms it's nice to use cached values. However, the cache was blown away every time you switched rooms, and the way this was done was a bit dubious. The better solution here is to retain the caches of items that span rooms and in the various getters filter out the pertinent items from the rooms, and moreover to use a @cached decorator to prevent the filter from running on every consumption of the property, and only recomputed when the room changes.

I believe that our message scrolling logic is actually dependent on the RoomMessageComponent re-render bug. so very likely we'll need to fix this again. (/cc @FadhlanR)

codeblock

Comment on lines -87 to -89
if (this.isNewRoom(named.roomId)) {
this.resetCache();
}
Copy link
Contributor Author

@habdelra habdelra Feb 19, 2025

Choose a reason for hiding this comment

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

this is not a good strategy. it defeats the purpose of the cache and its a really awkward way to constrain the values returned from this reource to only those that pertain to this room. A better approach is to keep this cache, but then filter out only items that pertain to this room in our various getters, and to make sure those getters use a @cached decorator

Comment on lines 113 to +151
get messages() {
return [...this._messageCache.values()].sort(
(a, b) => a.created.getTime() - b.created.getTime(),
);
if (this.roomId == undefined) {
return [];
}
return [...this._messageCache.values()]
.filter((m) => m.roomId === this.roomId)
.sort((a, b) => a.created.getTime() - b.created.getTime());
}

@cached
get members() {
return Array.from(this._memberCache.values()) ?? [];
if (this.roomId == undefined) {
return [];
}
return (
Array.from(this._memberCache.values()).filter(
(m) => m.roomId === this.roomId,
) ?? []
);
}

@cached
get invitedMembers() {
return this.members.filter((m) => m.membership === 'invite');
if (this.roomId == undefined) {
return [];
}
return this.members.filter(
(m) => m.membership === 'invite' && m.roomId === this.roomId,
);
}

@cached
get joinedMembers() {
return this.members.filter((m) => m.membership === 'join');
if (this.roomId == undefined) {
return [];
}
return this.members.filter(
(m) => m.membership === 'join' && m.roomId === this.roomId,
);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

note that these caches span rooms. that is fine, it makes for quick access when switching between rooms, but it means that we should filter these values by room and use a @cached decorator so we only do this filtering when the room switches--not for every access of the property

Comment on lines +16 to +20
// we pass the code thru using localstorage instead of in the DOM,
// that way we don't have to worry about escaping code. note that the
// DOM wants to render "<template>" strings when we put them in the
// DOM even when wrapped by <pre>. also consider a codeblock that has
// a "</pre>" string in it.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this strategy actually worked out really well. so our AI bot's messages can have any manner of code within them., however we are not using this approach for messages that originate from the user, and those do have code escaping problems. like if a user tries to type <template> in a message, that message will be rendered without the string<template>. you can use your imagination for other things that might be problematic. I suspect that the HTML sanitization may be getting in our way here. users should be able to ask questions about code. filing this as a bug

Copy link
Contributor Author

Choose a reason for hiding this comment

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

filed as CS-8030

'class',
'preview-code monaco-container code-block',
);
monacoContainer.setAttribute('style', `height: ${lines + 4}rem`);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a simple heuristic to get the codeblock to have a variable height based on the content. The monaco div itself does not automaticlaly grow based on the contents within it. (note that there is a min-height/max-height CSS prop on this to make sure this is well behaved)

@habdelra habdelra marked this pull request as ready for review February 19, 2025 19:21
@habdelra habdelra requested a review from a team February 19, 2025 19:21
Copy link

github-actions bot commented Feb 19, 2025

Host Test Results

    1 files  ±0      1 suites  ±0   24m 2s ⏱️ +35s
772 tests +1  770 ✔️ +1  2 💤 ±0  0 ±0 
777 runs  +1  775 ✔️ +1  2 💤 ±0  0 ±0 

Results for commit 67c85db. ± Comparison against base commit 868be56.

♻️ This comment has been updated with latest results.

Copy link
Contributor

@backspace backspace left a comment

Choose a reason for hiding this comment

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

IIRC this is not the first time constant rerendering of room messages has been uncovered, I can’t think of how, but is there some way to detect this wastefulness? 😳

}
// note that since the localstorage item was set from runtime-common there
// was no ember-window-mock available, so we can't use window mock here
let maybeContent = window.localStorage.getItem(id);
Copy link
Contributor

Choose a reason for hiding this comment

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

It’s worrisome to me that this custom lint rule isn’t triggering 😳

.dom('button.code-copy-button')
.exists('the copy code to clipboard button exists');

// assert that new messages don't destabilize the RoomMessage component
Copy link
Contributor

Choose a reason for hiding this comment

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

What actually asserts this?

Copy link
Contributor Author

@habdelra habdelra Feb 20, 2025

Choose a reason for hiding this comment

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

Adding a new event would previously destabilize the RoomMessage component (as described in the PR description). The copy button and Monaco editors are from a modifier that adds DOM. A rerender of this component would blow away the DOM added by their modifier. Meaning that the assertions below this added event would fail.

@habdelra
Copy link
Contributor Author

IIRC this is not the first time constant rerendering of room messages has been uncovered, I can’t think of how, but is there some way to detect this wastefulness? 😳

Yeah I added assertions that will fail (you commented on it actually) if this starts happening again. The CodeBlockModifier adds DOM to the RoomMessage. If it rerenders that added DOM from the modifier will get blown away.

@backspace
Copy link
Contributor

Yeah I added assertions that will fail (you commented on it actually) if this starts happening again. The CodeBlockModifier adds DOM to the RoomMessage. If it rerenders that added DOM from the modifier will get blown away.

ah great, I wasn’t grasping that, thanks for the explanation 🤩

Copy link
Contributor

@jurgenwerk jurgenwerk left a comment

Choose a reason for hiding this comment

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

Nice optimization, and the feature works great, I tested it locally:

image

@jurgenwerk
Copy link
Contributor

jurgenwerk commented Feb 20, 2025

I noticed a visual annoyance when the assistant is streaming file contents to the user. When the code is streamed, it looks like the code editor is continuously rerendering which produces flashing (check the video below). Probably this is out of scope for this PR and we should probably make a separate ticket:

code-flashing.mov

@habdelra
Copy link
Contributor Author

habdelra commented Feb 20, 2025

I noticed a visual annoyance when the assistant is streaming file contents to the user. When the code is streamed, it looks like the code editor is continuously rerendering which produces flashing (check the video below). Probably this is out of scope for this PR and we should probably make a separate ticket:

code-flashing.mov

i think the alternative would be to show it in a <pre> tag until it stops streaming, and once it stops streaming render it in monaco. I think actually there is a natural place to do that in this PR

@habdelra
Copy link
Contributor Author

@jurgenwerk I updated the PR in the latest commit so that we don't render the code block while the ai bot response is streaming.

@habdelra habdelra merged commit a36c5af into main Feb 20, 2025
48 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants