// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import QtQuick import QtQuick.Controls import ".." // The history panel, over the transcript. Empty search: every conversation, // newest first. Text in the field: the daemon's full-text results, with the // matched words in bold from its snippet markers. Rectangle { id: panel property var canvas color: Qt.alpha(Theme.base, 0.97) radius: 8 onVisibleChanged: if (visible) { field.text = ""; field.forceActiveFocus(); } MouseArea { anchors.fill: parent } // keep clicks off the transcript below TextField { id: field anchors { top: parent.top; left: parent.left; right: parent.right } height: 32 placeholderText: "Search conversations" color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } background: Rectangle { radius: 6 color: Qt.alpha(Theme.surface, 0.9) border.width: 1 border.color: Qt.alpha(Theme.text, 0.15) } onTextChanged: debounce.restart() } // One query per pause in typing, not per key. Timer { id: debounce interval: 150 onTriggered: field.text.trim() === "" ? panel.canvas.send({ type: "list" }) : panel.canvas.send({ type: "search", q: field.text }) } ListView { id: rows anchors { top: field.bottom; topMargin: 8; left: parent.left; right: parent.right; bottom: parent.bottom } clip: true spacing: 4 boundsBehavior: Flickable.StopAtBounds model: field.text.trim() === "" ? panel.canvas.convs : panel.canvas.results delegate: Rectangle { id: r required property var modelData width: rows.width height: col.height + 12 radius: 6 color: hover.hovered ? Qt.alpha(Theme.surface, 0.9) : "transparent" border.width: r.modelData.id === panel.canvas.convId ? 1 : 0 border.color: Qt.alpha(Theme.accent, 0.5) HoverHandler { id: hover } MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { panel.canvas.send({ type: "open", id: r.modelData.id }); panel.canvas.historyOpen = false; } } Column { id: col anchors { left: parent.left; right: del.left; verticalCenter: parent.verticalCenter; margins: 8 } spacing: 2 Text { width: col.width text: r.modelData.title elide: Text.ElideRight color: Theme.text font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } } Text { width: col.width // A list row has its date; a search row its snippet. text: r.modelData.snippet !== undefined // Backticks go: a match inside a code span would // show its ** markers literally. Sanitized like any // model output: a snippet can hold a remote image. ? Notify.sanitize(r.modelData.snippet.replace(/\s+/g, " ").replace(/`/g, "")) : new Date(r.modelData.updated * 1000).toLocaleString(Qt.locale(), Locale.ShortFormat) textFormat: r.modelData.snippet !== undefined ? Text.MarkdownText : Text.PlainText elide: Text.ElideRight maximumLineCount: 1 color: Theme.subtext font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 4 } } } // Two clicks: the first arms it for 3 seconds, as the clipboard's // clear-all does. Button { id: del anchors { right: parent.right; rightMargin: 6; verticalCenter: parent.verticalCenter } text: armed.running ? "Sure?" : "Delete" danger: true onClicked: { if (!armed.running) { armed.restart(); return; } armed.stop(); panel.canvas.send({ type: "delete", id: r.modelData.id }); } Timer { id: armed; interval: 3000 } } } Text { anchors.centerIn: parent visible: rows.count === 0 text: field.text.trim() === "" ? "No conversations yet." : "Nothing found." color: Theme.overlay font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } } } }