import QtQuick import QtQuick.Controls import QtQuick.Layouts import QtQuick.Window import Quickshell import Quickshell.Io import qs.config import qs.modules.functions import qs.modules.components import qs.services FloatingWindow { id: appWin color: Appearance.m3colors.m3background property bool initialChatSelected: false property bool chatsInitialized: false function appendMessage(sender, message) { messageModel.append({ "sender": sender, "message": message }); scrollToBottom(); } function updateChatsList(files) { let existing = { }; for (let i = 0; i < chatListModel.count; i++) existing[chatListModel.get(i).name] = true for (let file of files) { let name = file.trim(); if (!name.length) continue; if (name.endsWith(".txt")) name = name.slice(0, -4); if (!existing[name]) chatListModel.append({ "name": name }); delete existing[name]; } // remove chats that no longer exist for (let name in existing) { for (let i = 0; i < chatListModel.count; i++) { if (chatListModel.get(i).name === name) { chatListModel.remove(i); break; } } } // ensure default exists let hasDefault = false; for (let i = 0; i < chatListModel.count; i++) if (chatListModel.get(i).name === "default") { hasDefault = true; } if (!hasDefault) { chatListModel.insert(0, { "name": "default" }); FileUtils.createFile(FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/default.txt"); } } function scrollToBottom() { // Always scroll to end after appending chatView.forceLayout(); chatView.positionViewAtEnd(); } function sendMessage() { if (userInput.text === "" || Zenith.loading) return ; Zenith.pendingInput = userInput.text; appendMessage("You", userInput.text); userInput.text = ""; Zenith.loading = true; Zenith.send(); } function loadChatHistory(chatName) { messageModel.clear(); Zenith.loadChat(chatName); } function selectDefaultChat() { let defaultIndex = -1; for (let i = 0; i < chatListModel.count; i++) { if (chatListModel.get(i).name === "default") { defaultIndex = i; break; } } if (defaultIndex !== -1) { chatSelector.currentIndex = defaultIndex; Zenith.currentChat = "default"; loadChatHistory("default"); } else if (chatListModel.count > 0) { chatSelector.currentIndex = 0; Zenith.currentChat = chatListModel.get(0).name; loadChatHistory(Zenith.currentChat); } } visible: Globals.states.intelligenceWindowOpen onVisibleChanged: { if (!visible) return ; chatsInitialized = false; messageModel.clear(); } IpcHandler { function openWindow() { Globals.states.intelligenceWindowOpen = true; } function closeWindow() { Globals.states.intelligenceWindowOpen = false; } target: "intelligence" } ListModel { // { sender: "You" | "AI", message: string } id: messageModel } ListModel { id: chatListModel } ColumnLayout { spacing: Metrics.spacing(8) anchors.centerIn: parent StyledText { visible: !Config.runtime.misc.intelligence.enabled text: "Intelligence is disabled!" Layout.leftMargin: Metrics.margin(24) font.pixelSize: Metrics.fontSize("huge") } StyledText { visible: !Config.runtime.misc.intelligence.enabled text: "Go to the settings to enable intelligence" } } StyledRect { anchors.fill: parent color: "transparent" visible: Config.runtime.misc.intelligence.enabled ColumnLayout { anchors.fill: parent anchors.margins: Metrics.margin(16) spacing: Metrics.spacing(10) RowLayout { Layout.fillWidth: true spacing: Metrics.spacing(10) StyledDropDown { id: chatSelector Layout.fillWidth: true model: chatListModel textRole: "name" Layout.preferredHeight: 40 onCurrentIndexChanged: { if (currentIndex < 0) return ; let name = chatListModel.get(currentIndex).name; if (name === Zenith.currentChat) return ; Zenith.currentChat = name; loadChatHistory(name); } } StyledButton { icon: "add" Layout.preferredWidth: 40 onClicked: { let name = "new-chat-" + chatListModel.count; let path = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + name + ".txt"; FileUtils.createFile(path, function(success) { if (success) { chatListModel.append({ "name": name }); chatSelector.currentIndex = chatListModel.count - 1; Zenith.currentChat = name; messageModel.clear(); } }); } } StyledButton { icon: "edit" Layout.preferredWidth: 40 enabled: chatSelector.currentIndex >= 0 onClicked: renameDialog.open() } StyledButton { icon: "delete" Layout.preferredWidth: 40 enabled: chatSelector.currentIndex >= 0 && chatSelector.currentText !== "default" onClicked: { let name = chatSelector.currentText; let path = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + name + ".txt"; FileUtils.removeFile(path, function(success) { if (success) { chatListModel.remove(chatSelector.currentIndex); selectDefaultChat(); } }); } } } RowLayout { Layout.fillWidth: true spacing: Metrics.spacing(10) StyledDropDown { id: modelSelector Layout.fillWidth: true model: ["openai/gpt-4o","openai/gpt-4","openai/gpt-3.5-turbo","openai/gpt-4o-mini","anthropic/claude-3.5-sonnet","anthropic/claude-3-haiku","meta-llama/llama-3.3-70b-instruct:free","deepseek/deepseek-r1-0528:free","qwen/qwen3-coder:free"] currentIndex: 0 Layout.preferredHeight: 40 onCurrentTextChanged: Zenith.currentModel = currentText } StyledButton { icon: "close_fullscreen" Layout.preferredWidth: 40 onClicked: { Quickshell.execDetached(["nucleus", "ipc", "call", "intelligence", "closeWindow"]); Globals.visiblility.sidebarLeft = false; } } } StyledRect { Layout.fillWidth: true Layout.fillHeight: true radius: Metrics.radius("normal") color: Appearance.m3colors.m3surfaceContainerLow ScrollView { anchors.fill: parent clip: true ListView { id: chatView model: messageModel spacing: Metrics.spacing(8) anchors.fill: parent anchors.margins: Metrics.margin(12) clip: true delegate: Item { property bool isCodeBlock: message.split("\n").length > 2 && message.includes("import ") // simple heuristic width: chatView.width height: bubble.implicitHeight + 6 Component.onCompleted: { chatView.forceLayout(); } Row { width: parent.width spacing: Metrics.spacing(8) Item { width: sender === "AI" ? 0 : parent.width * 0.2 } StyledRect { id: bubble radius: Metrics.radius("normal") color: sender === "You" ? Appearance.m3colors.m3primaryContainer : Appearance.m3colors.m3surfaceContainerHigh implicitWidth: Math.min(textItem.implicitWidth + 20, chatView.width * 0.8) implicitHeight: textItem.implicitHeight anchors.right: sender === "You" ? parent.right : undefined anchors.left: sender === "AI" ? parent.left : undefined anchors.topMargin: Metrics.margin(2) TextEdit { id: textItem text: StringUtils.markdownToHtml(message) wrapMode: TextEdit.Wrap textFormat: TextEdit.RichText readOnly: true // make it selectable but not editable font.pixelSize: Metrics.fontSize(16) color: Appearance.syntaxHighlightingTheme padding: Metrics.padding(8) anchors.fill: parent } MouseArea { id: ma anchors.fill: parent acceptedButtons: Qt.RightButton onClicked: { let p = Qt.createQmlObject('import Quickshell; import Quickshell.Io; Process { command: ["wl-copy", "' + message + '"] }', parent); p.running = true; } } } Item { width: sender === "You" ? 0 : parent.width * 0.2 } } } } } } StyledRect { Layout.fillWidth: true height: 50 radius: Metrics.radius("normal") color: Appearance.m3colors.m3surfaceContainer RowLayout { anchors.fill: parent anchors.margins: Metrics.margin(6) spacing: Metrics.spacing(10) StyledTextField { // Shift+Enter → insert newline // Enter → send message id: userInput Layout.fillWidth: true placeholderText: "Type your message..." font.pixelSize: Metrics.iconSize(14) padding: Metrics.spacing(8) Keys.onPressed: { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { if (event.modifiers & Qt.ShiftModifier) insert("\n"); else sendMessage(); event.accepted = true; } } } StyledButton { text: "Send" enabled: userInput.text.trim().length > 0 && !Zenith.loading opacity: enabled ? 1 : 0.5 onClicked: sendMessage() } } } } Dialog { id: renameDialog title: "Rename Chat" modal: true visible: false standardButtons: Dialog.NoButton x: (appWin.width - 360) / 2 // center horizontally y: (appWin.height - 160) / 2 // center vertically width: 360 height: 200 ColumnLayout { anchors.fill: parent anchors.margins: Metrics.margin(16) spacing: Metrics.spacing(12) StyledText { text: "Enter a new name for the chat" font.pixelSize: Metrics.fontSize(18) horizontalAlignment: Text.AlignHCenter Layout.fillWidth: true } StyledTextField { id: renameInput Layout.fillWidth: true placeholderText: "New name" filled: false highlight: false text: chatSelector.currentText font.pixelSize: Metrics.fontSize(16) Layout.preferredHeight: 45 padding: Metrics.padding(8) } RowLayout { Layout.fillWidth: true spacing: Metrics.spacing(12) Layout.alignment: Qt.AlignRight StyledButton { text: "Cancel" Layout.preferredWidth: 80 onClicked: renameDialog.close() } StyledButton { text: "Rename" Layout.preferredWidth: 100 enabled: renameInput.text.trim().length > 0 && renameInput.text !== chatSelector.currentText onClicked: { let oldName = chatSelector.currentText; let newName = renameInput.text.trim(); let oldPath = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + oldName + ".txt"; let newPath = FileUtils.trimFileProtocol(Directories.config) + "/zenith/chats/" + newName + ".txt"; FileUtils.renameFile(oldPath, newPath, function(success) { if (success) { chatListModel.set(chatSelector.currentIndex, { "name": newName }); Zenith.currentChat = newName; renameDialog.close(); } }); } } } } background: StyledRect { color: Appearance.m3colors.m3surfaceContainer radius: Metrics.radius("normal") border.color: Appearance.colors.colOutline border.width: 1 } header: StyledRect { color: Appearance.m3colors.m3surfaceContainer radius: Metrics.radius("normal") border.color: Appearance.colors.colOutline border.width: 1 } } StyledText { text: "Thinking…" visible: Zenith.loading color: Appearance.colors.colSubtext font.pixelSize: Metrics.fontSize(14) anchors { left: parent.left bottom: parent.bottom leftMargin: Metrics.margin(22) bottomMargin: Metrics.margin(76) } } } Connections { // only auto-select once function onChatsListed(text) { let lines = text.split(/\r?\n/); let previousChat = Zenith.currentChat; updateChatsList(lines); // select & load once if (!chatsInitialized) { chatsInitialized = true; let index = -1; for (let i = 0; i < chatListModel.count; i++) { if (chatListModel.get(i).name === previousChat) { index = i; break; } } if (index === -1 && chatListModel.count > 0) index = 0; if (index !== -1) { chatSelector.currentIndex = index; Zenith.currentChat = chatListModel.get(index).name; loadChatHistory(Zenith.currentChat); } return ; } // AFTER init: only react if current chat vanished let stillExists = false; for (let i = 0; i < chatListModel.count; i++) { if (chatListModel.get(i).name === Zenith.currentChat) { stillExists = true; break; } } if (!stillExists && chatListModel.count > 0) { chatSelector.currentIndex = 0; Zenith.currentChat = chatListModel.get(0).name; loadChatHistory(Zenith.currentChat); } } function onAiReply(text) { appendMessage("AI", text.slice(5)); Zenith.loading = false; } function onChatLoaded(text) { let lines = text.split(/\r?\n/); let batch = []; for (let l of lines) { let line = l.trim(); if (!line.length) continue; let u = line.match(/^\[\d{4}-.*\] User: (.*)$/); let a = line.match(/^\[\d{4}-.*\] AI: (.*)$/); if (u) batch.push({ "sender": "You", "message": u[1] }); else if (a) batch.push({ "sender": "AI", "message": a[1] }); else if (batch.length) batch[batch.length - 1].message += "\n" + line; } messageModel.clear(); for (let m of batch) messageModel.append(m) scrollToBottom(); } target: Zenith } }