From 9f1aec13841517dea2ec134dab6c326a92589e19 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Wed, 19 Aug 2026 18:07:25 +0200 Subject: [PATCH] Add session settings dialog (edit mode for NewSessionDialog) - Add session-level cfg file (read: name/cwd/remote/workflow/variant/yolo; write: workflow, variant, cwd) - Add readSessionConfig() and updateSessionConfig() to C++ client - NewSessionDialog supports editMode: pre-populates fields from session cfg, title becomes 'Session Settings', button becomes 'Save' - Name and Remote fields disabled in edit mode (non-reconfigurable) - YOLO checkbox hidden in edit mode - Only changed values are written back on save - Add 'Settings...' to session context menu in SessionTree - Wire sessionSettingsRequested signal through to dialog --- cmd/olliesrv/internal/fs/spec.go | 42 ++++++++++++ kde/gui/NewSessionDialog.qml | 106 +++++++++++++++++++++++++------ kde/gui/SessionTree.qml | 7 ++ kde/gui/main.qml | 9 ++- kde/gui/ollie9pclient.cpp | 30 +++++++++ kde/gui/ollie9pclient.h | 2 + 6 files changed, 175 insertions(+), 21 deletions(-) diff --git a/cmd/olliesrv/internal/fs/spec.go b/cmd/olliesrv/internal/fs/spec.go index a4a9c7c..3f46605 100644 --- a/cmd/olliesrv/internal/fs/spec.go +++ b/cmd/olliesrv/internal/fs/spec.go @@ -537,6 +537,48 @@ func buildSessionChildren( return renameFn(newName) }), ), + virtfs.FileNode("cfg", 0666, + virtfs.Doc("Session configuration. Read: key=value lines. Write: single key=value to update."), + virtfs.Read(func() ([]byte, error) { + var sb strings.Builder + fmt.Fprintf(&sb, "name=%s\n", s.Name()) + fmt.Fprintf(&sb, "cwd=%s\n", s.Cwd()) + fmt.Fprintf(&sb, "remote=%s\n", s.Remote) + fmt.Fprintf(&sb, "workflow=%s\n", s.Workflow()) + fmt.Fprintf(&sb, "variant=%s\n", s.Variant()) + fmt.Fprintf(&sb, "yolo=%t\n", s.Yolo) + return []byte(sb.String()), nil + }), + virtfs.Write(func(data []byte) error { + input := strings.TrimSpace(string(data)) + if input == "" { + return nil + } + for _, line := range strings.Split(input, "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + k, v, ok := strings.Cut(line, "=") + if !ok { + return fmt.Errorf("invalid cfg format (expected key=value)") + } + switch k { + case "workflow": + s.SetWorkflow(v) + case "variant": + s.SetVariant(v) + case "cwd": + if v != "" { + s.SetCwd(v) + } + default: + return fmt.Errorf("unknown session cfg key: %s", k) + } + } + return nil + }), + ), virtfs.FileNode("id", 0444, virtfs.Read(func() ([]byte, error) { return []byte(s.ID + "\n"), nil }), ), diff --git a/kde/gui/NewSessionDialog.qml b/kde/gui/NewSessionDialog.qml index 1f0a2fd..be86680 100644 --- a/kde/gui/NewSessionDialog.qml +++ b/kde/gui/NewSessionDialog.qml @@ -1,17 +1,21 @@ -// NewSessionDialog.qml — Dialog for creating a new session +// NewSessionDialog.qml — Dialog for creating or editing a session import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 Dialog { id: root - title: "New Session" + title: editMode ? "Session Settings" : "New Session" width: 520 modal: false x: (parent ? (parent.width - width) / 2 : 0) y: (parent ? (parent.height - height) / 2 : 0) property bool dragging: false + // Mode: false = create, true = edit existing session + property bool editMode: false + property string editSessionName: "" + // Keep the non-modal dialog visually separate from the application behind it. background: Item { // A dark offset layer reads as a shadow instead of a bright outline. @@ -63,6 +67,13 @@ Dialog { signal sessionCreated() + // Open in edit mode for a given session. + function openForSession(sessionName) { + editMode = true + editSessionName = sessionName + open() + } + contentItem: ColumnLayout { spacing: 16 Layout.margins: 16 @@ -82,6 +93,7 @@ Dialog { id: nameField Layout.fillWidth: true placeholderText: "(auto-generated)" + enabled: !root.editMode } } @@ -110,6 +122,7 @@ Dialog { id: remoteField Layout.fillWidth: true placeholderText: "user@host (optional)" + enabled: !root.editMode } } @@ -145,6 +158,7 @@ Dialog { RowLayout { Layout.fillWidth: true + visible: !root.editMode CheckBox { id: yoloCheckBox text: "YOLO (DANGEROUS: disables\ntool sandboxing)" @@ -162,22 +176,45 @@ Dialog { Item { Layout.fillWidth: true } Button { id: okButton - text: "OK" + text: root.editMode ? "Save" : "OK" onClicked: { errorLabel.text = "" - var name = nameField.text.trim() - var remote = remoteField.text.trim() - var cwd = cwdField.text.trim() + if (root.editMode) { + // Edit mode: write changed values back. + var cfg = root._originalConfig + var newCwd = cwdField.text.trim() + var newWorkflow = workflowCombo.currentText + var newVariant = variantCombo.currentText - var workflow = workflowCombo.currentText - var variant = variantCombo.currentText - var err = ollie.createSession(name, remote, cwd, workflow, variant, yoloCheckBox.checked) - if (err === "") { - ollie.refreshSessions() - root.sessionCreated() + if (newCwd !== cfg.cwd && newCwd !== "") { + var err = ollie.updateSessionConfig(root.editSessionName, "cwd", newCwd) + if (err) { errorLabel.text = err; return } + } + if (newWorkflow !== cfg.workflow) { + var err2 = ollie.updateSessionConfig(root.editSessionName, "workflow", newWorkflow) + if (err2) { errorLabel.text = err2; return } + } + if (newVariant !== cfg.variant) { + var varVal = newVariant === "default" ? "" : newVariant + var err3 = ollie.updateSessionConfig(root.editSessionName, "variant", varVal) + if (err3) { errorLabel.text = err3; return } + } root.close() } else { - errorLabel.text = err + // Create mode. + var name = nameField.text.trim() + var remote = remoteField.text.trim() + var cwd = cwdField.text.trim() + var workflow = workflowCombo.currentText + var variant = variantCombo.currentText + var err4 = ollie.createSession(name, remote, cwd, workflow, variant, yoloCheckBox.checked) + if (err4 === "") { + ollie.refreshSessions() + root.sessionCreated() + root.close() + } else { + errorLabel.text = err4 + } } } } @@ -190,14 +227,43 @@ Dialog { // Do not request keyboard focus while opening. Clicking a field still focuses it. focus: false + // Store original config when opened in edit mode for diffing on save. + property var _originalConfig: ({}) + onOpened: { errorLabel.text = "" - nameField.text = "" - cwdField.text = "" - remoteField.text = "" - yoloCheckBox.checked = false - var idx = ollie.availableWorkflows.indexOf("conductor") - workflowCombo.currentIndex = idx >= 0 ? idx : 0 - variantCombo.currentIndex = 0 + if (!ollie.workflowsLoaded) ollie.loadWorkflows() + + if (editMode && editSessionName !== "") { + // Read current session config and populate fields. + var cfg = ollie.readSessionConfig(editSessionName) + _originalConfig = cfg + nameField.text = cfg.name || editSessionName + cwdField.text = cfg.cwd || "" + remoteField.text = cfg.remote || "" + + var wfIdx = ollie.availableWorkflows.indexOf(cfg.workflow || "conductor") + workflowCombo.currentIndex = wfIdx >= 0 ? wfIdx : 0 + + // Populate variant after workflow is set. + var variants = ollie.variantsForWorkflow(workflowCombo.currentText) + variantCombo.model = variants + var vIdx = variants.indexOf(cfg.variant || "default") + variantCombo.currentIndex = vIdx >= 0 ? vIdx : 0 + + yoloCheckBox.checked = cfg.yolo === "true" + } else { + // Create mode: reset all fields. + editMode = false + editSessionName = "" + _originalConfig = {} + nameField.text = "" + cwdField.text = "" + remoteField.text = "" + yoloCheckBox.checked = false + var idx = ollie.availableWorkflows.indexOf("conductor") + workflowCombo.currentIndex = idx >= 0 ? idx : 0 + variantCombo.currentIndex = 0 + } } } diff --git a/kde/gui/SessionTree.qml b/kde/gui/SessionTree.qml index a56802b..68d82fb 100644 --- a/kde/gui/SessionTree.qml +++ b/kde/gui/SessionTree.qml @@ -13,6 +13,7 @@ Item { property string killTargetAgentId: "" signal newSessionRequested() signal settingsRequested() + signal sessionSettingsRequested(string sessionName) signal newAgentRequested(string sessionId) signal goalRequested(string sessionId, string sessionName) signal renameFailed(string itemType, string attemptedName) @@ -152,6 +153,12 @@ Item { tree.goalRequested(tree.killTargetSessionId, tree.killTargetSessionName) } } + MenuItem { + text: "Settings..." + onClicked: { + tree.sessionSettingsRequested(tree.killTargetSessionName) + } + } MenuItem { text: "New Agent..." onClicked: { diff --git a/kde/gui/main.qml b/kde/gui/main.qml index 44cb230..4176ade 100644 --- a/kde/gui/main.qml +++ b/kde/gui/main.qml @@ -184,8 +184,15 @@ ApplicationWindow { SplitView.preferredWidth: root.sidebarVisible ? root.width * 0.15 : 0 SplitView.minimumWidth: 0 visiblePane: root.sidebarVisible - onNewSessionRequested: newSessionDialog.open() + onNewSessionRequested: { + newSessionDialog.editMode = false + newSessionDialog.editSessionName = "" + newSessionDialog.open() + } onSettingsRequested: settingsDialog.open() + onSessionSettingsRequested: function(sessionName) { + newSessionDialog.openForSession(sessionName) + } onNewAgentRequested: function(sessionId) { newAgentDialog.targetSessionId = sessionId newAgentDialog.open() diff --git a/kde/gui/ollie9pclient.cpp b/kde/gui/ollie9pclient.cpp index 2e1cf8c..417db25 100644 --- a/kde/gui/ollie9pclient.cpp +++ b/kde/gui/ollie9pclient.cpp @@ -652,6 +652,36 @@ QString Ollie9pClient::createSession(const QString &name, const QString &remote, return QString(); // success } +QVariantMap Ollie9pClient::readSessionConfig(const QString &sessionName) const +{ + QVariantMap result; + if (!m_9p || !m_9p->isConnected() || sessionName.isEmpty()) return result; + + QString path = "session/" + sessionName + "/cfg"; + QByteArray out = m_9p->read(path); + QString raw = QString::fromUtf8(out).trimmed(); + if (raw.isEmpty()) return result; + + const auto lines = raw.split('\n', Qt::SkipEmptyParts); + for (const auto &line : lines) { + int eq = line.indexOf('='); + if (eq > 0) { + result[line.left(eq)] = line.mid(eq + 1); + } + } + return result; +} + +QString Ollie9pClient::updateSessionConfig(const QString &sessionName, const QString &key, const QString &value) +{ + if (!m_9p || !m_9p->isConnected() || sessionName.isEmpty()) return QStringLiteral("not connected"); + + QString path = "session/" + sessionName + "/cfg"; + QString payload = key + "=" + value + "\n"; + m_9p->write(path, payload.toUtf8()); + return m_9p->lastError(); +} + void Ollie9pClient::setGoal(const QString &sessionName, const QString &goalText) { if (!m_9p || !m_9p->isConnected()) return; diff --git a/kde/gui/ollie9pclient.h b/kde/gui/ollie9pclient.h index 1cd6203..5fb3c47 100644 --- a/kde/gui/ollie9pclient.h +++ b/kde/gui/ollie9pclient.h @@ -87,6 +87,8 @@ public: Q_INVOKABLE QStringList getAvailableModels(const QString &backend) const; Q_INVOKABLE bool createQuickSession(); Q_INVOKABLE QString createSession(const QString &name, const QString &remote, const QString &cwd = QString(), const QString &workflow = QString(), const QString &variant = QString(), bool yolo = false); + Q_INVOKABLE QVariantMap readSessionConfig(const QString &sessionName) const; + Q_INVOKABLE QString updateSessionConfig(const QString &sessionName, const QString &key, const QString &value); Q_INVOKABLE void setGoal(const QString &sessionName, const QString &goalText); Q_INVOKABLE QString createAgent(const QString &sessionId, const QString &backend, const QString &model, const QString &agent, const QString &remote, const QString &agentAlias); Q_INVOKABLE bool killAgent(const QString &sessionId, const QString &agentId);