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: {
|
Component.onCompleted: {
|
||||||
var log = ollie.readLog()
|
var log = ollie.readLogForSession(ollie.activeSessionId, ollie.activeAgentId)
|
||||||
if (log) {
|
if (log) {
|
||||||
chatModel.appendData(log)
|
chatModel.appendData(log)
|
||||||
chatModel.trimFront(30)
|
chatModel.trimFront(20)
|
||||||
}
|
}
|
||||||
scrollTimer.start()
|
scrollTimer.start()
|
||||||
buildSessionTree()
|
buildSessionTree()
|
||||||
|
|
@ -155,10 +155,10 @@ ApplicationWindow {
|
||||||
target: ollie
|
target: ollie
|
||||||
function onActiveSessionIdChanged() {
|
function onActiveSessionIdChanged() {
|
||||||
chatModel.reset()
|
chatModel.reset()
|
||||||
var log = ollie.readLog()
|
var log = ollie.readLogForSession(ollie.activeSessionId, ollie.activeAgentId)
|
||||||
if (log) {
|
if (log) {
|
||||||
chatModel.appendData(log)
|
chatModel.appendData(log)
|
||||||
chatModel.trimFront(30)
|
chatModel.trimFront(20)
|
||||||
}
|
}
|
||||||
scrollTimer.start()
|
scrollTimer.start()
|
||||||
// Expand the active session in the tree
|
// Expand the active session in the tree
|
||||||
|
|
@ -335,31 +335,32 @@ ApplicationWindow {
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Label { text: "Backend:"; Layout.preferredWidth: 80 }
|
Label { text: "Backend:"; Layout.preferredWidth: 80 }
|
||||||
TextField {
|
ComboBox {
|
||||||
id: backendField
|
id: backendCombo
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
placeholderText: "ollama"
|
enabled: ollie.rootBackendsLoaded
|
||||||
}
|
model: ollie.availableBackends
|
||||||
Button {
|
currentIndex: 0
|
||||||
text: "..."
|
editable: true
|
||||||
onClicked: backendPicker.open()
|
onAccepted: backendCombo.editText = backendCombo.currentText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Model list (populated reactively from backend selection)
|
||||||
|
ListModel { id: modelList }
|
||||||
|
|
||||||
// Model
|
// Model
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Label { text: "Model:"; Layout.preferredWidth: 80 }
|
Label { text: "Model:"; Layout.preferredWidth: 80 }
|
||||||
TextField {
|
ComboBox {
|
||||||
id: modelField
|
id: modelCombo
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
placeholderText: "(auto-populated)"
|
enabled: backendCombo.currentIndex >= 0 && backendCombo.currentText.trim() !== "" && ollie.rootModelsLoaded
|
||||||
enabled: backendField.text.trim() !== ""
|
model: modelList
|
||||||
}
|
currentIndex: -1
|
||||||
Button {
|
editable: true
|
||||||
text: "..."
|
onAccepted: modelCombo.editText = modelCombo.currentText
|
||||||
enabled: backendField.text.trim() !== "" && ollie.rootModelsLoaded
|
|
||||||
onClicked: modelPicker.open()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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: {
|
Component.onCompleted: {
|
||||||
if (!ollie.rootBackendsLoaded) ollie.loadRootBackends()
|
if (!ollie.rootBackendsLoaded) ollie.loadRootBackends()
|
||||||
if (!ollie.rootAgentsLoaded) ollie.loadRootAgents()
|
if (!ollie.rootAgentsLoaded) ollie.loadRootAgents()
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: backendField
|
target: backendCombo
|
||||||
function onTextChanged() {
|
function onCurrentTextChanged() {
|
||||||
var be = backendField.text.trim()
|
var be = backendCombo.currentText.trim()
|
||||||
|
modelList.clear()
|
||||||
if (be !== "" && ollie.rootModelsLoaded) {
|
if (be !== "" && ollie.rootModelsLoaded) {
|
||||||
var models = ollie.getAvailableModels(be)
|
var models = ollie.getAvailableModels(be)
|
||||||
|
for (var i = 0; i < models.length; ++i) {
|
||||||
|
modelList.append({ modelData: models[i] })
|
||||||
|
}
|
||||||
if (models.length > 0) {
|
if (models.length > 0) {
|
||||||
modelField.placeholderText = models[0]
|
modelCombo.currentIndex = 0
|
||||||
} else {
|
} else {
|
||||||
modelField.placeholderText = "(no models)"
|
modelCombo.currentIndex = -1
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
modelField.placeholderText = "(auto-populated)"
|
modelCombo.currentIndex = -1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -463,8 +422,8 @@ ApplicationWindow {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var name = nameField.text.trim()
|
var name = nameField.text.trim()
|
||||||
var backend = backendField.text.trim()
|
var backend = backendCombo.currentText.trim()
|
||||||
var model = modelField.text.trim()
|
var model = modelCombo.currentIndex >= 0 ? modelCombo.currentText.trim() : ""
|
||||||
var agent = agentCombo.currentText
|
var agent = agentCombo.currentText
|
||||||
var remote = remoteField.text.trim()
|
var remote = remoteField.text.trim()
|
||||||
|
|
||||||
|
|
@ -647,102 +606,113 @@ ApplicationWindow {
|
||||||
}
|
}
|
||||||
|
|
||||||
delegate: ColumnLayout {
|
delegate: ColumnLayout {
|
||||||
|
id: blockItem
|
||||||
width: chatView.width - 8
|
width: chatView.width - 8
|
||||||
spacing: 2
|
spacing: 2
|
||||||
|
|
||||||
// Header decoration
|
property bool blockExpanded: {
|
||||||
Rectangle {
|
switch (blockType) {
|
||||||
Layout.fillWidth: true
|
case "user": return true
|
||||||
height: 22
|
case "assistant": return true
|
||||||
radius: 3
|
case "tool": return true
|
||||||
color: {
|
default: return false
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
// Clickable header
|
||||||
|
Rectangle {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.leftMargin: 4
|
height: 22
|
||||||
Layout.rightMargin: 4
|
radius: 3
|
||||||
Layout.bottomMargin: 4
|
color: {
|
||||||
spacing: 0
|
var bg = theme.baseColor
|
||||||
|
if (theme.isDark) {
|
||||||
// Rendered HTML (fenced code/diff) — shown when available
|
switch (blockType) {
|
||||||
TextEdit {
|
case "user": return Qt.lighter(bg, 1.8)
|
||||||
Layout.fillWidth: true
|
case "assistant": return Qt.lighter(bg, 1.5)
|
||||||
visible: blockRenderedHtml && blockRenderedHtml.length > 0
|
case "reasoning": return Qt.lighter(bg, 1.3)
|
||||||
text: blockRenderedHtml
|
case "call": return Qt.lighter(bg, 1.7)
|
||||||
textFormat: Text.RichText
|
case "tool": return Qt.lighter(bg, 1.2)
|
||||||
readOnly: true
|
case "error": return Qt.rgba(0.6, 0.15, 0.15, 0.7)
|
||||||
wrapMode: TextEdit.Wrap
|
case "info": return Qt.lighter(bg, 1.4)
|
||||||
font.family: theme.fontFamily || "monospace"
|
default: return bg
|
||||||
font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12
|
}
|
||||||
color: theme.textColor
|
} 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)
|
MouseArea {
|
||||||
Label {
|
anchors.fill: parent
|
||||||
Layout.fillWidth: true
|
cursorShape: Qt.PointingHandCursor
|
||||||
visible: (!blockRenderedHtml || blockRenderedHtml.length === 0)
|
onClicked: blockItem.blockExpanded = !blockItem.blockExpanded
|
||||||
text: blockContent || ""
|
|
||||||
wrapMode: Text.Wrap
|
|
||||||
textFormat: Text.PlainText
|
|
||||||
font.family: theme.fontFamily || "monospace"
|
|
||||||
font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12
|
|
||||||
color: theme.textColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
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)
|
bool Ollie9pClient::submit(const QString &prompt)
|
||||||
{
|
{
|
||||||
if (m_activeSessionId.isEmpty() || prompt.trimmed().isEmpty()) return false;
|
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;
|
if (!remote.isEmpty()) args << "remote=" + remote;
|
||||||
|
|
||||||
QProcess proc;
|
QProcess proc;
|
||||||
QStringList fullArgs = {"-a", serverAddr(), "write", "new"};
|
QStringList fullArgs = {"-a", serverAddr(), "write", "session/new"};
|
||||||
proc.start(ninepBin(), fullArgs);
|
proc.start(ninepBin(), fullArgs);
|
||||||
proc.waitForStarted(3000);
|
proc.waitForStarted(3000);
|
||||||
|
|
||||||
|
|
@ -280,7 +287,9 @@ void Ollie9pClient::ensureRootDataLoaded()
|
||||||
QString be = line.left(tab).trimmed();
|
QString be = line.left(tab).trimmed();
|
||||||
QString mo = line.mid(tab + 1).trimmed();
|
QString mo = line.mid(tab + 1).trimmed();
|
||||||
if (!be.isEmpty() && !mo.isEmpty()) {
|
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;
|
m_rootModelsLoaded = true;
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ public:
|
||||||
|
|
||||||
Q_INVOKABLE void refreshSessions();
|
Q_INVOKABLE void refreshSessions();
|
||||||
Q_INVOKABLE QString readLog();
|
Q_INVOKABLE QString readLog();
|
||||||
|
Q_INVOKABLE QString readLogForSession(const QString &sessionId, const QString &agentId);
|
||||||
Q_INVOKABLE bool submit(const QString &prompt);
|
Q_INVOKABLE bool submit(const QString &prompt);
|
||||||
Q_INVOKABLE bool interrupt();
|
Q_INVOKABLE bool interrupt();
|
||||||
Q_INVOKABLE bool kill();
|
Q_INVOKABLE bool kill();
|
||||||
|
|
|
||||||
Reference in New Issue