Watch: activity line on working cards, All threads browsing, a thread's subagents, Load earlier, and markdown messages
This commit is contained in:
1 parent
abd2fd0e13
commit
d916bfbab5
21 files changed
+928
-46
No files matched your search
@@ -1,7 +1,8 @@
|
||||
// Mock B's attention feed: the count waiting on you, then cards for every host. Needs you first
|
||||
// (with Allow and Deny on the card), then working (with what the agent is doing), then recent.
|
||||
// A Recent row swipes left to settle, and the Recent header settles every completed thread. Past
|
||||
// Recent's first 10, an "N more" row opens the full Recent list.
|
||||
// (with Allow and Deny on the card), then working (with the step the agent is on and how long it
|
||||
// has run), then recent. A Recent row swipes left to settle, and the Recent header settles every
|
||||
// completed thread. Past Recent's first 10, an "N more" row opens the full Recent list. The last
|
||||
// row opens All threads.
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@@ -39,6 +40,7 @@ struct FeedView: View {
|
||||
.foregroundStyle(Theme.secondaryText)
|
||||
.feedRow()
|
||||
}
|
||||
AllThreadsRow().feedRow()
|
||||
} else if model.feedError != nil {
|
||||
Button("Try again") { Task { await model.refreshFeed() } }
|
||||
.buttonStyle(PillButtonStyle(fill: Theme.control, foreground: .white))
|
||||
@@ -207,6 +209,28 @@ struct MoreRecentRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// The feed's last row, a plain card opening All threads.
|
||||
struct AllThreadsRow: View {
|
||||
@Environment(AppModel.self) private var model
|
||||
|
||||
var body: some View {
|
||||
Button { model.path.append(.allThreads) } label: {
|
||||
Card(edge: Theme.done) {
|
||||
HStack {
|
||||
Text("All threads")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
Spacer(minLength: 4)
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 11, weight: .bold))
|
||||
.foregroundStyle(Theme.secondaryText)
|
||||
}
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
|
||||
/// An amber card for a thread waiting on an approval or an answer, with Allow and Deny inline.
|
||||
/// When a subagent asked, one line names it, and the answer goes to the subagent.
|
||||
struct NeedsYouCard: View {
|
||||
@@ -292,7 +316,8 @@ struct NeedsYouCard: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// A teal card for a working thread, or a grey one for a recent thread. A recent thread that failed
|
||||
/// A teal card for a working thread, or a grey one for a recent thread. A working thread shows the
|
||||
/// step it is on, or its latest message when the gateway names no step. A recent thread that failed
|
||||
/// to settle shows why.
|
||||
struct StatusCard: View {
|
||||
@Environment(AppModel.self) private var model
|
||||
@@ -310,8 +335,11 @@ struct StatusCard: View {
|
||||
.foregroundStyle(item.phase.isWorking ? Color.white : Theme.secondaryText)
|
||||
.lineLimit(2)
|
||||
.padding(.top, 1)
|
||||
if item.phase.isWorking, let line = item.lastMessage {
|
||||
Text(line)
|
||||
if item.phase.isWorking, let activity = item.activity {
|
||||
ActivityLine(activity: activity)
|
||||
.padding(.top, 3)
|
||||
} else if item.phase.isWorking, let line = item.lastMessage {
|
||||
Text(Markdown.render(line))
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(Theme.secondaryText)
|
||||
.lineLimit(2)
|
||||
@@ -339,6 +367,58 @@ struct StatusCard: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// What a working thread is doing: a teal symbol for the kind of step, its text, and how long it has
|
||||
/// run. The elapsed time is worked out when the card renders, so the feed's 10 s refresh moves it on.
|
||||
struct ActivityLine: View {
|
||||
var activity: Activity
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 4) {
|
||||
Image(systemName: symbol)
|
||||
.font(.system(size: 10, weight: .bold))
|
||||
.foregroundStyle(Theme.work)
|
||||
.frame(width: 14)
|
||||
label
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(Theme.secondaryText)
|
||||
.lineLimit(2)
|
||||
Spacer(minLength: 4)
|
||||
if let startedAt = activity.startedAt {
|
||||
Text(Format.elapsed(since: startedAt))
|
||||
.font(.system(size: 11, weight: .semibold))
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(Theme.work)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var label: Text {
|
||||
// Marked as code, which Text sets in monospace. A monospaced Font loses to the app's rounded design.
|
||||
let mono = { (text: String) -> Text in
|
||||
var code = AttributedString(text)
|
||||
code.inlinePresentationIntent = .code
|
||||
return Text(code).font(.system(size: 11))
|
||||
}
|
||||
return switch activity.kind {
|
||||
case .command: mono(activity.text)
|
||||
case .edit: Text("Editing \(mono(activity.text))")
|
||||
default: Text(activity.text)
|
||||
}
|
||||
}
|
||||
|
||||
private var symbol: String {
|
||||
switch activity.kind {
|
||||
case .command: "terminal"
|
||||
case .edit: "pencil"
|
||||
case .search: "magnifyingglass"
|
||||
case .web: "globe"
|
||||
case .subagent: "arrow.triangle.branch"
|
||||
case .thinking: "ellipsis"
|
||||
default: "wrench.adjustable"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("Feed") {
|
||||
NavigationStack {
|
||||
FeedView()
|
||||
|
||||
Reference in new issue
Block a user