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:
parent
620cd6c66e
commit
9f1aec1384
|
|
@ -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 }),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue