Watch: the Subagents section on the thread screen collapses behind a header with the count and how many wait or work, and opens on its own when one waits

This commit is contained in:
gib committed 2026-11-02 14:12:45 -05:00
1 parent 480db90e98
commit d10df56cc9
6 files changed
+47 -9

No files matched your search

+2 -2
View File
@@ -60,8 +60,8 @@ xcrun simctl launch <udid> org.gbrown.wristcode.watchkitapp -demo -screen thread
status screens, the subagent card and thread, Recent with Settle all and the "N more" row, the full status screens, the subagent card and thread, Recent with Settle all and the "N more" row, the full
Recent list, a finished thread with Settle, a running thread with Ask before acting, New thread Recent list, a finished thread with Settle, a running thread with Ask before acting, New thread
with its model list, the working cards' activity lines, All threads' hosts and a project's threads, with its model list, the working cards' activity lines, All threads' hosts and a project's threads,
a thread's subagents, Load earlier with markdown, the changes line and Changed list, search and a thread's subagents collapsed and open, Load earlier with markdown, the changes line and Changed
its empty state, the complication's two modes, the Smart Stack widget, and a question list, search and its empty state, the complication's two modes, the Smart Stack widget, and a question
notification are rendered by `SnapshotTests` instead, which only run when given a directory: notification are rendered by `SnapshotTests` instead, which only run when given a directory:
```sh ```sh
+44 -7
View File
@@ -1,6 +1,6 @@
// One thread: the agent's latest message large, older messages one scroll up with Load earlier above // One thread: the agent's latest message large, older messages one scroll up with Load earlier above
// them, the pending approval or question with its options, the thread's subagents, the files its // them, the pending approval or question with its options, the thread's subagents under a header
// latest run changed with their line counts, Reply by // that collapses them, the files its latest run changed with their line counts, Reply by
// dictation or Scribble, Settle once it finished or failed, an Ask before acting switch, and Stop // dictation or Scribble, Settle once it finished or failed, an Ask before acting switch, and Stop
// while a run is active. A subagent's request shows first, labeled with the subagent. Message text // while a run is active. A subagent's request shows first, labeled with the subagent. Message text
// renders as markdown. // renders as markdown.
@@ -29,21 +29,26 @@ struct ThreadView: View {
/// Pages Load earlier fetched. The 10 s refresh replaces only the latest window. /// Pages Load earlier fetched. The 10 s refresh replaces only the latest window.
@State private var history = MessageHistory() @State private var history = MessageHistory()
@State private var loadingEarlier = false @State private var loadingEarlier = false
/// Whether the Subagents section is open, once tapped. Nil follows the default: open while a
/// subagent waits on you, closed otherwise. Kept for as long as the screen is.
@State private var subagentsOpen: Bool?
/// Snapshots render the column without its ScrollView, which ImageRenderer leaves blank. /// Snapshots render the column without its ScrollView, which ImageRenderer leaves blank.
private let scrolls: Bool private let scrolls: Bool
/// `detail` and `selections` start the screen with a thread loaded and options ticked, for /// `detail`, `selections` and `subagentsOpen` start the screen with a thread loaded, options
/// snapshots and previews. /// ticked and the Subagents section set, for snapshots and previews.
init( init(
hostId: String, threadId: String, hostId: String, threadId: String,
detail: ThreadDetail? = nil, selections: [String: Set<String>] = [:], scrolls: Bool = true detail: ThreadDetail? = nil, selections: [String: Set<String>] = [:], subagentsOpen: Bool? = nil,
scrolls: Bool = true
) { ) {
self.hostId = hostId self.hostId = hostId
self.threadId = threadId self.threadId = threadId
self.scrolls = scrolls self.scrolls = scrolls
_detail = State(initialValue: detail) _detail = State(initialValue: detail)
_selections = State(initialValue: selections) _selections = State(initialValue: selections)
_subagentsOpen = State(initialValue: subagentsOpen)
} }
var body: some View { var body: some View {
@@ -144,7 +149,9 @@ struct ThreadView: View {
} }
if !detail.subagents.isEmpty { if !detail.subagents.isEmpty {
sectionLabel("Subagents") let open = subagentsOpen ?? detail.subagents.contains { $0.statusWord == "waiting" }
subagentsHeader(detail.subagents, open: open)
if open {
ForEach(detail.subagents) { row in ForEach(detail.subagents) { row in
ThreadLine(row: row) { ThreadLine(row: row) {
Text(row.statusWord) Text(row.statusWord)
@@ -152,6 +159,7 @@ struct ThreadView: View {
} }
} }
} }
}
if !detail.changedFiles.isEmpty { if !detail.changedFiles.isEmpty {
sectionLabel("Changed") sectionLabel("Changed")
@@ -200,7 +208,7 @@ struct ThreadView: View {
} }
} }
/// A small grey title over a list, such as Subagents or Changed. /// A small grey title over a list, such as Changed.
private func sectionLabel(_ title: String) -> some View { private func sectionLabel(_ title: String) -> some View {
Text(title) Text(title)
.font(.system(size: 11, weight: .semibold)) .font(.system(size: 11, weight: .semibold))
@@ -209,6 +217,35 @@ struct ThreadView: View {
.padding(.top, 4) .padding(.top, 4)
} }
/// The Subagents title as a row that opens and closes the list, with the count, how many wait or
/// work, and a chevron on the right, as in "2 · 1 working".
private func subagentsHeader(_ rows: [ThreadRow], open: Bool) -> some View {
let counts = ["waiting", "working"].compactMap { word -> String? in
let count = rows.count { $0.statusWord == word }
return count > 0 ? "\(count) \(word)" : nil
}
let summary = (["\(rows.count)"] + counts).joined(separator: " · ")
return Button { subagentsOpen = !open } label: {
HStack(spacing: 4) {
Text("Subagents")
Spacer(minLength: 4)
Text(summary)
.lineLimit(1)
Image(systemName: open ? "chevron.down" : "chevron.right")
.font(.system(size: 10, weight: .semibold))
.frame(width: 12)
}
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(Theme.secondaryText)
.padding(.horizontal, 6)
.padding(.vertical, 6)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityLabel("Subagents, \(summary)")
.accessibilityValue(open ? "Expanded" : "Collapsed")
}
@ViewBuilder @ViewBuilder
private func requestCards(_ request: PendingRequest, from via: FeedItem.Via?) -> some View { private func requestCards(_ request: PendingRequest, from via: FeedItem.Via?) -> some View {
if request.isQuestion { if request.isQuestion {
+1
View File
@@ -127,6 +127,7 @@ struct SnapshotTests {
@Test func threadWithSubagents() async throws { @Test func threadWithSubagents() async throws {
let detail = try await DemoGateway().thread(hostId: "server-gib", threadId: "t-container") let detail = try await DemoGateway().thread(hostId: "server-gib", threadId: "t-container")
try write(titled("Thread", ThreadView(hostId: "server-gib", threadId: "t-container", detail: detail, scrolls: false)), "thread-subagents.png") try write(titled("Thread", ThreadView(hostId: "server-gib", threadId: "t-container", detail: detail, scrolls: false)), "thread-subagents.png")
try write(titled("Thread", ThreadView(hostId: "server-gib", threadId: "t-container", detail: detail, subagentsOpen: true, scrolls: false)), "thread-subagents-open.png")
} }
@Test func threadWithEarlierAndMarkdown() async throws { @Test func threadWithEarlierAndMarkdown() async throws {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 65 KiB