gui: replace backend/model text fields with editable ComboBoxes, make chat blocks collapsible
- New session dialog: Backend and Model now use proper ComboBoxes with editable=true so users can type or select from dropdown - Backend picker and model picker dialogs removed (no longer needed) - Model list populated reactively via Connections on backend selection - Fix model caching: m_rootModels[be].toStringList() returns a copy; extract to local QStringList, append, store back into map - Session creation uses correct path 'session/new' instead of 'new' - Chat blocks are now collapsible: user/assistant/tool expanded by default, reasoning/call/error/info/default collapsed - Fix chat history on session switch: readLogForSession() reads from the explicit session/agent path instead of relying on active state - Fix delegate layout: use plain ColumnLayout instead of Item-wrapped ColumnLayout so ListView calculates implicit heights correctly
This commit is contained in:
parent
65dee58a6a
commit
3255522144
290
gui/main.qml
290
gui/main.qml
|
|
@ -142,10 +142,10 @@ ApplicationWindow {
|
|||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
var log = ollie.readLog()
|
||||
var log = ollie.readLogForSession(ollie.activeSessionId, ollie.activeAgentId)
|
||||
if (log) {
|
||||
chatModel.appendData(log)
|
||||
chatModel.trimFront(30)
|
||||
chatModel.trimFront(20)
|
||||
}
|
||||
scrollTimer.start()
|
||||
buildSessionTree()
|
||||
|
|
@ -155,10 +155,10 @@ ApplicationWindow {
|
|||
target: ollie
|
||||
function onActiveSessionIdChanged() {
|
||||
chatModel.reset()
|
||||
var log = ollie.readLog()
|
||||
var log = ollie.readLogForSession(ollie.activeSessionId, ollie.activeAgentId)
|
||||
if (log) {
|
||||
chatModel.appendData(log)
|
||||
chatModel.trimFront(30)
|
||||
chatModel.trimFront(20)
|
||||
}
|
||||
scrollTimer.start()
|
||||
// Expand the active session in the tree
|
||||
|
|
@ -335,31 +335,32 @@ ApplicationWindow {
|
|||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
Label { text: "Backend:"; Layout.preferredWidth: 80 }
|
||||
TextField {
|
||||
id: backendField
|
||||
ComboBox {
|
||||
id: backendCombo
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "ollama"
|
||||
}
|
||||
Button {
|
||||
text: "..."
|
||||
onClicked: backendPicker.open()
|
||||
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 }
|
||||
TextField {
|
||||
id: modelField
|
||||
ComboBox {
|
||||
id: modelCombo
|
||||
Layout.fillWidth: true
|
||||
placeholderText: "(auto-populated)"
|
||||
enabled: backendField.text.trim() !== ""
|
||||
}
|
||||
Button {
|
||||
text: "..."
|
||||
enabled: backendField.text.trim() !== "" && ollie.rootModelsLoaded
|
||||
onClicked: modelPicker.open()
|
||||
enabled: backendCombo.currentIndex >= 0 && backendCombo.currentText.trim() !== "" && ollie.rootModelsLoaded
|
||||
model: modelList
|
||||
currentIndex: -1
|
||||
editable: true
|
||||
onAccepted: modelCombo.editText = modelCombo.currentText
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -387,70 +388,28 @@ ApplicationWindow {
|
|||
}
|
||||
}
|
||||
|
||||
// Backend picker dialog
|
||||
Dialog {
|
||||
id: backendPicker
|
||||
title: "Select Backend"
|
||||
anchors.centerIn: parent
|
||||
modal: true
|
||||
standardButtons: Dialog.Cancel
|
||||
|
||||
contentItem: ListView {
|
||||
width: 300
|
||||
height: 200
|
||||
model: ollie.availableBackends
|
||||
delegate: ItemDelegate {
|
||||
width: parent.width
|
||||
text: modelData
|
||||
onClicked: {
|
||||
backendField.text = modelData
|
||||
backendPicker.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Model picker dialog
|
||||
Dialog {
|
||||
id: modelPicker
|
||||
title: "Select Model"
|
||||
anchors.centerIn: parent
|
||||
modal: true
|
||||
standardButtons: Dialog.Cancel
|
||||
|
||||
contentItem: ListView {
|
||||
width: 400
|
||||
height: 300
|
||||
model: ollie.getAvailableModels(backendField.text.trim())
|
||||
delegate: ItemDelegate {
|
||||
width: parent.width
|
||||
text: modelData
|
||||
onClicked: {
|
||||
modelField.text = modelData
|
||||
modelPicker.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!ollie.rootBackendsLoaded) ollie.loadRootBackends()
|
||||
if (!ollie.rootAgentsLoaded) ollie.loadRootAgents()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: backendField
|
||||
function onTextChanged() {
|
||||
var be = backendField.text.trim()
|
||||
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) {
|
||||
modelField.placeholderText = models[0]
|
||||
modelCombo.currentIndex = 0
|
||||
} else {
|
||||
modelField.placeholderText = "(no models)"
|
||||
modelCombo.currentIndex = -1
|
||||
}
|
||||
} else {
|
||||
modelField.placeholderText = "(auto-populated)"
|
||||
modelCombo.currentIndex = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -463,8 +422,8 @@ ApplicationWindow {
|
|||
return
|
||||
}
|
||||
var name = nameField.text.trim()
|
||||
var backend = backendField.text.trim()
|
||||
var model = modelField.text.trim()
|
||||
var backend = backendCombo.currentText.trim()
|
||||
var model = modelCombo.currentIndex >= 0 ? modelCombo.currentText.trim() : ""
|
||||
var agent = agentCombo.currentText
|
||||
var remote = remoteField.text.trim()
|
||||
|
||||
|
|
@ -647,102 +606,113 @@ ApplicationWindow {
|
|||
}
|
||||
|
||||
delegate: ColumnLayout {
|
||||
id: blockItem
|
||||
width: chatView.width - 8
|
||||
spacing: 2
|
||||
|
||||
// Header decoration
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
height: 22
|
||||
radius: 3
|
||||
color: {
|
||||
// Derive header colors from base background with subtle tint.
|
||||
// Always readable because text uses theme.textColor.
|
||||
var bg = theme.baseColor
|
||||
if (theme.isDark) {
|
||||
// Dark theme: lighter tints against dark background
|
||||
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)
|
||||
default: return bg
|
||||
}
|
||||
} else {
|
||||
// Light theme: slightly darker tints
|
||||
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)
|
||||
default: return bg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 6
|
||||
spacing: 4
|
||||
|
||||
Label {
|
||||
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"
|
||||
default: return ""
|
||||
}
|
||||
}
|
||||
font.pointSize: theme.fontSize > 0 ? theme.fontSize * 0.85 : 11
|
||||
font.bold: true
|
||||
color: theme.textColor
|
||||
opacity: 0.9
|
||||
}
|
||||
property bool blockExpanded: {
|
||||
switch (blockType) {
|
||||
case "user": return true
|
||||
case "assistant": return true
|
||||
case "tool": return true
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
// Clickable header
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 4
|
||||
Layout.rightMargin: 4
|
||||
Layout.bottomMargin: 4
|
||||
spacing: 0
|
||||
|
||||
// Rendered HTML (fenced code/diff) — shown when available
|
||||
TextEdit {
|
||||
Layout.fillWidth: true
|
||||
visible: 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
|
||||
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)
|
||||
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)
|
||||
default: return bg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Plain text fallback (prose, streaming content)
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
visible: (!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
|
||||
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"
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -110,6 +110,13 @@ QString Ollie9pClient::readLog()
|
|||
return QString::fromUtf8(out);
|
||||
}
|
||||
|
||||
QString Ollie9pClient::readLogForSession(const QString &sessionId, const QString &agentId)
|
||||
{
|
||||
if (sessionId.isEmpty() || agentId.isEmpty()) return {};
|
||||
QByteArray out = run9p({"read", "session/" + sessionId + "/agent/" + agentId + "/log"});
|
||||
return QString::fromUtf8(out);
|
||||
}
|
||||
|
||||
bool Ollie9pClient::submit(const QString &prompt)
|
||||
{
|
||||
if (m_activeSessionId.isEmpty() || prompt.trimmed().isEmpty()) return false;
|
||||
|
|
@ -244,7 +251,7 @@ bool Ollie9pClient::createSession(const QString &cwd, const QString &name, const
|
|||
if (!remote.isEmpty()) args << "remote=" + remote;
|
||||
|
||||
QProcess proc;
|
||||
QStringList fullArgs = {"-a", serverAddr(), "write", "new"};
|
||||
QStringList fullArgs = {"-a", serverAddr(), "write", "session/new"};
|
||||
proc.start(ninepBin(), fullArgs);
|
||||
proc.waitForStarted(3000);
|
||||
|
||||
|
|
@ -280,7 +287,9 @@ void Ollie9pClient::ensureRootDataLoaded()
|
|||
QString be = line.left(tab).trimmed();
|
||||
QString mo = line.mid(tab + 1).trimmed();
|
||||
if (!be.isEmpty() && !mo.isEmpty()) {
|
||||
m_rootModels[be].toStringList().append(mo);
|
||||
QStringList list = m_rootModels[be].toStringList();
|
||||
list.append(mo);
|
||||
m_rootModels[be] = QVariant(list);
|
||||
}
|
||||
}
|
||||
m_rootModelsLoaded = true;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ public:
|
|||
|
||||
Q_INVOKABLE void refreshSessions();
|
||||
Q_INVOKABLE QString readLog();
|
||||
Q_INVOKABLE QString readLogForSession(const QString &sessionId, const QString &agentId);
|
||||
Q_INVOKABLE bool submit(const QString &prompt);
|
||||
Q_INVOKABLE bool interrupt();
|
||||
Q_INVOKABLE bool kill();
|
||||
|
|
|
|||
Reference in New Issue