Skip to content

Migrate Demo to ExyteChat 3.x and unblock the build on Xcode 27 - #448

Merged
nezhyborets merged 9 commits into
mainfrom
444-demo-app-fails-to-build-on-newer-xcode-exytechat-2x-needs-a-3x-migration
Sep 21, 2026
Merged

nezhyborets merged 9 commits into
mainfrom
444-demo-app-fails-to-build-on-newer-xcode-exytechat-2x-needs-a-3x-migration

Conversation

@nezhyborets

Copy link
Copy Markdown
Collaborator

Summary

Closes #444.

Demo/Demo.xcodeproj was failing to link on Xcode 27 with errors like:

Undefined symbols for architecture arm64:
  "variable initialization expression of ExyteChat.ChatView.(__isShowingMenu ...) : SwiftUI.LazyState<Swift.Bool>", referenced from: ...

Root cause

Two separate issues stacked here:

  1. ExyteChat's ChatView trips a Swift 6.4/Xcode 27 compiler bug. ChatView declares its @State properties in ChatView.swift but its designated initializer in a different file (ChatBuilderParameters.swift). That split-file shape is exactly what swiftlang/swift#91700 describes: the compiler can't link the "variable initialization expression" symbol for a @State property's default value when the initializer lives in another file. This affects ExyteChat 2.x (originally pinned here) and 3.x equally — it isn't a version issue.
  2. Bumping to ExyteChat 3.x (needed regardless, since 2.x is unmaintained relative to current Xcode) surfaces three unrelated, real API breaks in DemoChat.

Changes

  • Bump exyte/Chat from 2.5.7 to 3.3.0 and fix the fallout in DemoChat:
    • Drop .messageUseMarkdown(true) in DetailView.swift / ResponsesChatDetailView.swift — ChatView 3.x always applies markdown attributes to message text now, so the modifier no longer exists.
    • Drop Hashable from ResponsesStore.ConversationTurnExyteChat.Message lost Hashable in 3.x (Equatable only now), and the conformance wasn't used anywhere in DemoChat.
    • Pin exyte/AnchoredPopup to >=1.2.2 explicitly. MediaPicker 3.4.x calls PopupParameters.displayMode(_:), added in AnchoredPopup 1.2.0, but MediaPicker's own manifest still understates its requirement as from: "1.1.3". Without an explicit pin, SwiftPM's resolver can legitimately land on 1.1.3 and fail with "value of type 'PopupParameters' has no member 'displayMode'".
  • Point the Chat dependency at a fork branch, nezhyborets/Chat@macpaw-xcode27-init-fix, which moves ChatView's designated init into the same file as its @State declarations — the workaround the upstream Swift issue describes. This is also open upstream as exyte/Chat#302; once that (or the underlying Apple compiler fix) ships, Package.swift should go back to .package(url: "https://github.com/exyte/Chat.git", from: "3.3.0") (comment left in place noting this).

Test plan

  • xcodebuild -project Demo/Demo.xcodeproj -scheme Demo -destination "generic/platform=iOS Simulator" clean build — succeeds with zero errors (verified as a clean build, not incremental, since an incremental build can mask this specific linker failure by reusing a stale binary)
  • Installed and launched Demo in the iOS Simulator — renders correctly, no crash

