diff --git a/watch/README.md b/watch/README.md index af095de..b0c74f3 100644 --- a/watch/README.md +++ b/watch/README.md @@ -60,8 +60,8 @@ xcrun simctl launch 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 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, -a thread's subagents, Load earlier with markdown, the changes line and Changed list, search and -its empty state, the complication's two modes, the Smart Stack widget, and a question +a thread's subagents collapsed and open, Load earlier with markdown, the changes line and Changed +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: ```sh diff --git a/watch/WristCode/Views/ThreadView.swift b/watch/WristCode/Views/ThreadView.swift index c3b7d07..485ca59 100644 --- a/watch/WristCode/Views/ThreadView.swift +++ b/watch/WristCode/Views/ThreadView.swift @@ -1,6 +1,6 @@ // 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 -// latest run changed with their line counts, Reply by +// them, the pending approval or question with its options, the thread's subagents under a header +// 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 // while a run is active. A subagent's request shows first, labeled with the subagent. Message text // renders as markdown. @@ -29,21 +29,26 @@ struct ThreadView: View { /// Pages Load earlier fetched. The 10 s refresh replaces only the latest window. @State private var history = MessageHistory() @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. private let scrolls: Bool - /// `detail` and `selections` start the screen with a thread loaded and options ticked, for - /// snapshots and previews. + /// `detail`, `selections` and `subagentsOpen` start the screen with a thread loaded, options + /// ticked and the Subagents section set, for snapshots and previews. init( hostId: String, threadId: String, - detail: ThreadDetail? = nil, selections: [String: Set] = [:], scrolls: Bool = true + detail: ThreadDetail? = nil, selections: [String: Set] = [:], subagentsOpen: Bool? = nil, + scrolls: Bool = true ) { self.hostId = hostId self.threadId = threadId self.scrolls = scrolls _detail = State(initialValue: detail) _selections = State(initialValue: selections) + _subagentsOpen = State(initialValue: subagentsOpen) } var body: some View { @@ -144,11 +149,14 @@ struct ThreadView: View { } if !detail.subagents.isEmpty { - sectionLabel("Subagents") - ForEach(detail.subagents) { row in - ThreadLine(row: row) { - Text(row.statusWord) - .foregroundStyle(row.isActive ? row.color : Theme.secondaryText) + let open = subagentsOpen ?? detail.subagents.contains { $0.statusWord == "waiting" } + subagentsHeader(detail.subagents, open: open) + if open { + ForEach(detail.subagents) { row in + ThreadLine(row: row) { + Text(row.statusWord) + .foregroundStyle(row.isActive ? row.color : Theme.secondaryText) + } } } } @@ -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 { Text(title) .font(.system(size: 11, weight: .semibold)) @@ -209,6 +217,35 @@ struct ThreadView: View { .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 private func requestCards(_ request: PendingRequest, from via: FeedItem.Via?) -> some View { if request.isQuestion { diff --git a/watch/WristCodeTests/SnapshotTests.swift b/watch/WristCodeTests/SnapshotTests.swift index f438680..2b146d3 100644 --- a/watch/WristCodeTests/SnapshotTests.swift +++ b/watch/WristCodeTests/SnapshotTests.swift @@ -127,6 +127,7 @@ struct SnapshotTests { @Test func threadWithSubagents() async throws { 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, subagentsOpen: true, scrolls: false)), "thread-subagents-open.png") } @Test func threadWithEarlierAndMarkdown() async throws { diff --git a/watch/screenshots/thread-subagent.png b/watch/screenshots/thread-subagent.png index c0383c1..391bf00 100644 Binary files a/watch/screenshots/thread-subagent.png and b/watch/screenshots/thread-subagent.png differ diff --git a/watch/screenshots/thread-subagents-open.png b/watch/screenshots/thread-subagents-open.png new file mode 100644 index 0000000..8313093 Binary files /dev/null and b/watch/screenshots/thread-subagents-open.png differ diff --git a/watch/screenshots/thread-subagents.png b/watch/screenshots/thread-subagents.png index 19c962f..654176c 100644 Binary files a/watch/screenshots/thread-subagents.png and b/watch/screenshots/thread-subagents.png differ