Watch: what changed on Recent cards and the thread screen, search in All threads, a switching complication with Allow and Deny in the Smart Stack, and question options on notifications

This commit is contained in:
gib committed 2026-11-02 13:56:10 -05:00
1 parent f8241e42bb
commit bfcbb69550
29 files changed
+1100 -137

No files matched your search

+50 -4
View File
@@ -2,7 +2,8 @@
// SwiftUI previews use it, so every screen renders without a live gateway. It reacts to writes:
// answering an approval moves the thread to working, a reply restarts a finished thread, settling
// hides a thread from the feed, Ask before acting and the notification switches stick, and so on.
// Subagent threads open like any other but stay out of the feed, as the gateway's do.
// Subagent threads open like any other but stay out of the feed, as the gateway's do. Search matches
// titles and messages on the online hosts.
import Foundation
@@ -13,6 +14,8 @@ actor DemoGateway: GatewayAPI {
private var messages: [String: [Message]]
/// Pages before each thread's messages, newest page first.
private let earlierPages: [String: [[Message]]]
/// Files each thread's latest run changed, by thread id.
private let changedFiles: [String: [ChangedFile]]
/// Subagent ids by parent id. Subagents are in `items` but not the feed.
private let children: [String: [String]]
private let subagentIds: Set<String>
@@ -30,6 +33,7 @@ actor DemoGateway: GatewayAPI {
items = fixtures.items
messages = fixtures.messages
earlierPages = fixtures.history
changedFiles = fixtures.changedFiles
children = fixtures.children
subagentIds = Set(fixtures.children.values.joined())
settled = fixtures.settled
@@ -52,7 +56,8 @@ actor DemoGateway: GatewayAPI {
item: item, messages: messages[item.id] ?? [], canStop: item.phase.isWorking,
askFirst: askingFirst.contains(item.id),
earlierCursor: earlierPages[item.id] == nil ? nil : "p0",
subagents: (children[item.id] ?? []).compactMap { id in items.first { $0.id == id }.map(row) })
subagents: (children[item.id] ?? []).compactMap { id in items.first { $0.id == id }.map(row) },
changedFiles: changedFiles[item.id] ?? [])
}
/// Cursors are `p<n>`, the n-th page back from the thread's messages.
@@ -149,6 +154,31 @@ actor DemoGateway: GatewayAPI {
return ThreadListResponse(items: rows)
}
/// Titles first, then messages, on online hosts only, newest first. A message match carries the
/// words around it.
func search(_ query: String) async throws -> SearchResponse {
let query = query.trimmingCharacters(in: .whitespacesAndNewlines)
guard query.count >= 2 else { throw GatewayError.http(status: 400, message: "Search for at least two letters.") }
let online = Set(hosts.filter { $0.status == .online }.map(\.id))
let hits: [(SearchHit, Date)] = items.compactMap { item in
guard online.contains(item.hostId) else { return nil }
let hit = { (snippet: String?) in
SearchHit(thread: self.row(item), projectTitle: item.projectTitle, hostLabel: item.hostLabel, snippet: snippet)
}
if item.title.localizedCaseInsensitiveContains(query) { return (hit(nil), item.updatedAt) }
guard let message = messages[item.id]?.last(where: { $0.text.localizedCaseInsensitiveContains(query) }),
let range = message.text.range(of: query, options: .caseInsensitive)
else { return nil }
let start = message.text.index(range.lowerBound, offsetBy: -30, limitedBy: message.text.startIndex) ?? message.text.startIndex
let end = message.text.index(range.upperBound, offsetBy: 50, limitedBy: message.text.endIndex) ?? message.text.endIndex
let snippet = (start > message.text.startIndex ? "…" : "") + message.text[start ..< end] + (end < message.text.endIndex ? "…" : "")
return (hit(snippet), message.createdAt)
}
return SearchResponse(
hits: hits.sorted { $0.1 > $1.1 }.map(\.0),
unsearched: hosts.filter { !online.contains($0.id) }.map(\.label))
}
func newThread(_ request: NewThreadRequest) async throws -> NewThreadResponse {
guard let host = projectHosts.first(where: { $0.id == request.hostId }),
let project = host.projects.first(where: { $0.id == request.projectId })
@@ -229,6 +259,8 @@ struct DemoFixtures {
let items: [FeedItem]
let messages: [String: [Message]]
let history: [String: [[Message]]]
/// Files each thread's latest run changed, by thread id. Each thread's `changes` totals them.
let changedFiles: [String: [ChangedFile]]
/// Subagent ids by parent id.
let children: [String: [String]]
/// Threads settled from the start, which only a project's thread list shows.
@@ -388,8 +420,22 @@ struct DemoFixtures {
}
settled = Set(settledThreads.map(\.id))
items = [approval, question, release, homelab, scaffold, containerfile, statusScreens, nextjs, hyprlock, cdn]
+ olderRecent + settledThreads + [path, image, healthcheck]
func file(_ path: String, _ additions: Int, _ deletions: Int) -> ChangedFile {
ChangedFile(path: path, name: String(path.split(separator: "/").last ?? ""), additions: additions, deletions: deletions)
}
let changed = [
nextjs.id: [file("package.json", 31, 0), file("next.config.ts", 9, 6), file("app/layout.tsx", 2, 2)],
cdn.id: [file("src/headers.rs", 14, 6)],
]
changedFiles = changed
items = ([approval, question, release, homelab, scaffold, containerfile, statusScreens, nextjs, hyprlock, cdn]
+ olderRecent + settledThreads + [path, image, healthcheck]).map { item in
guard let files = changed[item.id] else { return item }
var item = item
item.changes = ChangeTotals(
files: files.count, additions: files.map(\.additions).reduce(0, +), deletions: files.map(\.deletions).reduce(0, +))
return item
}
children = [homelab.id: [path.id], containerfile.id: [image.id, healthcheck.id]]
func m(_ id: String, _ role: Message.Role, _ minutes: Double, _ text: String) -> Message {