Demo/Demo.xcodeproj fails to link on Xcode 27 because ExyteChat's ChatView
declares its @State properties in one file and its designated initializer
in another, which trips a Swift 6.4/Xcode 27 compiler bug
(swiftlang/swift#91700): the linker can't find the "variable initialization
expression" symbol for each @State property's default value.

- Bump exyte/Chat from 2.5.7 to 3.3.0, which required three follow-up fixes:
  - drop .messageUseMarkdown(true): ChatView 3.x always applies markdown
    attributes to message text now, the modifier no longer exists
  - drop Hashable from ResponsesStore.ConversationTurn: ExyteChat.Message
    lost Hashable in 3.x (Equatable only), and the conformance was unused
  - pin exyte/AnchoredPopup to >=1.2.2 explicitly: MediaPicker 3.4.x calls
    PopupParameters.displayMode(_:), added in AnchoredPopup 1.2.0, but
    MediaPicker's manifest still understates its own requirement as
    `from: "1.1.3"`, so SwiftPM could otherwise resolve an incompatible
    1.1.3 and fail to build
- Point Chat at a fork branch (nezhyborets/Chat@macpaw-xcode27-init-fix)
  that moves ChatView's designated init into the same file as its @State
  declarations, working around the Xcode 27 linker bug. Upstreamed as
  exyte/Chat#302. Revert to the tagged release once that lands and a fixed
  Xcode toolchain ships.

Verified with a full clean build (not incremental) and by installing and
launching Demo in the iOS Simulator.

Fixes #444

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

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.

Copilot review overview

🟡 Changes recommended

Pin the mutable fork dependency to an immutable commit or release tag.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Medium severity

Open (1)
What changed in this PR

Migrates DemoChat to ExyteChat 3.x, updates incompatible APIs and dependency locks, and applies an Xcode 27 linker workaround.

Changes:

  • Removes obsolete markdown configuration and Hashable conformance.
  • Pins AnchoredPopup and refreshes resolved dependencies.
  • Uses a forked Chat branch containing the compiler workaround.

Review note: Pin the fork to an immutable commit or release tag for reproducibility.

File Description
Demo/​DemoChat/​Sources/​UI/​ResponsesChatDetailView.swift Removes obsolete markdown modifier.
Demo/​DemoChat/​Sources/​UI/​DetailView.swift Removes obsolete markdown modifier.
Demo/​DemoChat/​Sources/​ResponsesStore.swift Removes incompatible Hashable conformance.
Demo/​DemoChat/​Package.swift Updates Chat and AnchoredPopup dependencies.
Demo/​DemoChat/​Package.resolved Refreshes Swift package versions.
Demo/​Demo.xcodeproj/​project.xcworkspace/​xcshareddata/​swiftpm/​Package.resolved Synchronizes Xcode dependency resolution.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread Demo/DemoChat/Package.swift Outdated

Copilot AI left a comment

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.

Copilot review overview

🔵 Needs a closer look

It relies on a forked dependency and compiler-specific workaround requiring human validation.

Review effort: Lite
Findings: None

Resolved since last review (1)

exyte/Chat#302 merged and shipped in 3.3.3, so the fork branch used as a
stopgap is no longer needed. Point back at the upstream package.

Verified with a full clean build of Demo on Xcode 27.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@nezhyborets

Copy link
Copy Markdown
Collaborator Author

Update: exyte/Chat#302 merged and shipped in 3.3.3, so I switched this PR off the fork branch and back to the upstream package (from: "3.3.3"). Re-verified with a full clean build on Xcode 27 — still succeeds with zero linker errors.

The fork branch (nezhyborets/Chat@macpaw-xcode27-init-fix) is no longer referenced by anything here and can be deleted whenever convenient.

nezhyborets and others added 3 commits September 21, 2026 17:00
Streaming requests cancelled the task as soon as a >=400 response came
in, before the body was ever read, so OpenAIError.statusError only
carried response headers and discarded OpenAI's actual error message
(e.g. an invalid model name). Buffer the error body instead and decode
it the same way the non-streaming path does, falling back to
statusError only when the body isn't decodable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
StreamingSessionErrorHandlingTests used HTTPURLResponse and
URLSession.ResponseDisposition without the FoundationNetworking import
that every other file touching these types already guards with, so
Linux CI (Swift 5.10/6.0/6.3) failed to compile the test target.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Prompted by a real CI failure: a new test file used HTTPURLResponse /
URLSession.ResponseDisposition without the FoundationNetworking import
Linux needs, which compiled fine on macOS but broke all three Linux CI
jobs. Add a standing reminder so future changes check for this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

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.

Copilot review overview

🟡 Changes recommended

Critical dependency and streaming error-handling issues remain, and the new test needs an explicit Foundation import.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 3 High severity

Open (3)

Comment thread Demo/DemoChat/Package.swift
Comment thread Sources/OpenAI/Private/Streaming/StreamingSession.swift
Comment thread Tests/OpenAITests/StreamingSessionErrorHandlingTests.swift
nezhyborets and others added 3 commits September 21, 2026 20:58
The response-disposition assertions captured and mutated a local var
from inside the @sendable completionHandler closure, which Linux
Swift 5.10's stricter checker rejects as a data race (macOS/newer
toolchains only warn). Route the value through an instance array
instead, matching how the other callback closures in this file
already record their results.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
decodeErrorResponse indexed element 0 after decoding [GeminiAPIErrorResponse]
without checking it was non-empty. A body that happens to decode as [] (e.g.
literally "[]") traps with an index-out-of-range fatal error instead of
falling through to the statusError fallback. This was reachable from the
non-streaming path already, and became reachable from the streaming path too
once it started decoding error bodies. Use .first instead of [0].

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
CI takes several minutes per push; pushing after every small commit means
paying that wait repeatedly. Commit locally as work progresses, but only
push when explicitly asked to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

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.

Copilot review overview

🟡 Changes recommended

Streaming error bodies can grow without bounds and require a size limit or cancellation path.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (3)

Comment thread Sources/OpenAI/Private/Streaming/StreamingSession.swift
Once a >=400 response was accepted, every subsequent chunk was appended
to errorData with no size limit and no cancellation path, so a
malformed or malicious server/proxy could force unbounded memory
growth by never ending the error body. Cap it at 256 KB (real API
error bodies are a few hundred bytes) and cancel the task once
exceeded; didCompleteWithError already falls back to statusError when
the buffered body doesn't decode, so a truncated body degrades
gracefully into the existing bounded-failure behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

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.

Copilot review overview

🟡 Changes recommended

Streaming error handling can exceed its size cap, and production callbacks are not guaranteed to execute in order.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (1)

Comment thread Sources/OpenAI/Private/Streaming/StreamingSession.swift

Copilot AI left a comment

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.

Copilot review overview

🔵 Needs a closer look

Streaming error buffering can exceed its advertised 256 KiB limit for oversized chunks.

Review effort: Lite
Findings: None

Resolved since last review (1)

@nezhyborets
nezhyborets merged commit b160c25 into main Sep 21, 2026
13 checks passed
@nezhyborets
nezhyborets deleted the 444-demo-app-fails-to-build-on-newer-xcode-exytechat-2x-needs-a-3x-migration branch September 21, 2026 22:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Demo app fails to build on newer Xcode: ExyteChat 2.x needs a 3.x migration

3 participants