gui: new session dialog with directory, backend, model, agent, remote fields
- Add C++ methods: loadRootBackends(), loadRootAgents(), getAvailableModels(), createSession() - Add Q_PROPERTYs for root data state (backends, agents, models) - Add '+' button to sessions toolbar - Create New Session Dialog with: - Directory field + Browse button (FolderDialog) - Name field (optional, auto-generated if empty) - Backend combo (populated from /backends) - Model combo (filtered by selected backend) - Agent combo (populated from /agents) - Remote field (user@host:port) - Root data loaded lazily on startup - OK creates session via 'write new' 9P call and refreshes list
This commit is contained in:
parent
69458631dc
commit
774c3d4d77
133
gui/main.qml
133
gui/main.qml
|
|
@ -1,6 +1,7 @@
|
||||||
import QtQuick 2.15
|
import QtQuick 2.15
|
||||||
import QtQuick.Controls 2.15
|
import QtQuick.Controls 2.15
|
||||||
import QtQuick.Layouts 1.15
|
import QtQuick.Layouts 1.15
|
||||||
|
import QtQuick.Dialogs 1.15
|
||||||
|
|
||||||
ApplicationWindow {
|
ApplicationWindow {
|
||||||
id: root
|
id: root
|
||||||
|
|
@ -278,6 +279,132 @@ ApplicationWindow {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- 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: folderDialog.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Label { text: "Model:"; Layout.preferredWidth: 80 }
|
||||||
|
ComboBox {
|
||||||
|
id: modelCombo
|
||||||
|
Layout.fillWidth: true
|
||||||
|
enabled: ollie.rootModelsLoaded && backendCombo.count > 0
|
||||||
|
model: ollie.getAvailableModels(backendCombo.currentText)
|
||||||
|
currentIndex: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
var model = modelCombo.currentText
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FolderDialog {
|
||||||
|
id: folderDialog
|
||||||
|
title: "Select Project Directory"
|
||||||
|
currentFolder: StandardPaths.locate(StandardPaths.HomeLocation, "", StandardPaths.LocateFile)
|
||||||
|
onAccepted: {
|
||||||
|
dirField.text = folderDialog.selectedFolder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
SplitView {
|
SplitView {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
|
|
||||||
|
|
@ -292,6 +419,12 @@ ApplicationWindow {
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
Label { text: "Sessions"; Layout.fillWidth: true; font.bold: true; leftPadding: 8 }
|
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 {
|
ToolButton {
|
||||||
text: "\u21BB"
|
text: "\u21BB"
|
||||||
onClicked: ollie.refreshSessions()
|
onClicked: ollie.refreshSessions()
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
|
#include <QDebug>
|
||||||
|
|
||||||
static QString ninepBin()
|
static QString ninepBin()
|
||||||
{
|
{
|
||||||
|
|
@ -30,6 +31,7 @@ Ollie9pClient::Ollie9pClient(QObject *parent)
|
||||||
: QObject(parent)
|
: QObject(parent)
|
||||||
{
|
{
|
||||||
refreshSessions();
|
refreshSessions();
|
||||||
|
ensureRootDataLoaded();
|
||||||
}
|
}
|
||||||
|
|
||||||
Ollie9pClient::~Ollie9pClient()
|
Ollie9pClient::~Ollie9pClient()
|
||||||
|
|
@ -191,6 +193,102 @@ void Ollie9pClient::switchAgent(const QString &sessionId, const QString &agentId
|
||||||
startStateStream();
|
startStateStream();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Ollie9pClient::loadRootBackends()
|
||||||
|
{
|
||||||
|
if (m_rootBackendsLoaded) return;
|
||||||
|
QByteArray out = run9p({"read", "backends"});
|
||||||
|
QString raw = QString::fromUtf8(out).trimmed();
|
||||||
|
if (!raw.isEmpty()) {
|
||||||
|
m_availableBackends = raw.split('\n', Qt::SkipEmptyParts);
|
||||||
|
m_rootBackendsLoaded = true;
|
||||||
|
emit availableBackendsChanged();
|
||||||
|
}
|
||||||
|
emit rootBackendsLoadedChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Ollie9pClient::loadRootAgents()
|
||||||
|
{
|
||||||
|
if (m_rootAgentsLoaded) return;
|
||||||
|
QByteArray out = run9p({"read", "agents"});
|
||||||
|
QString raw = QString::fromUtf8(out).trimmed();
|
||||||
|
if (!raw.isEmpty()) {
|
||||||
|
m_availableAgents = raw.split('\n', Qt::SkipEmptyParts);
|
||||||
|
m_rootAgentsLoaded = true;
|
||||||
|
emit availableAgentsChanged();
|
||||||
|
} else {
|
||||||
|
m_availableAgents = {"default"};
|
||||||
|
m_rootAgentsLoaded = true;
|
||||||
|
emit availableAgentsChanged();
|
||||||
|
}
|
||||||
|
emit rootAgentsLoadedChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
QStringList Ollie9pClient::getAvailableModels(const QString &backend) const
|
||||||
|
{
|
||||||
|
if (!m_rootModelsLoaded) return {};
|
||||||
|
auto it = m_rootModels.find(backend);
|
||||||
|
if (it == m_rootModels.end()) return {};
|
||||||
|
return it.value().toStringList();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Ollie9pClient::createSession(const QString &cwd, const QString &name, const QString &backend, const QString &model, const QString &agent, const QString &remote)
|
||||||
|
{
|
||||||
|
if (cwd.isEmpty()) return false;
|
||||||
|
|
||||||
|
QStringList args;
|
||||||
|
args << "cwd=" + cwd;
|
||||||
|
if (!name.isEmpty()) args << "name=" + name;
|
||||||
|
if (!backend.isEmpty()) args << "backend=" + backend;
|
||||||
|
if (!model.isEmpty()) args << "model=" + model;
|
||||||
|
if (!agent.isEmpty()) args << "agent=" + agent;
|
||||||
|
if (!remote.isEmpty()) args << "remote=" + remote;
|
||||||
|
|
||||||
|
QProcess proc;
|
||||||
|
QStringList fullArgs = {"-a", serverAddr(), "write", "new"};
|
||||||
|
proc.start(ninepBin(), fullArgs);
|
||||||
|
proc.waitForStarted(3000);
|
||||||
|
|
||||||
|
QString input = args.join(" ");
|
||||||
|
proc.write(input.toUtf8());
|
||||||
|
proc.closeWriteChannel();
|
||||||
|
proc.waitForFinished(5000);
|
||||||
|
|
||||||
|
if (proc.exitCode() != 0) {
|
||||||
|
QString err = QString::fromUtf8(proc.readAllStandardError()).trimmed();
|
||||||
|
if (err.isEmpty())
|
||||||
|
err = QString::fromUtf8(proc.readAllStandardOutput()).trimmed();
|
||||||
|
qDebug() << "createSession failed:" << err;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Root data loading (lazy) ---
|
||||||
|
|
||||||
|
void Ollie9pClient::ensureRootDataLoaded()
|
||||||
|
{
|
||||||
|
if (!m_rootBackendsLoaded) loadRootBackends();
|
||||||
|
if (!m_rootAgentsLoaded) loadRootAgents();
|
||||||
|
if (!m_rootModelsLoaded) {
|
||||||
|
QByteArray out = run9p({"read", "models"});
|
||||||
|
QString raw = QString::fromUtf8(out);
|
||||||
|
if (!raw.isEmpty()) {
|
||||||
|
for (const QString &line : raw.split('\n')) {
|
||||||
|
int tab = line.indexOf('\t');
|
||||||
|
if (tab < 0) continue;
|
||||||
|
QString be = line.left(tab).trimmed();
|
||||||
|
QString mo = line.mid(tab + 1).trimmed();
|
||||||
|
if (!be.isEmpty() && !mo.isEmpty()) {
|
||||||
|
m_rootModels[be].toStringList().append(mo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_rootModelsLoaded = true;
|
||||||
|
emit rootModelsLoadedChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Streaming ---
|
// --- Streaming ---
|
||||||
|
|
||||||
void Ollie9pClient::startChatStream()
|
void Ollie9pClient::startChatStream()
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,11 @@ class Ollie9pClient : public QObject
|
||||||
Q_PROPERTY(QString activeSessionId READ activeSessionId WRITE setActiveSessionId NOTIFY activeSessionIdChanged)
|
Q_PROPERTY(QString activeSessionId READ activeSessionId WRITE setActiveSessionId NOTIFY activeSessionIdChanged)
|
||||||
Q_PROPERTY(QString activeAgentId READ activeAgentId NOTIFY activeAgentIdChanged)
|
Q_PROPERTY(QString activeAgentId READ activeAgentId NOTIFY activeAgentIdChanged)
|
||||||
Q_PROPERTY(QString activeState READ activeState NOTIFY activeStateChanged)
|
Q_PROPERTY(QString activeState READ activeState NOTIFY activeStateChanged)
|
||||||
|
Q_PROPERTY(bool rootBackendsLoaded READ rootBackendsLoaded NOTIFY rootBackendsLoadedChanged)
|
||||||
|
Q_PROPERTY(QStringList availableBackends READ availableBackends NOTIFY availableBackendsChanged)
|
||||||
|
Q_PROPERTY(bool rootAgentsLoaded READ rootAgentsLoaded NOTIFY rootAgentsLoadedChanged)
|
||||||
|
Q_PROPERTY(QStringList availableAgents READ availableAgents NOTIFY availableAgentsChanged)
|
||||||
|
Q_PROPERTY(bool rootModelsLoaded READ rootModelsLoaded NOTIFY rootModelsLoadedChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
explicit Ollie9pClient(QObject *parent = nullptr);
|
explicit Ollie9pClient(QObject *parent = nullptr);
|
||||||
|
|
@ -33,6 +38,12 @@ public:
|
||||||
QString activeAgentId() const { return m_agentId; }
|
QString activeAgentId() const { return m_agentId; }
|
||||||
QString activeState() const { return m_activeState; }
|
QString activeState() const { return m_activeState; }
|
||||||
|
|
||||||
|
bool rootBackendsLoaded() const { return m_rootBackendsLoaded; }
|
||||||
|
QStringList availableBackends() const { return m_availableBackends; }
|
||||||
|
bool rootAgentsLoaded() const { return m_rootAgentsLoaded; }
|
||||||
|
QStringList availableAgents() const { return m_availableAgents; }
|
||||||
|
bool rootModelsLoaded() const { return m_rootModelsLoaded; }
|
||||||
|
|
||||||
Q_INVOKABLE void refreshSessions();
|
Q_INVOKABLE void refreshSessions();
|
||||||
Q_INVOKABLE QString readLog();
|
Q_INVOKABLE QString readLog();
|
||||||
Q_INVOKABLE bool submit(const QString &prompt);
|
Q_INVOKABLE bool submit(const QString &prompt);
|
||||||
|
|
@ -42,6 +53,10 @@ public:
|
||||||
Q_INVOKABLE QStringList getAgents(const QString &sessionId);
|
Q_INVOKABLE QStringList getAgents(const QString &sessionId);
|
||||||
Q_INVOKABLE void setActiveAgentId(const QString &agentId);
|
Q_INVOKABLE void setActiveAgentId(const QString &agentId);
|
||||||
Q_INVOKABLE void switchAgent(const QString &sessionId, const QString &agentId);
|
Q_INVOKABLE void switchAgent(const QString &sessionId, const QString &agentId);
|
||||||
|
Q_INVOKABLE void loadRootBackends();
|
||||||
|
Q_INVOKABLE void loadRootAgents();
|
||||||
|
Q_INVOKABLE QStringList getAvailableModels(const QString &backend) const;
|
||||||
|
Q_INVOKABLE bool createSession(const QString &cwd, const QString &name, const QString &backend, const QString &model, const QString &agent, const QString &remote);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void sessionsChanged();
|
void sessionsChanged();
|
||||||
|
|
@ -50,6 +65,11 @@ signals:
|
||||||
void activeStateChanged();
|
void activeStateChanged();
|
||||||
void chatReceived(const QString &text);
|
void chatReceived(const QString &text);
|
||||||
void streamingDone(); // chat process exited (session killed)
|
void streamingDone(); // chat process exited (session killed)
|
||||||
|
void rootBackendsLoadedChanged();
|
||||||
|
void availableBackendsChanged();
|
||||||
|
void rootAgentsLoadedChanged();
|
||||||
|
void availableAgentsChanged();
|
||||||
|
void rootModelsLoadedChanged();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onChatReadyRead();
|
void onChatReadyRead();
|
||||||
|
|
@ -63,12 +83,21 @@ private:
|
||||||
void stopStreams();
|
void stopStreams();
|
||||||
QString agentPath() const;
|
QString agentPath() const;
|
||||||
QByteArray run9p(const QStringList &args);
|
QByteArray run9p(const QStringList &args);
|
||||||
|
void ensureRootDataLoaded();
|
||||||
|
|
||||||
QVariantList m_sessions;
|
QVariantList m_sessions;
|
||||||
QString m_activeSessionId;
|
QString m_activeSessionId;
|
||||||
QString m_activeState = "idle";
|
QString m_activeState = "idle";
|
||||||
QString m_agentId;
|
QString m_agentId;
|
||||||
|
|
||||||
|
// Root data cache
|
||||||
|
bool m_rootBackendsLoaded = false;
|
||||||
|
QStringList m_availableBackends;
|
||||||
|
bool m_rootAgentsLoaded = false;
|
||||||
|
QStringList m_availableAgents;
|
||||||
|
bool m_rootModelsLoaded = false;
|
||||||
|
QVariantMap m_rootModels;
|
||||||
|
|
||||||
QProcess *m_chatProc = nullptr;
|
QProcess *m_chatProc = nullptr;
|
||||||
QProcess *m_stateProc = nullptr;
|
QProcess *m_stateProc = nullptr;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Reference in New Issue