Add a pause that holds an agent after its current step

This commit is contained in:
Ian Gulliver
2026-10-22 21:55:42 -07:00
parent 73519bf4dd
commit 293c21ca8c
25 changed files with 1014 additions and 472 deletions
+40
View File
@@ -174,6 +174,10 @@ final class ChatModel {
builder.open && session?.state == .running
}
var paused: Bool {
liveSession?.paused ?? false
}
var reviewing: Bool {
let state = taskId.flatMap { store.findTask($0) }?.state
return state == .reviewing || state == .reviewPending || state == .mergeAfterReview
@@ -559,6 +563,20 @@ final class ChatModel {
}
}
}
func setPaused(_ sessionId: String, _ paused: Bool, _ overlays: Overlays) {
Task {
do {
if paused {
_ = try await store.api.client.pauseSession(path: .init(sessionId: sessionId)).ok
} else {
_ = try await store.api.client.resumeSession(path: .init(sessionId: sessionId)).ok
}
} catch {
overlays.showAlert(errorText(error))
}
}
}
}
struct ChatView: View {
@@ -825,6 +843,24 @@ private struct ScrollMetrics: Equatable {
let offset: CGFloat
}
private struct PausedLine: View {
let running: Bool
var body: some View {
HStack(spacing: 6) {
Image(systemName: "pause.fill")
.font(.system(size: 10))
Text(running ? "Paused: finishes its current step, then holds until you resume" : "Paused: no turn starts until you resume")
.font(Fonts.font(13))
}
.foregroundStyle(Palette.muted)
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(Palette.surface, in: RoundedRectangle(cornerRadius: 6))
.overlay(RoundedRectangle(cornerRadius: 6).stroke(Palette.border))
}
}
private struct TranscriptScroll: View {
let model: ChatModel
@Environment(Store.self) private var store
@@ -855,6 +891,10 @@ private struct TranscriptScroll: View {
RowView(row: row, model: model, nested: false)
.padding(.top, 12)
}
if model.paused {
PausedLine(running: running)
.padding(.top, 12)
}
ForEach(model.builder.pending) { p in
PendingView(pending: p, model: model)
.padding(.top, 12)
+15 -2
View File
@@ -527,7 +527,7 @@ struct ComposerView: View {
controls(m)
.frame(maxWidth: .infinity, alignment: .leading)
status
actions(live?.id, running: sessionState == .running)
actions(live?.id, running: sessionState == .running, paused: live?.paused ?? false)
.frame(maxWidth: .infinity, alignment: .trailing)
}
}
@@ -622,7 +622,7 @@ struct ComposerView: View {
}
}
private func actions(_ sessionId: String?, running: Bool) -> some View {
private func actions(_ sessionId: String?, running: Bool, paused: Bool) -> some View {
HStack(spacing: 8) {
Button("Interrupt") {
if let sessionId {
@@ -632,6 +632,19 @@ struct ComposerView: View {
.buttonStyle(WebButtonStyle(danger: true))
.disabled(!running)
.fixedSize()
if case .task = chat.kind, !chat.team, !chat.closed {
Button {
if let sessionId {
chat.setPaused(sessionId, !paused, overlays)
}
} label: {
Image(systemName: paused ? "play.fill" : "pause.fill")
.frame(width: 14, height: 14)
}
.buttonStyle(WebButtonStyle())
.disabled(sessionId == nil)
.help(paused ? "Resume the agent" : "Pause the agent after its current step")
}
Button(chat.closed ? "Continue" : "Send") {
submit(modelState())
}
+1 -1
View File
@@ -858,7 +858,7 @@ final class Store {
}
notifyTask(self, ev)
reloadTask(ev)
case "session.created", "session.state", "session.subagents", "session.model":
case "session.created", "session.state", "session.subagents", "session.model", "session.paused":
if ev.owner != nil {
reloadSessions(nil)
} else if let taskId = ev.taskId {