Watch: activity line on working cards, All threads browsing, a thread's subagents, Load earlier, and markdown messages

This commit is contained in:
gib committed 2026-11-02 13:10:02 -05:00
1 parent abd2fd0e13
commit d916bfbab5
21 files changed
+928 -46

No files matched your search

+106 -2
View File
@@ -155,8 +155,11 @@ private let feedItemJSON = """
"canStop": true }
""")
#expect(detail.canStop)
// An older gateway leaves out askFirst, so the switch stays hidden.
// An older gateway leaves out askFirst, so the switch stays hidden, and the phase 3 fields.
#expect(detail.askFirst == nil)
#expect(detail.earlierCursor == nil)
#expect(detail.subagents.isEmpty)
#expect(detail.item.activity == nil)
#expect(detail.messages.first?.role == .assistant)
#expect(detail.messages.first?.createdAt == Date(timeIntervalSince1970: 1_793_526_000))
}
@@ -201,6 +204,45 @@ private let feedItemJSON = """
#expect(plain["model"] == nil && plain["askFirst"] == nil)
}
@Test func phase3FromDocs() throws {
let working = feedItemJSON
.replacingOccurrences(of: "waiting_for_approval", with: "running")
.replacingOccurrences(
of: #""request": {"#,
with: #""activity": { "kind": "command", "text": "bun test", "startedAt": "2026-11-01T09:40:00.000Z" }, "request": {"#)
let detail = try decode(ThreadDetail.self, """
{ "item": \(working), "messages": [], "canStop": true, "askFirst": false, "earlierCursor": "w12",
"subagents": [
{ "hostId": "h", "threadId": "s1", "title": "Check PATH", "phase": "running", "settled": false, "updatedAt": "2026-11-01T09:41:00.000Z" },
{ "hostId": "h", "threadId": "s2", "title": "Stopped", "phase": null, "settled": true, "updatedAt": "2026-11-01T09:30:00.000Z" }
] }
""")
let activity = try #require(detail.item.activity)
#expect(activity.kind == .command)
#expect(activity.text == "bun test")
#expect(activity.startedAt == Date(timeIntervalSince1970: 1_793_526_000))
#expect(detail.earlierCursor == "w12")
#expect(detail.subagents.map(\.statusWord) == ["working", "settled"])
#expect(detail.subagents.map(\.isActive) == [true, false])
#expect(detail.subagents.last?.phase == nil)
let thinking = try decode(Activity.self, #"{ "kind": "thinking", "text": "Thinking", "startedAt": null }"#)
#expect(thinking.startedAt == nil)
let history = try decode(HistoryResponse.self, """
{ "messages": [{ "id": "m1", "role": "user", "text": "Hi", "createdAt": "2026-11-01T09:00:00Z" }], "earlierCursor": null }
""")
#expect(history.messages.map(\.id) == ["m1"])
#expect(history.earlierCursor == nil)
let list = try decode(ThreadListResponse.self, """
{ "items": [{ "hostId": "h", "threadId": "t", "title": "Phase 3", "phase": "failed", "settled": false,
"updatedAt": "2026-11-01T09:41:00.000Z" }] }
""")
#expect(list.items.first?.statusWord == "failed")
#expect(list.items.first?.route == .thread(hostId: "h", threadId: "t"))
}
@Test func smallResponses() throws {
#expect(try decode(PairResponse.self, #"{ "deviceId": "9b0c", "deviceToken": "wc_x" }"#).deviceToken == "wc_x")
#expect(try decode(NewThreadResponse.self, #"{ "hostId": "h", "threadId": "t" }"#).threadId == "t")
@@ -291,6 +333,52 @@ private let feedItemJSON = """
#expect(Format.age(of: now.addingTimeInterval(-50 * 3600), now: now) == "2 d")
}
@Test func elapsedTimes() {
let now = Date(timeIntervalSince1970: 100_000)
#expect(Format.elapsed(since: now.addingTimeInterval(5), now: now) == "0s")
#expect(Format.elapsed(since: now.addingTimeInterval(-42), now: now) == "42s")
#expect(Format.elapsed(since: now.addingTimeInterval(-60), now: now) == "1m")
#expect(Format.elapsed(since: now.addingTimeInterval(-199), now: now) == "3m")
#expect(Format.elapsed(since: now.addingTimeInterval(-3600), now: now) == "1h")
#expect(Format.elapsed(since: now.addingTimeInterval(-50 * 3600), now: now) == "2d")
}
@Test func olderMessagesMerge() throws {
func m(_ id: String) -> Message { Message(id: id, role: .assistant, text: id, createdAt: .now) }
let detail = try decode(ThreadDetail.self, #"{ "item": \#(feedItemJSON), "messages": [], "canStop": false, "earlierCursor": "w3" }"#)
var history = MessageHistory()
#expect(history.cursor(detail) == "w3")
#expect(history.messages(before: [m("4"), m("5")]).map(\.id) == ["4", "5"])
// A page that overlaps the window shows each message once.
history.prepend(HistoryResponse(messages: [m("2"), m("3"), m("4")], earlierCursor: "w1"))
#expect(history.cursor(detail) == "w1")
#expect(history.messages(before: [m("4"), m("5")]).map(\.id) == ["2", "3", "4", "5"])
// A refresh moves the window on. The outgoing window stays, so no gap opens.
history.keep([m("4"), m("5")])
#expect(history.messages(before: [m("6"), m("7")]).map(\.id) == ["2", "3", "4", "5", "6", "7"])
history.keep([m("6"), m("7")])
history.prepend(HistoryResponse(messages: [m("1"), m("2")], earlierCursor: nil))
#expect(history.cursor(detail) == nil)
#expect(history.messages(before: [m("7")]).map(\.id) == ["1", "2", "3", "4", "5", "6", "7"])
// Nothing is kept before Load earlier is used.
var fresh = MessageHistory()
fresh.keep([m("4")])
#expect(fresh.messages(before: [m("5")]).map(\.id) == ["5"])
}
@Test func markdownReadsCleanly() {
#expect(Markdown.block("## Done") == "**Done**")
#expect(Markdown.block(" - item") == " • item")
#expect(Markdown.block("> quoted") == "quoted")
#expect(Markdown.block("**bold** stays") == "**bold** stays")
let text = String(Markdown.render("All **48** pass in `host.ts`:\n- bounded retry\n```\n# not a heading\n```").characters)
#expect(text == "All 48 pass in host.ts:\n• bounded retry\n# not a heading")
}
@Test func messageSplitShowsLatestAgentMessage() {
func m(_ id: String, _ role: Message.Role) -> Message { Message(id: id, role: role, text: id, createdAt: .now) }
let split = MessageSplit([m("1", .user), m("2", .assistant), m("3", .assistant), m("4", .user)])
@@ -361,7 +449,10 @@ private let feedItemJSON = """
#expect(thread.url.absoluteString == "wristcode://thread/mac%20mini/t%2F1")
#expect(Route(url: thread.url) == thread)
#expect(Route(url: Route.newThread.url) == .newThread)
for route in [Route.recent, .status, .host(id: "mac mini"), .notifications] {
for route in [
Route.recent, .status, .host(id: "mac mini"), .notifications, .allThreads, .hostProjects(hostId: "mac mini"),
.projectThreads(hostId: "mac mini", projectId: "p/1"),
] {
#expect(Route(url: route.url) == route)
}
#expect(Route(url: URL(string: "wristcode://thread/only-host")!) == nil)
@@ -548,6 +639,19 @@ private let feedItemJSON = """
#expect(try await gateway.feed().moreRecent == 0)
}
@Test func subagentsOpenButStayOutOfTheFeed() async throws {
let gateway = DemoGateway()
let feed = try await gateway.feed()
let parent = try #require(feed.working.first { $0.threadId == "t-container" })
let detail = try await gateway.thread(hostId: parent.hostId, threadId: parent.threadId)
#expect(!detail.subagents.isEmpty)
let listed = feed.needsYou + feed.working + feed.recent
for row in detail.subagents {
#expect(!listed.contains { $0.threadId == row.threadId })
#expect(try await gateway.thread(hostId: row.hostId, threadId: row.threadId).item.title == row.title)
}
}
@Test func multiSelectAnswerMovesThreadToWorking() async throws {
let gateway = DemoGateway()
let item = try #require(try await gateway.feed().needsYou.first { $0.request?.questions.first?.multiSelect == true })
+23
View File
@@ -55,9 +55,32 @@ struct SnapshotTests {
RecentHeader(recent: feed.recent, more: feed.moreRecent)
ForEach(feed.recent) { StatusCard(item: $0) }
MoreRecentRow(count: feed.moreRecent)
AllThreadsRow()
}, "feed-recent.png")
}
@Test func workingCardsWithActivity() async throws {
let feed = try await DemoGateway().feed()
try write(feedColumn { ForEach(feed.working) { StatusCard(item: $0) } }, "feed-activity.png")
}
@Test func browseHostsAndThreads() async throws {
// The preview model has the demo hosts and projects loaded.
try write(titled("All threads", HostsView(scrolls: false)), "browse-hosts.png")
let rows = try await DemoGateway().projectThreads(hostId: "mac-mini", projectId: "p-wc-mac").items
try write(titled("wristcode", ProjectThreadsView(hostId: "mac-mini", projectId: "p-wc-mac", rows: rows, scrolls: false)), "browse-threads.png")
}
@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")
}
@Test func threadWithEarlierAndMarkdown() async throws {
let detail = try await DemoGateway().thread(hostId: "server-gib", threadId: "t-hyprlock")
try write(titled("Thread", ThreadView(hostId: "server-gib", threadId: "t-hyprlock", detail: detail, scrolls: false)), "thread-earlier.png")
}
@Test func fullRecentList() async throws {
// The preview model has every demo Recent thread loaded.
try write(titled("Recent", RecentView(scrolls: false)), "recent.png")