diff --git a/gui/main.qml b/gui/main.qml index 966e9b2..7dd6d03 100644 --- a/gui/main.qml +++ b/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() diff --git a/gui/ollie9pclient.cpp b/gui/ollie9pclient.cpp index 305e9c3..3db27c3 100644 --- a/gui/ollie9pclient.cpp +++ b/gui/ollie9pclient.cpp @@ -2,6 +2,7 @@ #include #include +#include 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() diff --git a/gui/ollie9pclient.h b/gui/ollie9pclient.h index fe94ab7..ed11edc 100644 --- a/gui/ollie9pclient.h +++ b/gui/ollie9pclient.h @@ -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; };