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.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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
Reference in New Issue