use blobgroup for window chat interface + slim sidebar
C++ / fmt (pull_request) Successful in 4s
C++ / build (pull_request) Failing after 2m1s
C++ / clang-tidy (pull_request) Failing after 1m54s
JS/TS / fmt (pull_request) Failing after 7s
JS/TS / lint (pull_request) Successful in 7s
Python / static (pull_request) Successful in 29s
Rust / build (pull_request) Successful in 47s
Python / verify (pull_request) Successful in 1m35s
Rust / fmt (pull_request) Successful in 29s
Rust / clippy (pull_request) Successful in 51s

This commit is contained in:
2026-09-01 23:14:21 +02:00
parent b254000737
commit ae5c1fa148
6 changed files with 224 additions and 122 deletions
@@ -161,7 +161,7 @@ Item {
scrollAnim.start();
}
cacheBuffer: Math.max(height * 20, 0)
cacheBuffer: Math.max(height * 2, 0)
clip: true
fadeAmount: 0.05
fadeThreshold: Tokens.padding.medium
@@ -11,6 +11,7 @@ CustomClippingRect {
property bool expanded: false
property bool highlighted: false
property bool slim: false
required property ChatSession modelData
signal clicked(content: ChatSession)
@@ -20,6 +21,9 @@ CustomClippingRect {
implicitHeight: {
let h = 0;
if (slim)
return chatIcon.implicitHeight + chatIcon.anchors.topMargin * 2;
h += infoContainer.implicitHeight;
if (expanded)
@@ -85,6 +89,14 @@ CustomClippingRect {
return h;
}
opacity: root.slim ? 0 : 1
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
CustomRect {
id: title
@@ -246,7 +258,7 @@ CustomClippingRect {
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.small
maximumLineCount: 1
opacity: !root.expanded ? 1 : 0
opacity: !root.expanded && !root.slim ? 1 : 0
text: root.modelData.messagesModel.lastMessage?.activeGeneration.content.replace(/\s+/g, " ").trim() ?? qsTr("No messages yet")
Behavior on y {
@@ -347,6 +359,16 @@ CustomClippingRect {
radius: Tokens.rounding.full
color: Colors.layer(Colors.palette.m3surfaceContainerHighest, 3)
opacity: root.slim ? 0 : 1
visible: opacity > 0
enabled: visible
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
StateLayer {
onClicked: root.expanded = !root.expanded
}
@@ -14,6 +14,7 @@ Item {
property alias list: list
property alias model: list.model
property bool highlight: false
property bool slim: false
signal deleteChatRequest(content: ChatSession)
signal loadChatRequest(content: ChatSession, index: int)
@@ -79,6 +80,7 @@ Item {
implicitWidth: ListView.view.width
highlighted: root.highlight && ChatState.chatSession === modelData
slim: root.slim
onHighlightedChanged: if (highlighted)
list.currentIndex = index
@@ -248,6 +250,14 @@ Item {
font.pointSize: Math.round(18 * 1.2)
icon: "add"
isRound: ChatState.fabExpanded
opacity: root.slim ? 0 : 1
visible: opacity > 0
Behavior on opacity {
Anim {
type: Anim.DefaultEffects
}
}
label.transform: Rotation {
origin.y: fabRoot.label.height / 2
@@ -14,6 +14,7 @@ Singleton {
property bool inChat: false
property bool fabExpanded: false
property bool isWindow: false
property bool windowIsWide: false
property ShellScreen screen
property bool sidebarVisible: Visibilities.getForActive().sidebar
@@ -6,127 +6,138 @@ import qs.Services
import qs.Modules.Notifications.Sidebar.Chat
Item {
id: root
id: root
property int animOff
property Item currentItem
property bool loading: false
property int lastIdx: -1
property int animOff
property Item currentItem
property bool noAnim: false
property bool loading: false
property int lastIdx: -1
readonly property Component conversationComp: ChatContent {}
readonly property Component conversationComp: ChatContent {}
function loadConversation(chat): void {
if (currentItem) {
currentItem.destroy();
currentItem = null;
}
signal requestClose
if (!chat)
return;
function loadConversation(chat): void {
if (currentItem) {
currentItem.destroy();
currentItem = null;
}
root.loading = true;
if (!chat)
return;
const incubator = root.conversationComp.incubateObject(container, {
chatData: chat
});
root.loading = true;
const attach = () => {
incubator.object.anchors.fill = container;
currentItem = incubator.object;
root.loading = false;
enterAnim.start();
};
const incubator = root.conversationComp.incubateObject(container, {
chatData: chat
});
if (incubator.status === Component.Ready)
attach();
else
incubator.onStatusChanged = status => {
if (status === Component.Ready)
attach();
};
}
const attach = () => {
incubator.object.anchors.fill = container;
incubator.object.requestClose.connect(root.requestClose);
currentItem = incubator.object;
root.loading = false;
if (!noAnim)
enterAnim.start();
};
Item {
id: container
if (incubator.status === Component.Ready)
attach();
else
incubator.onStatusChanged = status => {
if (status === Component.Ready)
attach();
};
}
anchors.fill: parent
layer.enabled: opacity < 1
objectName: "ConversationContainer"
Item {
id: container
Component.onCompleted: {
if (ChatState.chatSession)
root.loadConversation(ChatState.chatSession);
}
}
anchors.fill: parent
layer.enabled: opacity < 1
objectName: "ConversationContainer"
LoadingIndicator {
anchors.centerIn: parent
implicitSize: Tokens.font.size.extraLarge * 4
opacity: root.loading ? 1 : 0
visible: opacity > 0
Component.onCompleted: {
if (ChatState.chatSession)
root.loadConversation(ChatState.chatSession);
}
}
Behavior on opacity {
Anim {}
}
}
LoadingIndicator {
anchors.centerIn: parent
implicitSize: Tokens.font.size.extraLarge * 4
opacity: root.loading ? 1 : 0
visible: opacity > 0
Connections {
function onChatSessionChanged(): void {
exitAnim.complete();
enterAnim.complete();
root.animOff = Tokens.padding.small * (ChatState.currentIdx > root.lastIdx ? 1 : -1);
root.lastIdx = ChatState.currentIdx;
exitAnim.start();
}
Behavior on opacity {
Anim {}
}
}
target: ChatState
}
Connections {
function onChatSessionChanged(): void {
exitAnim.complete();
enterAnim.complete();
SequentialAnimation {
id: exitAnim
if (root.noAnim) {
root.loadConversation(ChatState.chatSession);
return;
}
Anim {
property: "opacity"
target: container
to: 0
type: Anim.DefaultEffects
}
root.animOff = Tokens.padding.small * (ChatState.currentIdx > root.lastIdx ? 1 : -1);
root.lastIdx = ChatState.currentIdx;
exitAnim.start();
}
ScriptAction {
script: root.loadConversation(ChatState.chatSession)
}
}
target: ChatState
}
SequentialAnimation {
id: enterAnim
SequentialAnimation {
id: exitAnim
PropertyAction {
property: "topMargin"
target: container.anchors
value: root.animOff
}
Anim {
property: "opacity"
target: container
to: 0
type: Anim.DefaultEffects
}
PropertyAction {
property: "bottomMargin"
target: container.anchors
value: -root.animOff
}
ScriptAction {
script: root.loadConversation(ChatState.chatSession)
}
}
ParallelAnimation {
Anim {
from: 0
property: "opacity"
target: container
to: 1
type: Anim.SlowEffects
}
SequentialAnimation {
id: enterAnim
Anim {
properties: "topMargin,bottomMargin"
target: container.anchors
to: 0
type: Anim.SlowEffects
}
}
}
PropertyAction {
property: "topMargin"
target: container.anchors
value: root.animOff
}
PropertyAction {
property: "bottomMargin"
target: container.anchors
value: -root.animOff
}
ParallelAnimation {
Anim {
from: 0
property: "opacity"
target: container
to: 1
type: Anim.SlowEffects
}
Anim {
properties: "topMargin,bottomMargin"
target: container.anchors
to: 0
type: Anim.SlowEffects
}
}
}
}
@@ -4,38 +4,65 @@ import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import ZShell.Blobs
import ZShell.Config
import ZShell.Llm
import qs.Components
import qs.Modules.Notifications.Sidebar.Chat.Content
import qs.Services
RowLayout {
Item {
id: root
property int breakpoint: 700
property int breakpoint: Math.round(Config.sidebar.sizes.width * 2) + Tokens.spacing.medium * 2 + Tokens.padding.medium * 3
property color blobColor: Colors.tPalette.m3surfaceContainerLow
readonly property int iconSize: Math.round(Tokens.font.size.extraLarge * (96 / 72)) + Tokens.padding.large * 2
property alias conversationModel: sidebar.model
property bool chatOpen: ChatState.chatSession !== null
readonly property bool isWide: root.width >= root.breakpoint
property bool narrowShowsSidebar: true
readonly property bool showList: isWide || !chatOpen
readonly property bool showContent: !isWide && chatOpen
function openConversation(conv: ChatSession, index: int): void {
}
BlobGroup {
id: blobGroup
color: root.blobColor
smoothing: Tokens.rounding.medium
}
BlobInvertedRect {
anchors.fill: parent
borderBottom: Tokens.padding.small
borderLeft: sidebar.implicitWidth + sidebar.anchors.margins + Tokens.spacing.medium
borderRight: Tokens.padding.small
borderTop: Tokens.padding.small
group: blobGroup
opacity: root.blobColor.a
radius: Tokens.rounding.largeIncreased
}
ChatList {
id: sidebar
Layout.fillHeight: true
Layout.margins: Tokens.padding.medium
Layout.rightMargin: Tokens.spacing.medium
Layout.maximumWidth: Config.sidebar.sizes.width
Layout.preferredWidth: root.width / 4
Layout.minimumWidth: Config.sidebar.sizes.width / 2
anchors.top: parent.top
anchors.left: parent.left
anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.medium
implicitWidth: root.isWide ? Config.sidebar.sizes.width : root.iconSize
highlight: true
slim: !root.isWide
Behavior on implicitWidth {
Anim {}
Anim {
type: Anim.DefaultEffects
}
}
model: ScriptModel {
values: Chat.chats.values
}
@@ -46,9 +73,11 @@ RowLayout {
}
onDeleteChatRequest: chat => {
if (chat === ChatState.chatSession) {
ChatState.chatSession = null;
ChatState.currentIdx = -1;
}
Chat.chats.remove(chat);
ChatState.currentIdx = -1;
ChatState.chatSession = null;
}
onNewChatRequest: {
const data = Chat.chats.insert();
@@ -58,12 +87,34 @@ RowLayout {
}
Item {
Layout.margins: Tokens.padding.extraLarge
Layout.leftMargin: Tokens.spacing.extraLarge
Layout.topMargin: Tokens.padding.large
Layout.preferredWidth: Config.sidebar.sizes.width * 2
Layout.fillWidth: true
Layout.fillHeight: true
id: contentArea
property int leftMargin: sidebar.implicitWidth
z: 1
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: Tokens.padding.medium * 2
anchors.right: parent.right
implicitWidth: root.width - leftMargin - sidebar.anchors.margins - Tokens.spacing.medium * 2 - anchors.margins
states: State {
name: "wide"
when: !root.isWide
PropertyChanges {
contentArea.leftMargin: root.iconSize
}
}
transitions: Transition {
to: "wide"
Anim {
type: Anim.DefaultEffects
property: "leftMargin"
}
}
ChatHost {
id: convHost
@@ -71,10 +122,11 @@ RowLayout {
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: Math.min(parent.width, 800)
implicitWidth: Math.max(Math.min(parent.width, 800), Config.sidebar.sizes.width)
clip: true
onImplicitWidthChanged: console.log(implicitWidth)
onLoadingChanged: if (!loading)
root.chatOpen = true
}
ColumnLayout {
@@ -102,6 +154,12 @@ RowLayout {
}
}
Binding {
target: ChatState
property: "windowIsWide"
value: root.showList
}
Component {
id: conversationView