Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,16 @@ final class HostAppControlTypesTests: XCTestCase {
// MARK: - HostAppControlInput wire shape

func test_input_decodes_from_tool_discriminator() throws {
// Wire format uses snake_case (matches TOOLS.json input schema and the
// TypeScript HostAppControlPressInput shape). Swift maps to camelCase
// via explicit CodingKey raw values.
let json = #"""
{
"tool": "press",
"app": "com.apple.Safari",
"key": "Return",
"modifiers": ["cmd"],
"durationMs": 100
"duration_ms": 100
}
"""#
let decoded = try JSONDecoder().decode(HostAppControlInput.self, from: Data(json.utf8))
Expand All @@ -118,6 +121,34 @@ final class HostAppControlTypesTests: XCTestCase {
XCTAssertEqual(durationMs, 100)
}

func test_input_drag_decodes_snake_case_coordinates() throws {
// Regression guard for the pre-existing CodingKey bug where the
// snake_case `from_x`/`from_y`/`to_x`/`to_y` wire keys silently
// failed to decode and drag coordinates fell through to undefined
// behavior.
let json = #"""
{
"tool": "drag",
"app": "com.apple.Safari",
"from_x": 10,
"from_y": 20,
"to_x": 100,
"to_y": 200,
"button": "left"
}
"""#
let decoded = try JSONDecoder().decode(HostAppControlInput.self, from: Data(json.utf8))
guard case .drag(let app, let fromX, let fromY, let toX, let toY, let button) = decoded else {
return XCTFail("Expected .drag variant, got \(decoded)")
}
XCTAssertEqual(app, "com.apple.Safari")
XCTAssertEqual(fromX, 10)
XCTAssertEqual(fromY, 20)
XCTAssertEqual(toX, 100)
XCTAssertEqual(toY, 200)
XCTAssertEqual(button, "left")
}

func test_input_unknown_tool_throws() {
let json = #"{"tool": "teleport", "app": "x"}"#
XCTAssertThrowsError(
Expand Down
14 changes: 9 additions & 5 deletions clients/shared/Network/MessageTypes.swift
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Pre-existing: Swift HostAppControlCancel missing conversationId vs TypeScript definition

The Swift HostAppControlCancel struct (line 1890-1898) only has type and requestId, but the TypeScript counterpart at assistant/src/daemon/message-types/host-app-control.ts:116-120 also includes conversationId. Since HostAppControlCancel is Decodable and Swift's JSONDecoder ignores extra keys by default, this won't cause decoding failures — the conversationId is simply dropped. However, if any Swift code ever needs the conversationId from a cancel message, it won't be available. This is a pre-existing issue unrelated to this PR.

(Refers to lines 1890-1898)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -1757,17 +1757,21 @@ public enum HostAppControlInput: Codable, Equatable, Sendable {
case key
case keys
case modifiers
case durationMs
// Wire format uses snake_case for multi-word fields (driven by
// TOOLS.json schema property names). Map explicitly — without these
// raw values, decode silently misses `duration_ms` / `from_x` / etc.
// and hold-durations and drag coordinates fall through to defaults.
case durationMs = "duration_ms"
case steps
case text
case x
case y
case button
case double
case fromX
case fromY
case toX
case toY
case fromX = "from_x"
case fromY = "from_y"
case toX = "to_x"
case toY = "to_y"
case reason
}

Expand Down