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:
1 parent
f8241e42bb
commit
bfcbb69550
29 files changed
+1100
-137
No files matched your search
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user