Watch: notification switches, a model list and Ask before acting on new threads, and Ask before acting on a thread
This commit is contained in:
1 parent
3cc7e40852
commit
6bc9cac6ca
14 files changed
+623
-81
No files matched your search
@@ -155,6 +155,8 @@ private let feedItemJSON = """
|
||||
"canStop": true }
|
||||
""")
|
||||
#expect(detail.canStop)
|
||||
// An older gateway leaves out askFirst, so the switch stays hidden.
|
||||
#expect(detail.askFirst == nil)
|
||||
#expect(detail.messages.first?.role == .assistant)
|
||||
#expect(detail.messages.first?.createdAt == Date(timeIntervalSince1970: 1_793_526_000))
|
||||
}
|
||||
@@ -168,6 +170,35 @@ private let feedItemJSON = """
|
||||
let host = try #require(response.hosts.first)
|
||||
#expect(host.projects.count == 2)
|
||||
#expect(host.launchable.map(\.id) == ["a"])
|
||||
#expect(host.providers.isEmpty)
|
||||
#expect(host.projects.first?.defaultModel == nil)
|
||||
}
|
||||
|
||||
@Test func phase2FromDocs() throws {
|
||||
let projects = try decode(ProjectsResponse.self, """
|
||||
{ "hosts": [{ "id": "...", "label": "Mac mini", "status": "online", "error": null,
|
||||
"projects": [{ "id": "a", "title": "wristcode", "model": "claude-opus-5-5", "instanceId": "claudeAgent" }],
|
||||
"providers": [{ "instanceId": "claudeAgent", "name": "Claude", "canAskFirst": true,
|
||||
"models": [{ "slug": "claude-opus-5-5", "name": "Claude Opus 5.5" }] }] }] }
|
||||
""")
|
||||
let host = try #require(projects.hosts.first)
|
||||
#expect(host.projects.first?.defaultModel == ModelRef(instanceId: "claudeAgent", slug: "claude-opus-5-5"))
|
||||
#expect(host.offers(ModelRef(instanceId: "claudeAgent", slug: "claude-opus-5-5")))
|
||||
#expect(!host.offers(ModelRef(instanceId: "codex", slug: "claude-opus-5-5")))
|
||||
|
||||
let detail = try decode(ThreadDetail.self, #"{ "item": \#(feedItemJSON), "messages": [], "canStop": true, "askFirst": false }"#)
|
||||
#expect(detail.askFirst == false)
|
||||
|
||||
let prefs = try decode(NotificationPrefs.self, #"{ "needsMe": true, "finished": true, "failed": true, "subagents": false }"#)
|
||||
#expect(NotificationPrefs.Key.allCases.map { prefs[$0] } == [true, true, true, false])
|
||||
|
||||
let request = try encodeObject(NewThreadRequest(
|
||||
hostId: "h", projectId: "p", text: "t",
|
||||
model: ModelRef(instanceId: "claudeAgent", slug: "claude-sonnet-5-5"), askFirst: true))
|
||||
#expect((request["model"] as? [String: String]) == ["instanceId": "claudeAgent", "slug": "claude-sonnet-5-5"])
|
||||
#expect(request["askFirst"] as? Bool == true)
|
||||
let plain = try encodeObject(NewThreadRequest(hostId: "h", projectId: "p", text: "t"))
|
||||
#expect(plain["model"] == nil && plain["askFirst"] == nil)
|
||||
}
|
||||
|
||||
@Test func smallResponses() throws {
|
||||
@@ -285,6 +316,36 @@ private let feedItemJSON = """
|
||||
== ProjectRef(hostId: "on", projectId: "a"))
|
||||
}
|
||||
|
||||
@Test func newThreadAgentAndOptions() {
|
||||
let claude = AgentProvider(
|
||||
instanceId: "claudeAgent", name: "Claude", canAskFirst: true,
|
||||
models: [.init(slug: "opus", name: "Claude Opus"), .init(slug: "sonnet", name: "Claude Sonnet")])
|
||||
let other = AgentProvider(instanceId: "other", name: "Other", canAskFirst: false, models: [.init(slug: "o1", name: "O1")])
|
||||
let project = ProjectsResponse.Project(id: "p", title: "p", model: "opus", instanceId: "claudeAgent")
|
||||
let host = ProjectsResponse.Host(id: "h", label: "H", status: .online, error: nil, projects: [project], providers: [claude, other])
|
||||
let sonnet = ModelRef(instanceId: "claudeAgent", slug: "sonnet")
|
||||
|
||||
// The last pick wins while the host still offers it, else the project's default.
|
||||
#expect(NewThreadView.agent(on: host, project: project, last: sonnet) == sonnet)
|
||||
#expect(NewThreadView.agent(on: host, project: project, last: ModelRef(instanceId: "claudeAgent", slug: "gone"))
|
||||
== project.defaultModel)
|
||||
#expect(NewThreadView.agent(on: host, project: project, last: nil) == project.defaultModel)
|
||||
|
||||
// The default model is left out so it keeps its options. A provider that cannot ask sends false.
|
||||
let byDefault = NewThreadView.options(project: project, agent: project.defaultModel, provider: claude, askFirst: true)
|
||||
#expect(byDefault.model == nil && byDefault.askFirst == true)
|
||||
let picked = NewThreadView.options(project: project, agent: sonnet, provider: claude, askFirst: false)
|
||||
#expect(picked.model == sonnet && picked.askFirst == false)
|
||||
let o1 = ModelRef(instanceId: "other", slug: "o1")
|
||||
#expect(NewThreadView.options(project: project, agent: o1, provider: other, askFirst: true).askFirst == false)
|
||||
// An older gateway lists no providers, so neither is sent.
|
||||
let old = NewThreadView.options(project: project, agent: nil, provider: nil, askFirst: true)
|
||||
#expect(old.model == nil && old.askFirst == nil)
|
||||
|
||||
#expect(AgentList.shortName("Claude Opus 5.5", provider: "Claude") == "Opus 5.5")
|
||||
#expect(AgentList.shortName("GPT-5.5", provider: "Codex") == "GPT-5.5")
|
||||
}
|
||||
|
||||
@Test func summaryFromFeed() throws {
|
||||
let feed = DemoFixtures.previewFeed()
|
||||
let summary = Summary(feed)
|
||||
|
||||
@@ -73,6 +73,27 @@ struct SnapshotTests {
|
||||
try write(titled("Thread", ThreadView(hostId: "laptop", threadId: "t-status", detail: detail, scrolls: false)), "thread-settle.png")
|
||||
}
|
||||
|
||||
@Test func runningThreadWithAskFirst() async throws {
|
||||
let detail = try await DemoGateway().thread(hostId: "mac-mini", threadId: "t-scaffold")
|
||||
try write(titled("Thread", ThreadView(hostId: "mac-mini", threadId: "t-scaffold", detail: detail, scrolls: false)), "thread.png")
|
||||
}
|
||||
|
||||
@Test func newThreadAndAgentList() async throws {
|
||||
let projects = try await DemoGateway().projects()
|
||||
// The demo opens on Mac mini › wristcode, with Ask before acting on as the mock shows it.
|
||||
UserDefaults.standard.removeObject(forKey: "lastModels")
|
||||
UserDefaults.standard.set(true, forKey: "askFirst")
|
||||
defer { UserDefaults.standard.removeObject(forKey: "askFirst") }
|
||||
try write(titled("New thread", NewThreadView(projects: projects, scrolls: false)), "new-thread.png")
|
||||
|
||||
let host = try #require(projects.hosts.first)
|
||||
let project = try #require(host.projects.first)
|
||||
let list = AgentList(
|
||||
providers: host.providers, selected: project.defaultModel, defaultInstanceId: project.instanceId,
|
||||
scrolls: false) { _ in }
|
||||
try write(titled("Agent", list), "agent-list.png")
|
||||
}
|
||||
|
||||
@Test func statusScreens() async throws {
|
||||
let status = try await DemoGateway().status()
|
||||
try write(titled("Status", StatusView(status: status, scrolls: false)), "status.png")
|
||||
|
||||
Reference in new issue
Block a user