add markdown parsing + latex + tree-sitter highlighting for codeblocks in llm responses

This commit is contained in:
2026-08-24 23:55:51 +02:00
parent aae3757daf
commit e18875a752
53 changed files with 5043 additions and 618 deletions
@@ -1,6 +1,8 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import ZShell.Llm
import qs.Components
@@ -14,6 +16,7 @@ MouseArea {
readonly property bool isUser: modelData.role === ChatMessage.Role.User
required property ChatMessage modelData
property bool reasoningExpanded: false
readonly property var blocks: blockify(current.segments)
property real savedContentY: -1
function handleReasoningToggle(expanded: bool): void {
@@ -34,14 +37,36 @@ MouseArea {
}
}
anchors.left: parent.left
anchors.leftMargin: root.isUser ? 0 : Tokens.spacing.extraSmall
anchors.right: parent.right
anchors.rightMargin: root.isUser ? Tokens.spacing.extraSmall : 0
function blockify(segs) {
const blocks = [];
for (let i = 0; i < segs.length; i++) {
const seg = segs[i];
if (seg.type === LlmSegment.Type.Content) {
blocks.push({
kind: "content",
id: i,
segments: [seg]
});
} else {
const last = blocks.length ? blocks[blocks.length - 1] : null;
if (last && last.kind === "process") {
last.segments.push(seg);
} else {
blocks.push({
kind: "process",
id: i,
segments: [seg]
});
}
}
}
return blocks;
}
implicitWidth: ListView.view.width
hoverEnabled: true
// Explicit, synchronous height — sum of children, no Layout polish involved.
implicitHeight: reasoningLoader.implicitHeight + (reasoningLoader.active ? Tokens.spacing.medium : 0) + bubble.implicitHeight + actionsRow.implicitHeight + actionsRow.anchors.topMargin
preventStealing: true
implicitHeight: layout.implicitHeight + Tokens.padding.medium
Binding {
property: "contentY"
@@ -59,106 +84,61 @@ MouseArea {
type: Anim.DefaultEffects
}
Loader {
id: reasoningLoader
Column {
id: layout
readonly property bool shouldBeActive: root.current.reasoningActive || root.current.content === ""
active: !root.isUser
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: Tokens.spacing.extraLarge
anchors.top: parent.top
sourceComponent: Reasoning {
current: root.current
Repeater {
id: segmentRep
onExpandedChanged: {
root.handleReasoningToggle(expanded);
model: ScriptModel {
values: root.blocks
objectProp: "id"
}
}
}
delegate: DelegateChooser {
role: "kind"
CustomRect {
id: bubble
DelegateChoice {
roleValue: "process"
anchors.left: root.isUser ? undefined : parent.left
anchors.right: root.isUser ? parent.right : undefined
anchors.top: reasoningLoader.active ? reasoningLoader.bottom : parent.top
anchors.topMargin: reasoningLoader.active ? Tokens.spacing.medium : 0
color: root.isUser ? Colors.palette.m3primary : Colors.palette.m3surfaceContainer
implicitHeight: root.current.content !== "" ? msgText.contentHeight + Tokens.padding.medium * 2 : 0
implicitWidth: Math.min(msgText.implicitWidth + Tokens.padding.medium * 2, root.width - Tokens.spacing.extraSmall - Tokens.spacing.extraLarge)
radius: Tokens.rounding.medium
visible: !root.current.reasoningActive
delegate: ProcessBlock {
required property int index
required property var modelData
TextEditBase {
id: msgText
segments: modelData.segments
isActive: index === root.blocks.length - 1
width: root.width
anchors.left: parent.left
anchors.margins: Tokens.padding.medium
anchors.right: parent.right
anchors.top: parent.top
animateCursor: false
color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
cursor.color: root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary
font.pointSize: Tokens.font.size.smaller
readOnly: true
selectionColor: Qt.alpha((root.isUser ? Colors.palette.m3onPrimary : Colors.palette.m3primary), 0.4)
text: root.current.content
textFormat: CustomText.MarkdownText
wrapMode: Text.WordWrap
Keys.onPressed: event => {
if (event.key == Qt.Key_Return) {
if (!(event.modifiers & Qt.ShiftModifier)) {
readOnly = true;
event.accepted = true;
onExpandedChanged: root.handleReasoningToggle(expanded)
}
} else if (event.key == Qt.Key_Escape) {
text = root.current.content;
readOnly = true;
event.accepted = true;
}
}
onEditingFinished: {
const old = root.current.content;
if (old !== text) {
root.modelData.edit(text);
if (root.isUser)
root.modelData.generate();
}
readOnly = true;
}
onReadOnlyChanged: {
if (readOnly) {
animateCursor = false;
root.forceActiveFocus();
textFormat = CustomText.MarkdownText;
} else {
var raw = root.current.content;
textFormat = CustomText.PlainText;
text = raw;
forceActiveFocus();
cursorPosition = text.length;
animateCursor = true;
DelegateChoice {
roleValue: "content"
delegate: ContentBubble {
required property var modelData
isUser: root.isUser
segment: modelData.segments[0]
width: root.width
repeater: segmentRep
message: root.modelData
current: root.current
hovered: root.containsMouse
onEdit: text => {
root.modelData.edit(text);
if (root.isUser)
root.modelData.generate();
}
}
}
}
}
}
Actions {
id: actionsRow
anchors.left: parent.left
anchors.right: (implicitWidth > bubble.implicitWidth) ? undefined : bubble.right
anchors.top: bubble.bottom
anchors.topMargin: Tokens.spacing.medium
current: root.current
edit: msgText
hovered: root.containsMouse
message: root.modelData
}
}