882 lines
32 KiB
QML
882 lines
32 KiB
QML
import QtQuick 2.15
|
|
import QtQuick.Controls 2.15
|
|
import QtQuick.Layouts 1.15
|
|
|
|
ApplicationWindow {
|
|
id: root
|
|
visible: true
|
|
width: 1000
|
|
height: 600
|
|
title: "Ollie"
|
|
font.family: theme.fontFamily || Qt.application.font.family
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
|
|
|
|
// Apply theme palette to all Qt Quick Controls
|
|
palette.active.window: theme.windowColor
|
|
palette.active.windowText: theme.windowTextColor
|
|
palette.active.base: theme.baseColor
|
|
palette.active.text: theme.textColor
|
|
palette.active.button: theme.buttonColor
|
|
palette.active.buttonText: theme.buttonTextColor
|
|
palette.active.highlight: theme.highlightColor
|
|
palette.active.highlightedText: theme.highlightedTextColor
|
|
palette.active.mid: theme.midColor
|
|
palette.active.dark: theme.darkColor
|
|
palette.active.light: theme.lightColor
|
|
palette.active.shadow: theme.shadowColor
|
|
palette.active.link: theme.linkColor
|
|
palette.active.toolTipBase: theme.tooltipBaseColor
|
|
palette.active.toolTipText: theme.tooltipTextColor
|
|
palette.active.placeholderText: theme.placeholderTextColor
|
|
palette.active.alternateBase: theme.alternateBaseColor
|
|
palette.inactive.window: theme.windowColor
|
|
palette.inactive.windowText: theme.windowTextColor
|
|
palette.inactive.base: theme.baseColor
|
|
palette.inactive.text: theme.textColor
|
|
palette.inactive.button: theme.buttonColor
|
|
palette.inactive.buttonText: theme.buttonTextColor
|
|
palette.inactive.highlight: theme.highlightColor
|
|
palette.inactive.highlightedText: theme.highlightedTextColor
|
|
palette.inactive.mid: theme.midColor
|
|
palette.inactive.dark: theme.darkColor
|
|
palette.inactive.light: theme.lightColor
|
|
palette.inactive.shadow: theme.shadowColor
|
|
palette.inactive.link: theme.linkColor
|
|
palette.inactive.toolTipBase: theme.tooltipBaseColor
|
|
palette.inactive.toolTipText: theme.tooltipTextColor
|
|
palette.inactive.placeholderText: theme.placeholderTextColor
|
|
palette.inactive.alternateBase: theme.alternateBaseColor
|
|
palette.disabled.window: theme.windowColor
|
|
palette.disabled.windowText: theme.midColor
|
|
palette.disabled.base: theme.baseColor
|
|
palette.disabled.text: theme.midColor
|
|
palette.disabled.button: theme.buttonColor
|
|
palette.disabled.buttonText: theme.midColor
|
|
palette.disabled.highlight: theme.highlightColor
|
|
palette.disabled.highlightedText: theme.highlightedTextColor
|
|
palette.disabled.mid: theme.midColor
|
|
palette.disabled.dark: theme.darkColor
|
|
palette.disabled.light: theme.lightColor
|
|
palette.disabled.shadow: theme.shadowColor
|
|
|
|
Shortcut {
|
|
sequence: "Escape"
|
|
onActivated: {
|
|
if (renamingIndex >= 0) {
|
|
cancelRename()
|
|
} else if (ollie.activeState !== "idle") {
|
|
ollie.interrupt()
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Slash command definitions ---
|
|
readonly property var slashCommands: [
|
|
{ name: "model", desc: "Set model" },
|
|
{ name: "backend", desc: "Set backend" },
|
|
{ name: "agent", desc: "Switch agent" },
|
|
{ name: "stop", desc: "Stop current run" },
|
|
{ name: "kill", desc: "Kill session" },
|
|
{ name: "new", desc: "New session" },
|
|
{ name: "compact", desc: "Compact context" },
|
|
{ name: "help", desc: "Show help" }
|
|
]
|
|
|
|
// Deferred scroll timer — fires once after model changes settle
|
|
Timer {
|
|
id: scrollTimer
|
|
interval: 0
|
|
repeat: false
|
|
onTriggered: chatView.positionViewAtEnd()
|
|
}
|
|
|
|
// Session tree model — flat list with level info
|
|
ListModel {
|
|
id: sessionTreeModel
|
|
}
|
|
|
|
function buildSessionTree() {
|
|
console.log("buildSessionTree: sessions count = " + ollie.sessions.length)
|
|
sessionTreeModel.clear()
|
|
var sessions = ollie.sessions
|
|
for (var i = 0; i < sessions.length; ++i) {
|
|
var sess = sessions[i]
|
|
var sid = sess.id || ""
|
|
console.log(" session[" + i + "] = " + sid)
|
|
// Auto-expand active session
|
|
var expanded = (sid === ollie.activeSessionId)
|
|
// Add session node
|
|
sessionTreeModel.append({
|
|
type: "session",
|
|
id: sid,
|
|
level: 0,
|
|
expanded: expanded,
|
|
state: sess.state || "",
|
|
model: sess.model || "",
|
|
displayName: sid
|
|
})
|
|
// Fetch agents and add them
|
|
try {
|
|
var agents = ollie.getAgents(sid)
|
|
console.log(" agents for " + sid + ": " + agents.length)
|
|
for (var j = 0; j < agents.length; ++j) {
|
|
var aid = agents[j]
|
|
sessionTreeModel.append({
|
|
type: "agent",
|
|
parentId: sid,
|
|
agentId: aid,
|
|
level: 1,
|
|
state: "idle",
|
|
displayName: "Agent " + aid
|
|
})
|
|
}
|
|
} catch(e) {
|
|
console.log(" error getting agents for " + sid + ": " + e)
|
|
}
|
|
}
|
|
console.log(" total tree items: " + sessionTreeModel.count)
|
|
}
|
|
|
|
function toggleExpand(index) {
|
|
var item = sessionTreeModel.get(index)
|
|
if (item.type === "session") {
|
|
sessionTreeModel.set(index, { expanded: !item.expanded })
|
|
}
|
|
}
|
|
|
|
function activateAgent(parentId, agentId) {
|
|
ollie.switchAgent(parentId, agentId)
|
|
}
|
|
|
|
// Rename tracking
|
|
property int renamingIndex: -1
|
|
property string renamingText: ""
|
|
|
|
function startRename(index) {
|
|
var item = sessionTreeModel.get(index)
|
|
if (item.type === "session") {
|
|
renamingIndex = index
|
|
renamingText = item.displayName
|
|
}
|
|
}
|
|
|
|
function commitRename() {
|
|
if (renamingIndex < 0) return
|
|
var item = sessionTreeModel.get(renamingIndex)
|
|
var newName = renamingText.trim()
|
|
if (newName !== "" && newName !== item.id) {
|
|
if (ollie.renameSession(item.id, newName)) {
|
|
sessionTreeModel.set(renamingIndex, { id: newName, displayName: newName })
|
|
}
|
|
}
|
|
renamingIndex = -1
|
|
renamingText = ""
|
|
}
|
|
|
|
function cancelRename() {
|
|
renamingIndex = -1
|
|
renamingText = ""
|
|
}
|
|
|
|
Connections {
|
|
target: ollie
|
|
function onSessionsChanged() {
|
|
buildSessionTree()
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
var log = ollie.readLogForSession(ollie.activeSessionId, ollie.activeAgentId)
|
|
if (log) {
|
|
chatModel.appendData(log)
|
|
chatModel.trimFront(20)
|
|
}
|
|
scrollTimer.start()
|
|
buildSessionTree()
|
|
}
|
|
|
|
Connections {
|
|
target: ollie
|
|
function onActiveSessionIdChanged() {
|
|
chatModel.reset()
|
|
var log = ollie.readLogForSession(ollie.activeSessionId, ollie.activeAgentId)
|
|
if (log) {
|
|
chatModel.appendData(log)
|
|
chatModel.trimFront(20)
|
|
}
|
|
scrollTimer.start()
|
|
// Expand the active session in the tree
|
|
buildSessionTree()
|
|
}
|
|
function onStreamingDone() {
|
|
chatModel.appendData("\n[info]\nsession ended\n")
|
|
}
|
|
}
|
|
|
|
// --- Settings Dialog ---
|
|
Dialog {
|
|
id: settingsDialog
|
|
title: "Settings"
|
|
anchors.centerIn: parent
|
|
width: 380
|
|
modal: true
|
|
standardButtons: Dialog.Close
|
|
|
|
contentItem: ColumnLayout {
|
|
spacing: 16
|
|
Layout.margins: 16
|
|
|
|
Label {
|
|
text: "Color Scheme"
|
|
font.bold: true
|
|
}
|
|
|
|
Label {
|
|
text: "Uses KDE Breeze color schemes from /usr/share/color-schemes/"
|
|
opacity: 0.6
|
|
wrapMode: Text.Wrap
|
|
Layout.fillWidth: true
|
|
font.pixelSize: Qt.application.font.pixelSize * 0.9
|
|
}
|
|
|
|
ColumnLayout {
|
|
spacing: 8
|
|
|
|
RadioButton {
|
|
id: systemRadio
|
|
text: "Use system theme"
|
|
checked: theme.mode === "system"
|
|
onClicked: theme.mode = "system"
|
|
}
|
|
RadioButton {
|
|
id: lightRadio
|
|
text: "Breeze Light"
|
|
checked: theme.mode === "light"
|
|
onClicked: theme.mode = "light"
|
|
}
|
|
RadioButton {
|
|
id: darkRadio
|
|
text: "Breeze Dark"
|
|
checked: theme.mode === "dark"
|
|
onClicked: theme.mode = "dark"
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: theme.isDark ? "Current: dark" : "Current: light"
|
|
opacity: 0.5
|
|
font.pixelSize: Qt.application.font.pixelSize * 0.85
|
|
}
|
|
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 1
|
|
color: palette.mid
|
|
Layout.topMargin: 8
|
|
}
|
|
|
|
Label {
|
|
text: "Font"
|
|
font.bold: true
|
|
}
|
|
|
|
GridLayout {
|
|
columns: 2
|
|
columnSpacing: 12
|
|
rowSpacing: 8
|
|
Layout.fillWidth: true
|
|
|
|
Label { text: "Family:" }
|
|
ComboBox {
|
|
id: fontFamilyCombo
|
|
Layout.fillWidth: true
|
|
model: theme.availableFamilies
|
|
editable: true
|
|
currentIndex: {
|
|
let fam = theme.fontFamily || Qt.application.font.family
|
|
let idx = model.indexOf(fam)
|
|
return idx >= 0 ? idx : 0
|
|
}
|
|
onActivated: theme.fontFamily = currentText
|
|
onAccepted: theme.fontFamily = editText
|
|
}
|
|
|
|
Label { text: "Style:" }
|
|
ComboBox {
|
|
id: fontStyleCombo
|
|
Layout.fillWidth: true
|
|
model: theme.availableStyles
|
|
currentIndex: {
|
|
let st = theme.fontStyle
|
|
let idx = model.indexOf(st)
|
|
return idx >= 0 ? idx : 0
|
|
}
|
|
onActivated: theme.fontStyle = currentText
|
|
}
|
|
|
|
Label { text: "Size:" }
|
|
SpinBox {
|
|
id: fontSizeSpin
|
|
from: 6
|
|
to: 72
|
|
value: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
|
|
onValueModified: theme.fontSize = value
|
|
}
|
|
}
|
|
|
|
Button {
|
|
text: "Reset to system font"
|
|
onClicked: theme.resetFont()
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- New Session Dialog ---
|
|
Dialog {
|
|
id: newSessionDialog
|
|
title: "New Session"
|
|
anchors.centerIn: parent
|
|
width: 420
|
|
modal: true
|
|
standardButtons: Dialog.Ok | Dialog.Cancel
|
|
|
|
contentItem: ColumnLayout {
|
|
spacing: 16
|
|
Layout.margins: 16
|
|
|
|
// Directory
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Label { text: "Directory:"; Layout.preferredWidth: 80 }
|
|
TextField {
|
|
id: dirField
|
|
Layout.fillWidth: true
|
|
placeholderText: "/path/to/project"
|
|
readOnly: false
|
|
}
|
|
Button {
|
|
text: "Browse..."
|
|
onClicked: {
|
|
var folder = theme.chooseFolder("Select Project Directory", dirField.text || "")
|
|
if (folder !== "")
|
|
dirField.text = folder
|
|
}
|
|
}
|
|
}
|
|
|
|
// Name
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Label { text: "Name:"; Layout.preferredWidth: 80 }
|
|
TextField {
|
|
id: nameField
|
|
Layout.fillWidth: true
|
|
placeholderText: "(auto-generated)"
|
|
}
|
|
}
|
|
|
|
// Backend
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Label { text: "Backend:"; Layout.preferredWidth: 80 }
|
|
ComboBox {
|
|
id: backendCombo
|
|
Layout.fillWidth: true
|
|
enabled: ollie.rootBackendsLoaded
|
|
model: ollie.availableBackends
|
|
currentIndex: 0
|
|
editable: true
|
|
onAccepted: backendCombo.editText = backendCombo.currentText
|
|
}
|
|
}
|
|
|
|
// Model list (populated reactively from backend selection)
|
|
ListModel { id: modelList }
|
|
|
|
// Model
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Label { text: "Model:"; Layout.preferredWidth: 80 }
|
|
ComboBox {
|
|
id: modelCombo
|
|
Layout.fillWidth: true
|
|
enabled: backendCombo.currentIndex >= 0 && backendCombo.currentText.trim() !== "" && ollie.rootModelsLoaded
|
|
model: modelList
|
|
currentIndex: -1
|
|
editable: true
|
|
onAccepted: modelCombo.editText = modelCombo.currentText
|
|
}
|
|
}
|
|
|
|
// Agent
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Label { text: "Agent:"; Layout.preferredWidth: 80 }
|
|
ComboBox {
|
|
id: agentCombo
|
|
Layout.fillWidth: true
|
|
enabled: ollie.rootAgentsLoaded
|
|
model: ollie.availableAgents
|
|
currentIndex: 0
|
|
}
|
|
}
|
|
|
|
// Remote
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Label { text: "Remote:"; Layout.preferredWidth: 80 }
|
|
TextField {
|
|
id: remoteField
|
|
Layout.fillWidth: true
|
|
placeholderText: "user@host:port"
|
|
}
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
if (!ollie.rootBackendsLoaded) ollie.loadRootBackends()
|
|
if (!ollie.rootAgentsLoaded) ollie.loadRootAgents()
|
|
}
|
|
|
|
Connections {
|
|
target: backendCombo
|
|
function onCurrentTextChanged() {
|
|
var be = backendCombo.currentText.trim()
|
|
modelList.clear()
|
|
if (be !== "" && ollie.rootModelsLoaded) {
|
|
var models = ollie.getAvailableModels(be)
|
|
for (var i = 0; i < models.length; ++i) {
|
|
modelList.append({ modelData: models[i] })
|
|
}
|
|
if (models.length > 0) {
|
|
modelCombo.currentIndex = 0
|
|
} else {
|
|
modelCombo.currentIndex = -1
|
|
}
|
|
} else {
|
|
modelCombo.currentIndex = -1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
onAccepted: {
|
|
var cwd = dirField.text.trim()
|
|
if (cwd === "") {
|
|
console.log("New session: directory is required")
|
|
return
|
|
}
|
|
var name = nameField.text.trim()
|
|
var backend = backendCombo.currentText.trim()
|
|
var model = modelCombo.currentIndex >= 0 ? modelCombo.currentText.trim() : ""
|
|
var agent = agentCombo.currentText
|
|
var remote = remoteField.text.trim()
|
|
|
|
var ok = ollie.createSession(cwd, name, backend, model, agent, remote)
|
|
if (ok) {
|
|
ollie.refreshSessions()
|
|
} else {
|
|
console.log("New session: creation failed")
|
|
}
|
|
}
|
|
}
|
|
|
|
SplitView {
|
|
anchors.fill: parent
|
|
|
|
// Left pane: session list
|
|
ColumnLayout {
|
|
SplitView.preferredWidth: root.width * 0.15
|
|
SplitView.minimumWidth: 300
|
|
spacing: 0
|
|
|
|
ToolBar {
|
|
Layout.fillWidth: true
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
Label { text: "Sessions"; Layout.fillWidth: true; font.bold: true; leftPadding: 8 }
|
|
ToolButton {
|
|
text: "\u2795"
|
|
onClicked: newSessionDialog.open()
|
|
ToolTip.text: "New Session"
|
|
ToolTip.visible: hovered
|
|
}
|
|
ToolButton {
|
|
text: "\u21BB"
|
|
onClicked: ollie.refreshSessions()
|
|
}
|
|
ToolButton {
|
|
text: "\u2699"
|
|
onClicked: settingsDialog.open()
|
|
ToolTip.text: "Settings"
|
|
ToolTip.visible: hovered
|
|
}
|
|
}
|
|
}
|
|
|
|
ListView {
|
|
id: sessionList
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
model: sessionTreeModel
|
|
clip: true
|
|
|
|
delegate: ItemDelegate {
|
|
width: sessionList.width
|
|
|
|
// Guard against undefined during initial render
|
|
height: {
|
|
if (!type) return 0
|
|
if (type === "agent" && !parentSessionExpanded(parentId))
|
|
return 0
|
|
return 28
|
|
}
|
|
visible: height > 0
|
|
|
|
// Click disambiguation: timer-based single vs double click
|
|
property int _clickCount: 0
|
|
Timer {
|
|
id: clickTimer
|
|
interval: 300
|
|
repeat: false
|
|
onTriggered: {
|
|
parent._clickCount = 0
|
|
if (type === "session" && index !== renamingIndex) {
|
|
toggleExpand(index)
|
|
} else if (type === "agent") {
|
|
activateAgent(parentId, agentId)
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
acceptedButtons: Qt.LeftButton
|
|
hoverEnabled: false
|
|
onClicked: function() {
|
|
if (renamingIndex >= 0) return
|
|
parent._clickCount++
|
|
if (parent._clickCount === 1) {
|
|
clickTimer.start()
|
|
} else if (parent._clickCount === 2) {
|
|
clickTimer.stop()
|
|
parent._clickCount = 0
|
|
if (type === "session") {
|
|
startRename(index)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
contentItem: RowLayout {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
|
|
// Indent + expand/collapse indicator
|
|
Item {
|
|
Layout.preferredWidth: (level || 0) * 16 + 16
|
|
Layout.preferredHeight: 28
|
|
|
|
Label {
|
|
anchors.centerIn: parent
|
|
text: type === "session" ? (expanded ? "▼" : "▶") : ""
|
|
font.pixelSize: theme.fontSize > 0 ? theme.fontSize * 0.7 : 10
|
|
color: theme.textColor
|
|
opacity: 0.6
|
|
}
|
|
}
|
|
|
|
// Name area
|
|
Item {
|
|
Layout.fillWidth: true
|
|
Layout.preferredHeight: 28
|
|
|
|
TextField {
|
|
anchors.fill: parent
|
|
text: (index === renamingIndex) ? renamingText : ""
|
|
visible: index === renamingIndex
|
|
verticalAlignment: Text.AlignVCenter
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
|
|
color: theme.textColor
|
|
background: Rectangle {
|
|
color: theme.baseColor
|
|
border.color: theme.highlightColor
|
|
radius: 2
|
|
}
|
|
onTextChanged: {
|
|
if (index === renamingIndex) {
|
|
renamingText = text
|
|
}
|
|
}
|
|
Keys.onReturnPressed: commitRename()
|
|
Keys.onEscapePressed: cancelRename()
|
|
onAccepted: commitRename()
|
|
onVisibleChanged: {
|
|
if (visible) {
|
|
forceActiveFocus()
|
|
selectAll()
|
|
}
|
|
}
|
|
}
|
|
|
|
Label {
|
|
anchors.fill: parent
|
|
text: displayName || ""
|
|
color: theme.textColor
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize : Qt.application.font.pointSize
|
|
verticalAlignment: Text.AlignVCenter
|
|
visible: index !== renamingIndex
|
|
}
|
|
}
|
|
|
|
// State dot
|
|
Rectangle {
|
|
Layout.preferredWidth: 8
|
|
Layout.preferredHeight: 8
|
|
Layout.alignment: Qt.AlignVCenter
|
|
radius: 4
|
|
color: {
|
|
var s = state || "idle"
|
|
if (s === "idle") return "#4caf50"
|
|
if (s.startsWith("calling")) return "#ff9800"
|
|
return "#2196f3"
|
|
}
|
|
}
|
|
}
|
|
|
|
highlighted: (type === "agent" && agentId === ollie.activeAgentId && parentId === ollie.activeSessionId)
|
|
|
|
function parentSessionExpanded(pid) {
|
|
for (var i = 0; i < sessionTreeModel.count; ++i) {
|
|
var item = sessionTreeModel.get(i)
|
|
if (item.id === pid && item.type === "session")
|
|
return item.expanded
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Right pane: chat + input
|
|
ColumnLayout {
|
|
spacing: 0
|
|
|
|
// Header
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.margins: 6
|
|
spacing: 8
|
|
|
|
Rectangle {
|
|
width: 10; height: 10; radius: 5
|
|
color: {
|
|
if (ollie.activeState === "idle") return "#4caf50"
|
|
if (ollie.activeState.startsWith("calling")) return "#ff9800"
|
|
return "#2196f3"
|
|
}
|
|
}
|
|
|
|
Label {
|
|
text: ollie.activeState
|
|
opacity: 0.7
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11
|
|
}
|
|
|
|
Item { Layout.fillWidth: true }
|
|
}
|
|
|
|
Rectangle { Layout.fillWidth: true; height: 1; color: theme.midColor }
|
|
|
|
// Chat display — block-based ListView
|
|
ScrollView {
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
clip: true
|
|
|
|
ListView {
|
|
id: chatView
|
|
model: chatModel
|
|
spacing: 4
|
|
anchors.fill: parent
|
|
anchors.margins: 4
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
onCountChanged: {
|
|
if (count > 0)
|
|
scrollTimer.start()
|
|
}
|
|
|
|
Connections {
|
|
target: chatModel
|
|
function onDataChanged() {
|
|
if (chatView.atYEnd && chatView.count > 0)
|
|
scrollTimer.start()
|
|
}
|
|
}
|
|
|
|
delegate: ColumnLayout {
|
|
id: blockItem
|
|
width: chatView.width - 8
|
|
spacing: 2
|
|
|
|
property bool blockExpanded: {
|
|
switch (blockType) {
|
|
case "user": return true
|
|
case "assistant": return true
|
|
case "tool": return true
|
|
case "info": return true
|
|
case "retry": return true
|
|
default: return false
|
|
}
|
|
}
|
|
|
|
// Clickable header
|
|
Rectangle {
|
|
Layout.fillWidth: true
|
|
height: 22
|
|
radius: 3
|
|
color: {
|
|
var bg = theme.baseColor
|
|
if (theme.isDark) {
|
|
switch (blockType) {
|
|
case "user": return Qt.lighter(bg, 1.8)
|
|
case "assistant": return Qt.lighter(bg, 1.5)
|
|
case "reasoning": return Qt.lighter(bg, 1.3)
|
|
case "call": return Qt.lighter(bg, 1.7)
|
|
case "tool": return Qt.lighter(bg, 1.2)
|
|
case "error": return Qt.rgba(0.6, 0.15, 0.15, 0.7)
|
|
case "info": return Qt.lighter(bg, 1.4)
|
|
case "retry": return Qt.rgba(0.6, 0.5, 0.0, 0.5)
|
|
default: return bg
|
|
}
|
|
} else {
|
|
switch (blockType) {
|
|
case "user": return Qt.darker(bg, 1.12)
|
|
case "assistant": return Qt.darker(bg, 1.06)
|
|
case "reasoning": return Qt.darker(bg, 1.03)
|
|
case "call": return Qt.darker(bg, 1.1)
|
|
case "tool": return Qt.darker(bg, 1.04)
|
|
case "error": return Qt.rgba(1.0, 0.8, 0.8, 0.7)
|
|
case "info": return Qt.darker(bg, 1.02)
|
|
case "retry": return Qt.rgba(1.0, 0.9, 0.6, 0.5)
|
|
default: return bg
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: blockItem.blockExpanded = !blockItem.blockExpanded
|
|
}
|
|
|
|
RowLayout {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 6
|
|
spacing: 4
|
|
|
|
Label {
|
|
text: blockItem.blockExpanded ? "▼" : "▶"
|
|
font.pixelSize: 8
|
|
opacity: 0.6
|
|
}
|
|
|
|
Label {
|
|
Layout.fillWidth: true
|
|
text: {
|
|
switch (blockType) {
|
|
case "user": return "User"
|
|
case "assistant": return "Assistant" + (blockName ? " (" + blockName + ")" : "")
|
|
case "reasoning": return "Reasoning"
|
|
case "call": return "Call: " + blockName
|
|
case "tool": return "Tool: " + blockName
|
|
case "error": return "Error"
|
|
case "info": return "Info"
|
|
case "retry": return "Retry"
|
|
default: return ""
|
|
}
|
|
}
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11
|
|
font.bold: true
|
|
color: theme.textColor
|
|
opacity: 0.9
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rendered HTML (fenced code/diff)
|
|
TextEdit {
|
|
Layout.fillWidth: true
|
|
visible: blockItem.blockExpanded && blockRenderedHtml && blockRenderedHtml.length > 0
|
|
text: blockRenderedHtml
|
|
textFormat: Text.RichText
|
|
readOnly: true
|
|
wrapMode: TextEdit.Wrap
|
|
font.family: theme.fontFamily || "monospace"
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12
|
|
color: theme.textColor
|
|
}
|
|
|
|
// Plain text fallback
|
|
Label {
|
|
Layout.fillWidth: true
|
|
visible: blockItem.blockExpanded && (!blockRenderedHtml || blockRenderedHtml.length === 0)
|
|
text: blockContent || ""
|
|
wrapMode: Text.Wrap
|
|
textFormat: Text.PlainText
|
|
font.family: theme.fontFamily || "monospace"
|
|
font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12
|
|
color: theme.textColor
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle { Layout.fillWidth: true; height: 1; color: theme.midColor }
|
|
|
|
// Input
|
|
RowLayout {
|
|
Layout.fillWidth: true
|
|
Layout.margins: 6
|
|
spacing: 4
|
|
|
|
ScrollView {
|
|
id: promptScroll
|
|
Layout.fillWidth: true
|
|
Layout.maximumHeight: 200
|
|
Layout.preferredHeight: Math.min(promptInput.contentHeight + 16, 200)
|
|
clip: true
|
|
|
|
TextArea {
|
|
id: promptInput
|
|
Layout.fillWidth: true
|
|
placeholderText: ollie.activeSessionId ? "Message..." : "No session"
|
|
enabled: ollie.activeSessionId !== ""
|
|
wrapMode: TextEdit.Wrap
|
|
|
|
property string _lastText: ""
|
|
property int _lastCursorPos: 0
|
|
|
|
onTextChanged: {
|
|
_lastText = text
|
|
_lastCursorPos = cursorPosition
|
|
}
|
|
|
|
Keys.onReturnPressed: function(event) {
|
|
if (event.modifiers & Qt.ShiftModifier) {
|
|
event.accepted = false
|
|
} else {
|
|
if (text.trim() !== "") {
|
|
ollie.submit(text.trim())
|
|
text = ""
|
|
}
|
|
event.accepted = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|