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
@@ -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 })
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user