feat: add v2 task control and asynchronous isolated workers

This commit is contained in:
WHC
2026-10-20 14:46:57 +08:00
parent 143dba203b
commit d971cb02b0
26 changed files with 1590 additions and 66 deletions
+5 -5
View File
@@ -22,7 +22,7 @@ Rectangle {
function submit() {
const t = input.text.trim()
if (!t.length || running)
if (!t.length)
return
input.text = ""
submitted(t)
@@ -142,7 +142,7 @@ Rectangle {
Layout.preferredHeight: Math.min(root.compact ? 120 : 170,
Math.max(56, contentHeight + 12))
placeholderText: root.running
? "the turn is running — Esc stops it"
? "Send a correction now — Esc pauses execution"
: (root.compact
? "Ask for something…"
: "Ask for something (Enter sends, Shift+Enter for a new line)")
@@ -155,7 +155,7 @@ Rectangle {
inputMethodHints: Qt.ImhMultiLine | Qt.ImhNoPredictiveText |
Qt.ImhNoAutoUppercase
background: Rectangle { color: "transparent" }
enabled: !root.running
enabled: true
Keys.onPressed: function (ev) {
if (ev.key === Qt.Key_Return || ev.key === Qt.Key_Enter) {
@@ -206,9 +206,9 @@ Rectangle {
Layout.alignment: Qt.AlignBottom
implicitWidth: root.compact ? 68 : 88
implicitHeight: Theme.touchTarget + 4
text: root.running ? "Stop" : "Send"
text: root.running ? (input.text.trim().length ? "Redirect" : "Stop") : "Send"
enabled: root.running || input.text.trim().length > 0
onClicked: root.running ? agent.cancel() : root.submit()
onClicked: root.running && !input.text.trim().length ? agent.cancel() : root.submit()
background: Rectangle {
radius: Theme.radiusSm
color: !action.enabled ? Theme.surfaceAlt
+7
View File
@@ -24,6 +24,7 @@ ApplicationWindow {
property string statusText: ""
property var sessionsView: [] // the store, for the sidebar
property bool navOpen: false // the sliding sidebar, compact only
TaskPanel { id: taskPanel; anchors.centerIn: parent }
property int sidebarWidth: 0 // 0 = follow the window width
readonly property bool compact: width < Theme.compactBreakpoint
readonly property int sidebarWide: sidebarWidth > 0
@@ -318,6 +319,12 @@ ApplicationWindow {
win.statusText = ""
}
}
ToolButton {
objectName: "taskPanelButton"
text: "Tasks"
implicitHeight: Theme.touchTarget
onClicked: taskPanel.open()
}
}
}
+94
View File
@@ -0,0 +1,94 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Dialog {
id: root
objectName: "taskControlDialog"
title: "Task control and jobs"
modal: true
standardButtons: Dialog.Close
width: Math.min(760, parent ? parent.width - 24 : 760)
height: Math.min(640, parent ? parent.height - 24 : 640)
property var state: ({})
function refresh() {
try { state = JSON.parse(agent.taskState()) } catch (e) { state = ({}) }
}
onOpened: { refresh(); config.text = agent.workerConfig() }
Timer { interval: 500; repeat: true; running: root.visible; onTriggered: root.refresh() }
ColumnLayout {
anchors.fill: parent
TabBar {
id: tabs
Layout.fillWidth: true
TabButton { text: "Task / Jobs" }
TabButton { text: "Models" }
TabButton { text: "Integration" }
}
StackLayout {
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: tabs.currentIndex
ColumnLayout {
Label { Layout.fillWidth: true; wrapMode: Text.Wrap; text: (root.state.goal || "No active goal") }
Label { text: "Revision " + (root.state.revision || 0) + " · " + (root.state.status || "idle") + " · " + (root.state.cancel_source || "") }
RowLayout {
CheckBox {
id: readonly
objectName: "taskReadOnly"
text: "Read only"
checked: !!root.state.read_only
onClicked: agent.taskControl(checked, !direct.checked, !!root.state.paused)
}
CheckBox {
id: direct
objectName: "taskMainOnly"
text: "Main agent only"
checked: root.state.delegation === false
onClicked: agent.taskControl(readonly.checked, !checked, !!root.state.paused)
}
Button { text: root.state.paused ? "Resume" : "Pause"; onClicked: agent.taskControl(readonly.checked, !direct.checked, !root.state.paused) }
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
ColumnLayout {
width: parent.width
Repeater {
model: root.state.jobs || []
Frame {
required property var modelData
Layout.fillWidth: true
ColumnLayout {
anchors.fill: parent
Label { text: modelData.job_id + " · " + modelData.kind + " · " + modelData.status }
Label { Layout.fillWidth: true; text: modelData.resource || ""; elide: Text.ElideMiddle }
Label { Layout.fillWidth: true; wrapMode: Text.Wrap; text: (modelData.output || "").slice(-1000) }
RowLayout {
Button { text: "Cancel"; enabled: modelData.status === "running" || modelData.status === "queued"; onClicked: agent.cancelJob(modelData.job_id, false) }
Button { text: "Force stop"; visible: modelData.status === "running_protected"; onClicked: agent.cancelJob(modelData.job_id, true) }
Button { text: "Review delta"; visible: (modelData.resource || "").indexOf("/.agent/v2/wt-") >= 0; onClicked: { worktree.text = modelData.resource; tabs.currentIndex = 2 } }
}
}
}
}
}
}
}
ColumnLayout {
Label { text: "Role model profiles and budgets (provider credentials stay separate)"; wrapMode: Text.Wrap; Layout.fillWidth: true }
ScrollView { Layout.fillWidth: true; Layout.fillHeight: true; TextArea { id: config; wrapMode: TextEdit.Wrap; selectByMouse: true } }
Label { id: configStatus; Layout.fillWidth: true; wrapMode: Text.Wrap }
Button { text: "Save model configuration"; enabled: !agent.running; onClicked: { const e = agent.saveWorkerConfig(config.text); configStatus.text = e.length ? e : "Saved" } }
}
ColumnLayout {
TextField { id: worktree; Layout.fillWidth: true; placeholderText: "Worker worktree path" }
RowLayout {
Button { text: "Preview"; enabled: !agent.running; onClicked: delta.text = agent.worktreeDiff(worktree.text, false) }
Button { text: "Integrate reviewed delta"; enabled: !agent.running; onClicked: delta.text = agent.worktreeDiff(worktree.text, true) }
}
ScrollView { Layout.fillWidth: true; Layout.fillHeight: true; TextArea { id: delta; readOnly: true; wrapMode: TextEdit.Wrap; selectByMouse: true } }
}
}
}
}