ios: smooth Agent entrances and refresh app icon
TestFlight / Build and upload (push) Successful in 1m49s

Grow row height and spacing with the existing entrance animation, remove the Latest overlay while retaining scroll/read/send behavior, and reuse the current PWA icon artwork. Cover occupied-height interpolation and scroll-to-latest navigation in focused checks.
This commit is contained in:
2026-10-16 16:54:46 -07:00
parent f59c7db7bb
commit 671ee98ab1
7 changed files with 118 additions and 37 deletions
+18 -4
View File
@@ -313,7 +313,8 @@ final class AgentUITests: XCTestCase {
XCTAssertTrue(app.staticTexts["History message 160: local simulator fixture."].isHittable)
scroll.swipeDown()
scroll.swipeDown()
XCTAssertTrue(app.buttons["agent-jump-latest"].waitForExistence(timeout: 3))
XCTAssertFalse(app.staticTexts["History message 160: local simulator fixture."].isHittable)
XCTAssertFalse(app.buttons["agent-jump-latest"].exists)
let anchor = try XCTUnwrap(app.staticTexts.matching(NSPredicate(format: "label BEGINSWITH %@", "History message")).allElementsBoundByIndex.first { $0.isHittable })
let before = anchor.frame.minY
try await control(["action": "reply", "content": "Reply while reading earlier history"])
@@ -327,14 +328,27 @@ final class AgentUITests: XCTestCase {
XCTAssertTrue(app.buttons["Load earlier messages"].isHittable)
app.buttons["Load earlier messages"].tap()
// Loading older lazy history may move the viewport. It must still load
// the remaining history and preserve Latest/keyboard behavior below.
// the remaining history and preserve scroll-to-latest/keyboard behavior below.
XCTAssertFalse(app.buttons["Load earlier messages"].exists)
capture("agent-older-history-loaded")
app.buttons["agent-jump-latest"].tap()
XCTAssertTrue(app.staticTexts["Reply while reading earlier history"].isHittable)
let latest = app.staticTexts["Reply while reading earlier history"]
for _ in 0..<30 {
if latest.isHittable { break }
scroll.swipeUp(velocity: .fast)
}
scroll.swipeUp(velocity: .slow)
XCTAssertTrue(latest.isHittable)
composer.tap(); composer.typeText("A multiline draft\nwith a second line\nand a third line")
XCTAssertTrue(app.staticTexts["Reply while reading earlier history"].isHittable)
capture("agent-multiline-keyboard")
// Sending remains the explicit navigation path while reading history.
scroll.swipeDown(velocity: .slow)
scroll.swipeDown(velocity: .slow)
XCTAssertFalse(latest.isHittable)
app.buttons["agent-send"].tap()
let sent = app.staticTexts["A multiline draft\nwith a second line\nand a third line"]
let followed = XCTNSPredicateExpectation(predicate: NSPredicate(format: "hittable == true"), object: sent)
await fulfillment(of: [followed], timeout: 5)
}
func testBackgroundUnreadReadReconciliationAndReplay() async throws {
@@ -41,6 +41,17 @@ final class TranscriptFadeUITests: XCTestCase {
app.scrollViews["agent-transcript"]
}
private func scrollToLatest(_ latest: XCUIElement) {
for _ in 0..<30 {
if latest.isHittable { break }
transcript.swipeUp(velocity: .fast)
}
// Finish at the physical end, including the reply metadata and spacing.
transcript.swipeUp(velocity: .slow)
XCTAssertTrue(latest.isHittable)
XCTAssertFalse(app.buttons["agent-jump-latest"].exists)
}
private func capture(_ name: String) {
let attachment = XCTAttachment(screenshot: XCUIScreen.main.screenshot())
attachment.name = name; attachment.lifetime = .keepAlways; add(attachment)
@@ -146,7 +157,7 @@ final class TranscriptFadeUITests: XCTestCase {
let scroll = transcript
XCTAssertTrue(scroll.exists)
scroll.swipeDown(velocity: .slow)
XCTAssertTrue(app.buttons["agent-jump-latest"].waitForExistence(timeout: 5))
XCTAssertFalse(app.staticTexts["History message 160: local simulator fixture."].isHittable)
capture("agent-content-crossing-shared-fade")
XCTAssertTrue(composer.isHittable)
XCTAssertTrue(app.buttons["agent-attach"].isHittable)
@@ -162,7 +173,8 @@ final class TranscriptFadeUITests: XCTestCase {
XCTAssertTrue(app.staticTexts["History message 160: local simulator fixture."].isHittable)
scroll.swipeDown(velocity: .slow)
scroll.swipeDown(velocity: .slow)
XCTAssertTrue(app.buttons["agent-jump-latest"].waitForExistence(timeout: 5))
XCTAssertFalse(app.staticTexts["History message 160: local simulator fixture."].isHittable)
XCTAssertFalse(app.buttons["agent-jump-latest"].exists)
// Stay below the header's content clearance, where text is readable.
let visibleTop = scroll.frame.minY + 100
let anchor = try XCTUnwrap(app.staticTexts.matching(NSPredicate(format: "label BEGINSWITH %@", "History message")).allElementsBoundByIndex.first {
@@ -171,8 +183,9 @@ final class TranscriptFadeUITests: XCTestCase {
let before = anchor.frame.minY
let stateBefore = try await control(["action": "state"])
try await control(["action": "reply", "content": "Fade regression reply while reading"])
let unread = XCTNSPredicateExpectation(predicate: NSPredicate(format: "label CONTAINS %@", "unread"), object: app.buttons["agent-jump-latest"])
await fulfillment(of: [unread], timeout: 8)
// The removed button no longer exposes unread state. Allow the entrance
// and read debounce to settle, then inspect the fixture's actual cursor.
try await Task.sleep(for: .milliseconds(500))
XCTAssertEqual(anchor.frame.minY, before, accuracy: 2)
let stateAfter = try await control(["action": "state"])
XCTAssertEqual(stateAfter["readCursor"] as? Int, stateBefore["readCursor"] as? Int)
@@ -191,8 +204,8 @@ final class TranscriptFadeUITests: XCTestCase {
XCTAssertFalse(load.exists)
capture("agent-fade-older-history-loaded")
app.buttons["agent-jump-latest"].tap()
let latest = app.staticTexts["Fade regression reply while reading"]
scrollToLatest(latest)
let followed = XCTNSPredicateExpectation(predicate: NSPredicate(format: "hittable == true"), object: latest)
await fulfillment(of: [followed], timeout: 5)
composer.tap()
@@ -217,14 +230,14 @@ final class TranscriptFadeUITests: XCTestCase {
let keyboardRead = try await control(["action": "state"])
XCTAssertEqual(keyboardRead["readCursor"] as? Int, keyboardRead["cursor"] as? Int)
// Interactive dismissal is user navigation; Latest must still return to
// the bottom, and presenting the keyboard again must keep it visible.
// Interactive dismissal is user navigation; scrolling must still return
// to the bottom, and presenting the keyboard again must keep it visible.
let dismissStart = scroll.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.9))
let dismissEnd = app.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.98))
dismissStart.press(forDuration: 0.1, thenDragTo: dismissEnd)
let dismissed = XCTNSPredicateExpectation(predicate: NSPredicate(format: "exists == false"), object: app.keyboards.firstMatch)
await fulfillment(of: [dismissed], timeout: 5)
if app.buttons["agent-jump-latest"].exists { app.buttons["agent-jump-latest"].tap() }
scrollToLatest(keyboardReply)
XCTAssertTrue(keyboardReply.isHittable)
composer.tap()
XCTAssertTrue(app.keyboards.firstMatch.waitForExistence(timeout: 5))