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
This commit is contained in:
Levi Neely 2026-08-19 18:07:25 +02:00
parent 620cd6c66e
commit 9f1aec1384
6 changed files with 175 additions and 21 deletions

View File

@ -537,6 +537,48 @@ func buildSessionChildren(
return renameFn(newName) 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.FileNode("id", 0444,
virtfs.Read(func() ([]byte, error) { return []byte(s.ID + "\n"), nil }), virtfs.Read(func() ([]byte, error) { return []byte(s.ID + "\n"), nil }),
), ),

View File

@ -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 2.15
import QtQuick.Controls 2.15 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
Dialog { Dialog {
id: root id: root
title: "New Session" title: editMode ? "Session Settings" : "New Session"
width: 520 width: 520
modal: false modal: false
x: (parent ? (parent.width - width) / 2 : 0) x: (parent ? (parent.width - width) / 2 : 0)
y: (parent ? (parent.height - height) / 2 : 0) y: (parent ? (parent.height - height) / 2 : 0)
property bool dragging: false 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. // Keep the non-modal dialog visually separate from the application behind it.
background: Item { background: Item {
// A dark offset layer reads as a shadow instead of a bright outline. // A dark offset layer reads as a shadow instead of a bright outline.
@ -63,6 +67,13 @@ Dialog {
signal sessionCreated() signal sessionCreated()
// Open in edit mode for a given session.
function openForSession(sessionName) {
editMode = true
editSessionName = sessionName
open()
}
contentItem: ColumnLayout { contentItem: ColumnLayout {
spacing: 16 spacing: 16
Layout.margins: 16 Layout.margins: 16
@ -82,6 +93,7 @@ Dialog {
id: nameField id: nameField
Layout.fillWidth: true Layout.fillWidth: true
placeholderText: "(auto-generated)" placeholderText: "(auto-generated)"
enabled: !root.editMode
} }
} }
@ -110,6 +122,7 @@ Dialog {
id: remoteField id: remoteField
Layout.fillWidth: true Layout.fillWidth: true
placeholderText: "user@host (optional)" placeholderText: "user@host (optional)"
enabled: !root.editMode
} }
} }
@ -145,6 +158,7 @@ Dialog {
RowLayout { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
visible: !root.editMode
CheckBox { CheckBox {
id: yoloCheckBox id: yoloCheckBox
text: "YOLO (DANGEROUS: disables\ntool sandboxing)" text: "YOLO (DANGEROUS: disables\ntool sandboxing)"
@ -162,22 +176,45 @@ Dialog {
Item { Layout.fillWidth: true } Item { Layout.fillWidth: true }
Button { Button {
id: okButton id: okButton
text: "OK" text: root.editMode ? "Save" : "OK"
onClicked: { onClicked: {
errorLabel.text = "" errorLabel.text = ""
var name = nameField.text.trim() if (root.editMode) {
var remote = remoteField.text.trim() // Edit mode: write changed values back.
var cwd = cwdField.text.trim() var cfg = root._originalConfig
var newCwd = cwdField.text.trim()
var newWorkflow = workflowCombo.currentText
var newVariant = variantCombo.currentText
var workflow = workflowCombo.currentText if (newCwd !== cfg.cwd && newCwd !== "") {
var variant = variantCombo.currentText var err = ollie.updateSessionConfig(root.editSessionName, "cwd", newCwd)
var err = ollie.createSession(name, remote, cwd, workflow, variant, yoloCheckBox.checked) if (err) { errorLabel.text = err; return }
if (err === "") { }
ollie.refreshSessions() if (newWorkflow !== cfg.workflow) {
root.sessionCreated() 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() root.close()
} else { } 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. // Do not request keyboard focus while opening. Clicking a field still focuses it.
focus: false focus: false
// Store original config when opened in edit mode for diffing on save.
property var _originalConfig: ({})
onOpened: { onOpened: {
errorLabel.text = "" errorLabel.text = ""
nameField.text = "" if (!ollie.workflowsLoaded) ollie.loadWorkflows()
cwdField.text = ""
remoteField.text = "" if (editMode && editSessionName !== "") {
yoloCheckBox.checked = false // Read current session config and populate fields.
var idx = ollie.availableWorkflows.indexOf("conductor") var cfg = ollie.readSessionConfig(editSessionName)
workflowCombo.currentIndex = idx >= 0 ? idx : 0 _originalConfig = cfg
variantCombo.currentIndex = 0 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
}
} }
} }

View File

@ -13,6 +13,7 @@ Item {
property string killTargetAgentId: "" property string killTargetAgentId: ""
signal newSessionRequested() signal newSessionRequested()
signal settingsRequested() signal settingsRequested()
signal sessionSettingsRequested(string sessionName)
signal newAgentRequested(string sessionId) signal newAgentRequested(string sessionId)
signal goalRequested(string sessionId, string sessionName) signal goalRequested(string sessionId, string sessionName)
signal renameFailed(string itemType, string attemptedName) signal renameFailed(string itemType, string attemptedName)
@ -152,6 +153,12 @@ Item {
tree.goalRequested(tree.killTargetSessionId, tree.killTargetSessionName) tree.goalRequested(tree.killTargetSessionId, tree.killTargetSessionName)
} }
} }
MenuItem {
text: "Settings..."
onClicked: {
tree.sessionSettingsRequested(tree.killTargetSessionName)
}
}
MenuItem { MenuItem {
text: "New Agent..." text: "New Agent..."
onClicked: { onClicked: {

View File

@ -184,8 +184,15 @@ ApplicationWindow {
SplitView.preferredWidth: root.sidebarVisible ? root.width * 0.15 : 0 SplitView.preferredWidth: root.sidebarVisible ? root.width * 0.15 : 0
SplitView.minimumWidth: 0 SplitView.minimumWidth: 0
visiblePane: root.sidebarVisible visiblePane: root.sidebarVisible
onNewSessionRequested: newSessionDialog.open() onNewSessionRequested: {
newSessionDialog.editMode = false
newSessionDialog.editSessionName = ""
newSessionDialog.open()
}
onSettingsRequested: settingsDialog.open() onSettingsRequested: settingsDialog.open()
onSessionSettingsRequested: function(sessionName) {
newSessionDialog.openForSession(sessionName)
}
onNewAgentRequested: function(sessionId) { onNewAgentRequested: function(sessionId) {
newAgentDialog.targetSessionId = sessionId newAgentDialog.targetSessionId = sessionId
newAgentDialog.open() newAgentDialog.open()

View File

@ -652,6 +652,36 @@ QString Ollie9pClient::createSession(const QString &name, const QString &remote,
return QString(); // success 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) void Ollie9pClient::setGoal(const QString &sessionName, const QString &goalText)
{ {
if (!m_9p || !m_9p->isConnected()) return; if (!m_9p || !m_9p->isConnected()) return;

View File

@ -87,6 +87,8 @@ public:
Q_INVOKABLE QStringList getAvailableModels(const QString &backend) const; Q_INVOKABLE QStringList getAvailableModels(const QString &backend) const;
Q_INVOKABLE bool createQuickSession(); 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 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 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 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); Q_INVOKABLE bool killAgent(const QString &sessionId, const QString &agentId);