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:
Levi Neely 2026-08-01 11:55:17 +02:00
parent 69458631dc
commit 774c3d4d77
3 changed files with 260 additions and 0 deletions

View File

@ -1,6 +1,7 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Dialogs 1.15
ApplicationWindow {
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 {
anchors.fill: parent
@ -292,6 +419,12 @@ ApplicationWindow {
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()

View File

@ -2,6 +2,7 @@
#include <QDir>
#include <QStandardPaths>
#include <QDebug>
static QString ninepBin()
{
@ -30,6 +31,7 @@ Ollie9pClient::Ollie9pClient(QObject *parent)
: QObject(parent)
{
refreshSessions();
ensureRootDataLoaded();
}
Ollie9pClient::~Ollie9pClient()
@ -191,6 +193,102 @@ void Ollie9pClient::switchAgent(const QString &sessionId, const QString &agentId
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 ---
void Ollie9pClient::startChatStream()

View File

@ -22,6 +22,11 @@ class Ollie9pClient : public QObject
Q_PROPERTY(QString activeSessionId READ activeSessionId WRITE setActiveSessionId NOTIFY activeSessionIdChanged)
Q_PROPERTY(QString activeAgentId READ activeAgentId NOTIFY activeAgentIdChanged)
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:
explicit Ollie9pClient(QObject *parent = nullptr);
@ -33,6 +38,12 @@ public:
QString activeAgentId() const { return m_agentId; }
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 QString readLog();
Q_INVOKABLE bool submit(const QString &prompt);
@ -42,6 +53,10 @@ public:
Q_INVOKABLE QStringList getAgents(const QString &sessionId);
Q_INVOKABLE void setActiveAgentId(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:
void sessionsChanged();
@ -50,6 +65,11 @@ signals:
void activeStateChanged();
void chatReceived(const QString &text);
void streamingDone(); // chat process exited (session killed)
void rootBackendsLoadedChanged();
void availableBackendsChanged();
void rootAgentsLoadedChanged();
void availableAgentsChanged();
void rootModelsLoadedChanged();
private slots:
void onChatReadyRead();
@ -63,12 +83,21 @@ private:
void stopStreams();
QString agentPath() const;
QByteArray run9p(const QStringList &args);
void ensureRootDataLoaded();
QVariantList m_sessions;
QString m_activeSessionId;
QString m_activeState = "idle";
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_stateProc = nullptr;
};