ios: hide raw tool entries in Agent transcript
This commit is contained in:
@@ -203,6 +203,23 @@ struct AgentRoomState: Equatable, Sendable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter only the transcript projection, before windowing. Keep the room's raw
|
||||||
|
// messages and cursors intact for upserts, replay, unread state and activity.
|
||||||
|
struct AgentTranscript {
|
||||||
|
let messages: [Message]
|
||||||
|
let startIndex: Int
|
||||||
|
var displayed: [Message] { Array(messages.dropFirst(startIndex)) }
|
||||||
|
|
||||||
|
init(messages: [Message], startID: String? = nil, limit: Int = 100) {
|
||||||
|
let visible = messages.filter {
|
||||||
|
$0.role != .tool && $0.metadata?.objectValue?["kind"]?.stringValue != "tool_call"
|
||||||
|
}
|
||||||
|
self.messages = visible
|
||||||
|
startIndex = startID.flatMap { id in visible.firstIndex(where: { $0.id == id }) }
|
||||||
|
?? max(0, visible.count - limit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The transcript latch is independent of the truthful sidebar/worker activity.
|
// The transcript latch is independent of the truthful sidebar/worker activity.
|
||||||
struct AgentPresentation {
|
struct AgentPresentation {
|
||||||
private(set) var suppressed = false
|
private(set) var suppressed = false
|
||||||
|
|||||||
@@ -22,9 +22,10 @@ struct SybilAgentView: View {
|
|||||||
}
|
}
|
||||||
var body: some View {
|
var body: some View {
|
||||||
let rendered = room.snapshot
|
let rendered = room.snapshot
|
||||||
let allMessages = room.messages
|
let transcript = AgentTranscript(messages: room.messages, startID: historyStartID, limit: historyLimit)
|
||||||
let startIndex = historyStartID.flatMap { id in allMessages.firstIndex(where: { $0.id == id }) } ?? max(0, allMessages.count - historyLimit)
|
let allMessages = transcript.messages
|
||||||
let displayed = Array(allMessages.dropFirst(startIndex))
|
let startIndex = transcript.startIndex
|
||||||
|
let displayed = transcript.displayed
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@@ -51,13 +52,7 @@ struct SybilAgentView: View {
|
|||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.padding(.vertical, 6)
|
.padding(.vertical, 6)
|
||||||
}
|
}
|
||||||
Group {
|
AgentMessageRow(message: message, retry: { room.retry(message.id) })
|
||||||
if message.isToolCallLog {
|
|
||||||
AgentToolCard(message: message)
|
|
||||||
} else {
|
|
||||||
AgentMessageRow(message: message, retry: { room.retry(message.id) })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(GeometryReader { geometry in
|
.background(GeometryReader { geometry in
|
||||||
Color.clear.preference(key: AgentRowFrames.self, value: [message.id: geometry.frame(in: .named("agent-scroll"))])
|
Color.clear.preference(key: AgentRowFrames.self, value: [message.id: geometry.frame(in: .named("agent-scroll"))])
|
||||||
})
|
})
|
||||||
@@ -110,8 +105,8 @@ struct SybilAgentView: View {
|
|||||||
.map { AgentReadingAnchor(id: $0.key, offset: $0.value.minY) }
|
.map { AgentReadingAnchor(id: $0.key, offset: $0.value.minY) }
|
||||||
let anchor = restoreAnchor ?? (!atLatest && !isScrolling ? previousAnchor : nil)
|
let anchor = restoreAnchor ?? (!atLatest && !isScrolling ? previousAnchor : nil)
|
||||||
if let anchor, let frame = frames[anchor.id], abs(frame.minY - anchor.offset) > 1 {
|
if let anchor, let frame = frames[anchor.id], abs(frame.minY - anchor.offset) > 1 {
|
||||||
// Restore the exact pixel offset after insertion, tool
|
// Restore the exact pixel offset after insertion or text
|
||||||
// expansion or text reflow. Normalize the saved frames to
|
// reflow. Normalize the saved frames to
|
||||||
// the requested offset so the next layout cannot undo it.
|
// the requested offset so the next layout cannot undo it.
|
||||||
let requested = scrollMetrics.offset + frame.minY - anchor.offset
|
let requested = scrollMetrics.offset + frame.minY - anchor.offset
|
||||||
let target = min(max(requested, -scrollMetrics.topInset), max(-scrollMetrics.topInset, scrollMetrics.contentHeight - scrollMetrics.height + scrollMetrics.bottomInset))
|
let target = min(max(requested, -scrollMetrics.topInset), max(-scrollMetrics.topInset, scrollMetrics.contentHeight - scrollMetrics.height + scrollMetrics.bottomInset))
|
||||||
@@ -260,57 +255,6 @@ private struct AgentMessageRow: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AgentToolCard: View {
|
|
||||||
var message: Message
|
|
||||||
@State private var expanded = false
|
|
||||||
var body: some View {
|
|
||||||
if let metadata = message.toolCallMetadata {
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
Button {
|
|
||||||
expanded.toggle()
|
|
||||||
} label: {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
ToolCallActivityChip(metadata: metadata, fallbackContent: message.content, createdAt: message.createdAt, compactLayout: true)
|
|
||||||
.allowsHitTesting(false)
|
|
||||||
Spacer(minLength: 0)
|
|
||||||
Image(systemName: expanded ? "chevron.down" : "chevron.right")
|
|
||||||
.font(.system(size: 13, weight: .semibold))
|
|
||||||
}
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
.accessibilityIdentifier("agent-tool-\(metadata.toolName ?? "tool")")
|
|
||||||
.accessibilityValue(expanded ? "Expanded" : "Collapsed")
|
|
||||||
if expanded {
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
|
||||||
if let args = metadata.args { detail("Input", text: pretty(args)) }
|
|
||||||
if let error = metadata.error { detail("Error", text: error) }
|
|
||||||
if let result = metadata.result { detail("Result", text: pretty(result)) }
|
|
||||||
else if let preview = metadata.resultPreview { detail("Result", text: preview) }
|
|
||||||
if metadata.args == nil && metadata.result == nil && metadata.resultPreview == nil && metadata.error == nil {
|
|
||||||
Text(message.content).font(.sybil(.caption))
|
|
||||||
}
|
|
||||||
}.padding(.top, 8).frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.foregroundStyle(SybilTheme.textMuted)
|
|
||||||
.padding(8)
|
|
||||||
.background(SybilTheme.surface.opacity(0.8), in: RoundedRectangle(cornerRadius: 14))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private func detail(_ title: String, text: String) -> some View {
|
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
|
||||||
Text(title).font(.sybil(.caption, weight: .semibold)).accessibilityIdentifier("agent-tool-\(title.lowercased())")
|
|
||||||
Text(text).font(.system(size: 12, design: .monospaced)).textSelection(.enabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private func pretty(_ value: JSONValue) -> String {
|
|
||||||
if let text = value.stringValue { return text }
|
|
||||||
let encoder = JSONEncoder(); encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
||||||
return (try? encoder.encode(value)).map { String(decoding: $0, as: UTF8.self) } ?? ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animate both occupied height and content, so incoming rows push older history up.
|
// Animate both occupied height and content, so incoming rows push older history up.
|
||||||
private struct AgentGrowingLayout: Layout {
|
private struct AgentGrowingLayout: Layout {
|
||||||
var progress: CGFloat
|
var progress: CGFloat
|
||||||
|
|||||||
@@ -97,6 +97,161 @@ private func eventually(_ predicate: () -> Bool) async -> Bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct AgentTests {
|
struct AgentTests {
|
||||||
|
@Test @MainActor func mixedLoadedHistoryFiltersOnlyToolRows() async throws {
|
||||||
|
var user = agentMessage("user", entry: "1", role: .user)
|
||||||
|
user.content = "Explain the tool_call and tool result."
|
||||||
|
let call = agentMessage("call", entry: "2", role: .tool, info: ["status": .string("initiated")])
|
||||||
|
var reply = agentMessage("reply", entry: "3")
|
||||||
|
reply.content = "The search tool failed; here is what you can do."
|
||||||
|
// Role-only results still need hiding even without tool-call metadata.
|
||||||
|
var result = agentMessage("result", entry: "4", role: .tool)
|
||||||
|
result.metadata = nil
|
||||||
|
let markedCall = agentMessage("marked-call", entry: "5", info: ["kind": .string("tool_call")])
|
||||||
|
var plainReply = agentMessage("plain-reply", entry: "6")
|
||||||
|
plainReply.metadata = nil
|
||||||
|
let failure = agentMessage("failure", entry: "7", info: ["status": .string("failed"), "error": .string("Please try again")])
|
||||||
|
let terminal = agentMessage("terminal", entry: "8", role: .tool, info: ["status": .string("failed"), "error": .string("Raw tool failure"), "resultPreview": .string("Raw result")])
|
||||||
|
let history = [user, call, reply, result, markedCall, plainReply, failure, terminal].enumerated().map { index, message in
|
||||||
|
var message = message
|
||||||
|
message.createdAt.addTimeInterval(Double(index))
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
let client = TestAgentClient(pages: ["0": agentPage(Array(history.dropLast()), cursor: "7", more: true), "7": agentPage([history[7]], cursor: "8")])
|
||||||
|
let room = SybilAgentRoom(); room.configure(client: client, storage: MemoryOutbox().storage)
|
||||||
|
defer { room.stop() }
|
||||||
|
#expect(await eventually { room.connection == "Connected" })
|
||||||
|
#expect(await client.requestedCursors() == ["0", "7"])
|
||||||
|
#expect(await client.connectedAfters() == ["8"])
|
||||||
|
let transcript = AgentTranscript(messages: room.messages)
|
||||||
|
#expect(transcript.displayed == [history[0], history[2], history[5], history[6]])
|
||||||
|
#expect(room.snapshot.room.messages == history)
|
||||||
|
#expect(room.messages.count == 8)
|
||||||
|
#expect(room.snapshot.room.cursor == "8")
|
||||||
|
#expect(room.snapshot.room.readCursor == "0")
|
||||||
|
#expect(room.unreadCount == 2)
|
||||||
|
#expect(room.snapshot.entranceIDs.isEmpty)
|
||||||
|
// Shared regular-chat tool metadata remains available and unchanged.
|
||||||
|
#expect(terminal.isToolCallLog)
|
||||||
|
#expect(terminal.toolCallMetadata?.resultPreview == "Raw result")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test func transcriptWindowsAndAnchorsUseVisibleRows() {
|
||||||
|
var raw: [Message] = [], visible: [Message] = []
|
||||||
|
for index in 1...160 {
|
||||||
|
var message = agentMessage("reply-\(index)", entry: "\(index * 2)")
|
||||||
|
message.createdAt.addTimeInterval(Double(index * 2) * 86_400)
|
||||||
|
var tool = agentMessage("tool-\(index)", entry: "\(index * 2 + 1)", role: .tool)
|
||||||
|
// Tools on separate dates must not introduce their own separators.
|
||||||
|
tool.createdAt = message.createdAt.addingTimeInterval(86_400)
|
||||||
|
raw += [message, tool]; visible.append(message)
|
||||||
|
}
|
||||||
|
let initial = AgentTranscript(messages: raw)
|
||||||
|
#expect(initial.startIndex == 60)
|
||||||
|
#expect(initial.displayed == Array(visible.suffix(100)))
|
||||||
|
let anchorID = initial.displayed.first?.id
|
||||||
|
let hidden = agentMessage("late-tool", entry: "400", role: .tool)
|
||||||
|
let updated = AgentTranscript(messages: raw + [hidden], startID: anchorID)
|
||||||
|
#expect(updated.displayed == initial.displayed)
|
||||||
|
let earlierID = initial.messages[max(0, initial.startIndex - 100)].id
|
||||||
|
let expanded = AgentTranscript(messages: raw, startID: earlierID, limit: 200)
|
||||||
|
#expect(expanded.displayed == visible)
|
||||||
|
#expect(expanded.displayed.firstIndex(where: { $0.id == anchorID }) == 60)
|
||||||
|
let hiddenAnchor = AgentTranscript(messages: raw, startID: "tool-160")
|
||||||
|
#expect(hiddenAnchor.displayed == initial.displayed)
|
||||||
|
let onlyTools = AgentTranscript(messages: raw.filter { $0.role == .tool })
|
||||||
|
#expect(onlyTools.messages.isEmpty)
|
||||||
|
#expect(onlyTools.displayed.isEmpty)
|
||||||
|
#expect(onlyTools.startIndex == 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test @MainActor func liveToolUpsertsPreserveRawHistoryCursorsAndActivity() async throws {
|
||||||
|
let user = agentMessage("user", entry: "1", role: .user)
|
||||||
|
let reply = agentMessage("reply", entry: "2")
|
||||||
|
let client = TestAgentClient(pages: ["0": agentPage([user, reply], cursor: "2")])
|
||||||
|
let room = SybilAgentRoom(); room.configure(client: client, storage: MemoryOutbox().storage)
|
||||||
|
defer { room.stop() }
|
||||||
|
#expect(await eventually { room.connection == "Connected" })
|
||||||
|
let queued = agentMessage("queued", entry: "0", role: .user, info: ["status": .string("queued")])
|
||||||
|
var activity = AgentActivity()
|
||||||
|
activity.isThinking = true; activity.backgroundWorkers = 2; activity.queued = 1
|
||||||
|
activity.queuedMessages = [queued]
|
||||||
|
activity.wait = .init(due: 1_800_000_000_000, reason: "Waiting for background work")
|
||||||
|
activity.error = "A worker needs attention"
|
||||||
|
try room.receive(.activity("root", activity)); room.flush()
|
||||||
|
#expect(await eventually { room.snapshot.isTyping })
|
||||||
|
let visible = AgentTranscript(messages: room.messages).displayed
|
||||||
|
#expect(Set(visible.map(\.id)) == ["user", "reply", "queued"])
|
||||||
|
let start = agentMessage("tool", entry: "3", role: .tool, info: ["status": .string("initiated"), "toolName": .string("search"), "args": .object(["query": .string("fixture")]), "startedAt": .string("start")])
|
||||||
|
let completed = agentMessage("tool", entry: "4", role: .tool, info: ["status": .string("completed"), "resultPreview": .string("Raw preview")])
|
||||||
|
let result = agentMessage("tool", entry: "5", role: .tool, info: ["status": .string("completed"), "result": .object(["matches": .number(3)])])
|
||||||
|
for (message, cursor) in [(start, "3"), (completed, "4"), (result, "5"), (start, "3")] {
|
||||||
|
try room.receive(.messages(agentPage([message], cursor: cursor))); room.flush()
|
||||||
|
#expect(AgentTranscript(messages: room.messages).displayed == visible)
|
||||||
|
#expect(room.snapshot.room.messages.count == 3)
|
||||||
|
#expect(room.unreadCount == 1)
|
||||||
|
#expect(room.snapshot.room.readCursor == "0")
|
||||||
|
#expect(room.snapshot.isTyping)
|
||||||
|
#expect(room.snapshot.room.activity == activity)
|
||||||
|
}
|
||||||
|
let tool = try #require(room.snapshot.room.messages.first(where: { $0.id == "tool" })?.toolCallMetadata)
|
||||||
|
#expect(tool.status == "completed")
|
||||||
|
#expect(tool.args == .object(["query": .string("fixture")]))
|
||||||
|
#expect(tool.resultPreview == "Raw preview")
|
||||||
|
#expect(tool.result == .object(["matches": .number(3)]))
|
||||||
|
#expect(tool.startedAt == "start")
|
||||||
|
#expect(room.snapshot.room.cursor == "5")
|
||||||
|
// The rendered snapshot's cursor includes hidden tool updates. A later
|
||||||
|
// reply must remain unread when only that captured cursor is acknowledged.
|
||||||
|
room.isSelected = true
|
||||||
|
let capturedCursor = room.snapshot.room.cursor
|
||||||
|
let lateReply = agentMessage("late-reply", entry: "6")
|
||||||
|
let lateTool = agentMessage("late-tool", entry: "7", role: .tool)
|
||||||
|
try room.receive(.messages(agentPage([lateReply, lateTool], cursor: "7"))); room.flush()
|
||||||
|
room.markRead(renderedCursor: capturedCursor)
|
||||||
|
#expect(await eventually { room.snapshot.room.readCursor == "5" })
|
||||||
|
#expect(room.snapshot.room.cursor == "7")
|
||||||
|
#expect(room.unreadCount == 1)
|
||||||
|
#expect(AgentTranscript(messages: room.messages).displayed == visible + [lateReply])
|
||||||
|
#expect(!room.snapshot.isTyping)
|
||||||
|
#expect(room.isBusy)
|
||||||
|
let socket = try #require(await client.latestSocket())
|
||||||
|
await socket.push(#"{"type":"error","error":"Room connection unavailable"}"#)
|
||||||
|
#expect(await eventually { room.error == "Room connection unavailable" })
|
||||||
|
#expect(AgentTranscript(messages: room.messages).displayed == visible + [lateReply])
|
||||||
|
#expect(room.snapshot.room.messages.count == 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test @MainActor func failedSendDetailsAndRetryStayVisibleWithHiddenTools() async throws {
|
||||||
|
let tool = agentMessage("tool", entry: "1", role: .tool)
|
||||||
|
let client = TestAgentClient(pages: ["0": agentPage([tool], cursor: "1")])
|
||||||
|
await client.setFailures(submit: true)
|
||||||
|
let memory = MemoryOutbox()
|
||||||
|
let room = SybilAgentRoom(); room.configure(client: client, storage: memory.storage)
|
||||||
|
defer { room.stop() }
|
||||||
|
#expect(await eventually { room.connection == "Connected" })
|
||||||
|
#expect(room.send("Please retry the search tool"))
|
||||||
|
#expect(await eventually { room.outbox.first?.status == "failed" })
|
||||||
|
let failed = try #require(AgentTranscript(messages: room.messages).displayed.first)
|
||||||
|
let requestID = try #require(room.outbox.first?.requestID)
|
||||||
|
#expect(failed.role == .user)
|
||||||
|
#expect(failed.content == "Please retry the search tool")
|
||||||
|
#expect(failed.metadata?.objectValue?["status"] == .string("failed"))
|
||||||
|
#expect(failed.metadata?.objectValue?["error"] == .string("Lost acknowledgement"))
|
||||||
|
#expect(AgentTranscript(messages: room.messages).displayed.count == 1)
|
||||||
|
#expect(memory.read().first?.requestID == requestID)
|
||||||
|
await client.setFailures()
|
||||||
|
room.retry(failed.id)
|
||||||
|
#expect(await eventually { room.outbox.first?.status == "queued" })
|
||||||
|
let retried = try #require(AgentTranscript(messages: room.messages).displayed.first)
|
||||||
|
#expect(retried.id == failed.id)
|
||||||
|
#expect(retried.createdAt == failed.createdAt)
|
||||||
|
#expect(retried.metadata?.objectValue?["status"] == .string("queued"))
|
||||||
|
#expect(retried.metadata?.objectValue?["error"] == nil)
|
||||||
|
#expect(await client.submittedIDs() == [requestID, requestID])
|
||||||
|
#expect(room.snapshot.room.messages == [tool])
|
||||||
|
#expect(room.messages.count == 2)
|
||||||
|
}
|
||||||
|
|
||||||
@Test func cursorAndMalformedFrames() throws {
|
@Test func cursorAndMalformedFrames() throws {
|
||||||
#expect(try AgentCursor.normalize("00012") == "12")
|
#expect(try AgentCursor.normalize("00012") == "12")
|
||||||
#expect(try AgentCursor.advance("9", "10") == "10")
|
#expect(try AgentCursor.advance("9", "10") == "10")
|
||||||
|
|||||||
+5
-2
@@ -26,8 +26,11 @@ Multiple messages can be queued while the Agent works. The local outbox is saved
|
|||||||
atomically under Application Support before submission and is scoped to the API
|
atomically under Application Support before submission and is scoped to the API
|
||||||
base URL. An interrupted send stays on its original bubble with **Retry**, retaining
|
base URL. An interrupted send stays on its original bubble with **Retry**, retaining
|
||||||
its request ID and timestamp across relaunch. A canonical server echo confirms it
|
its request ID and timestamp across relaunch. A canonical server echo confirms it
|
||||||
even if the HTTP acknowledgement is lost. Tool cards expand to show input, result,
|
even if the HTTP acknowledgement is lost. Raw tool calls and results are hidden
|
||||||
and error details. Attachments and model selection belong to ordinary chats.
|
in the Agent transcript, including loaded history and live updates. History windows,
|
||||||
|
date separators and scroll anchors use visible messages; raw merged history and
|
||||||
|
cursors remain intact. Send failures, room errors and activity indicators remain
|
||||||
|
visible. Attachments, model selection and tool cards belong to ordinary chats.
|
||||||
|
|
||||||
Agent contracts remain in `../docs/api/rest.md` and
|
Agent contracts remain in `../docs/api/rest.md` and
|
||||||
`../docs/api/streaming-chat.md`; this client requires no backend deployment.
|
`../docs/api/streaming-chat.md`; this client requires no backend deployment.
|
||||||
|
|||||||
@@ -55,11 +55,11 @@ final class AgentUITests: XCTestCase {
|
|||||||
XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS %@", "Finished: echo wins fixture")).firstMatch.waitForExistence(timeout: 12))
|
XCTAssertTrue(app.staticTexts.containing(NSPredicate(format: "label CONTAINS %@", "Finished: echo wins fixture")).firstMatch.waitForExistence(timeout: 12))
|
||||||
XCTAssertFalse(app.buttons["agent-retry"].exists)
|
XCTAssertFalse(app.buttons["agent-retry"].exists)
|
||||||
composer.tap(); app.swipeDown()
|
composer.tap(); app.swipeDown()
|
||||||
let failures = app.buttons.matching(identifier: "agent-tool-fixture_error")
|
XCTAssertFalse(app.buttons["agent-tool-fixture_search"].exists)
|
||||||
failures.element(boundBy: failures.count - 1).tap()
|
XCTAssertFalse(app.buttons["agent-tool-fixture_error"].exists)
|
||||||
XCTAssertTrue(app.staticTexts["agent-tool-input"].waitForExistence(timeout: 3))
|
XCTAssertFalse(app.staticTexts["Inspecting the fixture"].exists)
|
||||||
XCTAssertTrue(app.staticTexts["agent-tool-error"].exists)
|
XCTAssertFalse(app.staticTexts["Fixture unavailable"].exists)
|
||||||
capture("agent-tool-error")
|
capture("agent-hidden-tools")
|
||||||
let state = try await control(["action": "state"])
|
let state = try await control(["action": "state"])
|
||||||
XCTAssertEqual((state["requests"] as? [String: Any])?.count, 4)
|
XCTAssertEqual((state["requests"] as? [String: Any])?.count, 4)
|
||||||
}
|
}
|
||||||
@@ -71,25 +71,22 @@ final class AgentUITests: XCTestCase {
|
|||||||
capture("agent-reduce-motion")
|
capture("agent-reduce-motion")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testExpandedToolCollapseToShortHistory() async throws {
|
func testToolResultsStayHiddenLiveAndAfterReload() async throws {
|
||||||
try await control(["action": "long-tool"])
|
try await control(["action": "long-tool"])
|
||||||
let tool = app.buttons["agent-tool-fixture_long"]
|
try await control(["action": "reply", "content": "Reply after a hidden tool result"])
|
||||||
XCTAssertTrue(tool.waitForExistence(timeout: 5))
|
XCTAssertTrue(app.staticTexts["Reply after a hidden tool result"].waitForExistence(timeout: 5))
|
||||||
tool.tap()
|
XCTAssertTrue(app.staticTexts["Reply after a hidden tool result"].isHittable)
|
||||||
XCTAssertTrue(app.staticTexts["agent-tool-result"].waitForExistence(timeout: 3))
|
XCTAssertFalse(app.buttons["agent-tool-fixture_long"].exists)
|
||||||
let scroll = app.scrollViews.firstMatch
|
XCTAssertFalse(app.staticTexts["Local tool output"].exists)
|
||||||
for _ in 0..<12 {
|
XCTAssertFalse(app.staticTexts["agent-tool-result"].exists)
|
||||||
if tool.isHittable { break }
|
|
||||||
scroll.swipeDown()
|
|
||||||
}
|
|
||||||
XCTAssertTrue(tool.isHittable)
|
|
||||||
tool.tap()
|
|
||||||
XCTAssertEqual(tool.value as? String, "Collapsed")
|
|
||||||
try await control(["action": "reply", "content": "Reply after collapsing a tall tool"])
|
|
||||||
XCTAssertTrue(app.staticTexts["Reply after collapsing a tall tool"].waitForExistence(timeout: 5))
|
|
||||||
XCTAssertTrue(app.staticTexts["Reply after collapsing a tall tool"].isHittable)
|
|
||||||
XCTAssertFalse(app.buttons["agent-jump-latest"].exists)
|
XCTAssertFalse(app.buttons["agent-jump-latest"].exists)
|
||||||
capture("agent-collapsed-tool-short-history")
|
capture("agent-hidden-tool-live")
|
||||||
|
app.terminate(); app.launch(); openAgent()
|
||||||
|
XCTAssertTrue(app.staticTexts["Reply after a hidden tool result"].waitForExistence(timeout: 5))
|
||||||
|
XCTAssertFalse(app.buttons["agent-tool-fixture_long"].exists)
|
||||||
|
XCTAssertFalse(app.staticTexts["Local tool output"].exists)
|
||||||
|
XCTAssertFalse(app.staticTexts["agent-tool-result"].exists)
|
||||||
|
capture("agent-hidden-tool-history")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTabletSidebarAndInactiveChat() async throws {
|
func testTabletSidebarAndInactiveChat() async throws {
|
||||||
|
|||||||
Reference in New Issue
Block a user