Repository navigation
Implement custom navigation, logging, and UI enhancements - #3
Conversation
…and settings views including a new in-app log viewer and diagnostic logging infrastructure
…instead of a tab-based view
…ling for chat streams
…t media processing
…d visual properties
…rk mode and update Settings UI profile header and list styling.
…es and normalize quiet audio input for Whisper transcriber
|
Warning Review limit reached
More reviews will be available in 26 minutes and 36 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughThis PR introduces a tabbed ChangesSynapLink App Shell, Home Screen, Logging, and Inference
Sequence Diagram(s)sequenceDiagram
participant User
participant HomeView
participant TopicSuggester
participant ChatSession
participant AppRouter
participant RootTabView
participant ChatScreen
User->>HomeView: onAppear
HomeView->>TopicSuggester: loadIfNeeded()
TopicSuggester->>ChatSession: suggestTopics(count:5)
ChatSession-->>TopicSuggester: [String]
TopicSuggester-->>HomeView: topics (skeleton → rows)
User->>HomeView: tap topic row
HomeView->>AppRouter: openNewChat()
AppRouter->>ChatSession: startNewChat()
AppRouter->>AppRouter: presentedChat = newChat
AppRouter-->>RootTabView: presentedChat updated
RootTabView->>ChatScreen: slide-up overlay
HomeView->>ChatSession: send seeded topic message
User->>ChatScreen: tap close
ChatScreen->>AppRouter: closeChat()
AppRouter->>AppRouter: presentedChat = nil
RootTabView-->>HomeView: overlay dismissed
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (3)
SynapLink/Presentation/Components/Shimmer.swift (1)
17-22: ⚡ Quick winRespect Reduce Motion when applying shimmer.
Gate shimmer animation with
accessibilityReduceMotionand fall back to a static placeholder when motion reduction is enabled.Also applies to: 52-57
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@SynapLink/Presentation/Components/Shimmer.swift` around lines 17 - 22, The shimmer animation in the body method does not respect the user's accessibility reduce motion setting, which can cause discomfort for users who have enabled this preference. Use the `@Environment` property wrapper to access the accessibilityReduceMotion variable and conditionally apply either the shimmer animation with the AnimatedMask modifier when reduce motion is disabled, or return a static placeholder view (without the animation and onAppear modifier) when reduce motion is enabled. Apply this same conditional logic at all locations where shimmer animations are defined.SynapLink/Presentation/Components/ChatInputBar.swift (1)
36-36: 💤 Low valueRemove unused
canSubmitproperty.The
canSubmitcomputed property is defined but never used in the updated button logic (lines 80-88). The button disabled states now directly usehasDraftandcanSendinstead.🧹 Proposed cleanup
- /// A turn can be sent with text, an attachment, or both. - private var canSubmit: Bool { hasDraft || hasAttachment } -🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@SynapLink/Presentation/Components/ChatInputBar.swift` at line 36, The canSubmit computed property in ChatInputBar is no longer used since the button logic now directly references hasDraft and canSend instead. Remove the entire canSubmit property definition that computes hasDraft || hasAttachment to eliminate dead code and reduce maintenance burden.SynapLink/Data/Repositories/AppearanceSettings.swift (1)
44-59: 💤 Low valueConsider marking
AppearanceSettingsas@MainActorfor explicit thread-safety guarantees.While
UserDefaultsand@Observableprovide their own synchronization, and SwiftUI views implicitly run on the main thread, adding@MainActorto the class would make the main-thread requirement explicit and prevent accidental background-thread access.🧵 Suggested annotation
+@MainActor `@Observable` final class AppearanceSettings {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@SynapLink/Data/Repositories/AppearanceSettings.swift` around lines 44 - 59, The AppearanceSettings class is missing explicit thread-safety guarantees required for classes that interact with UserDefaults and SwiftUI. Add the `@MainActor` attribute to the AppearanceSettings class declaration (currently marked with `@Observable`) to explicitly require main-thread access and prevent accidental background-thread usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@SynapLink/Core/Utils/LogStore.swift`:
- Around line 60-64: The static stamp DateFormatter is not thread-safe and is
being accessed after the lock is released in the record() method, creating
potential race conditions with concurrent calls. Replace the DateFormatter-based
stamp variable with ISO8601DateFormatter (which is thread-safe per Apple
documentation), or alternatively move the stamp formatter access inside the lock
protection to ensure thread-safe access. If using ISO8601DateFormatter, update
the date formatting call site (around line 76 where the lock is released) to use
the new formatter's output method instead of the dateString(from:) call.
In `@SynapLink/Data/DataSources/Inference/ChatSession.swift`:
- Around line 284-286: The code logs raw user-derived text content
(resolved.promptText in the media turn logging block and transcript at another
location) to persistent diagnostics, creating a privacy and compliance risk.
Instead of logging the actual text content, replace these with metadata such as
character counts, token counts, or simple boolean flags to indicate that content
was present, while completely removing the raw text from the log statement.
- Around line 365-367: The guard statement in the suggestTopics method only
checks the isGenerating flag but does not check the isCreatingImage flag. Add a
check for !isCreatingImage to the guard condition on line 366 alongside the
existing !isGenerating check. This will prevent the method from calling
ensureEngineLoaded() while image generation is active, avoiding unnecessary
memory pressure during image creation operations.
In `@SynapLink/Presentation/Components/StreamChunk.swift`:
- Around line 234-244: The `imageOnly` function uses `firstIndex(of: ")")` to
find the closing parenthesis of the URL, but this incorrectly stops at the first
`)` character, breaking URLs that contain parentheses like
`https://host/a_(b).png`. Instead of finding the first closing parenthesis, find
the correct one by counting parenthesis nesting depth starting from the opening
`(` after `]` and finding where the depth returns to zero, or alternatively by
scanning backwards from the end of the line to find the last `)` character,
which should be the actual closing parenthesis of the markdown syntax. Update
the logic where `close` is assigned to implement one of these approaches.
In `@SynapLink/Presentation/Components/StreamChunkView.swift`:
- Around line 163-176: The AsyncImage in the body property automatically fetches
remote images from untrusted markdown URLs when the view appears, potentially
exposing client metadata to malicious hosts. Replace the automatic AsyncImage
loading with an explicit user-triggered mechanism (such as a "Tap to load image"
button) or implement a trusted-host allowlist that validates the URL domain
before calling AsyncImage. Only proceed with AsyncImage instantiation if the
host is verified as safe, otherwise show a placeholder or button prompting user
action instead of automatically fetching.
In `@SynapLink/Presentation/Views/Chat/ChatView.swift`:
- Around line 216-230: The onChange observer at line 220 only scrolls to the top
when the last message is a user message, leaving assistant messages unhandled.
Additionally, onAppear at line 226 may fire before chat data is loaded. To fix
this, modify the onChange closure to also scroll to the bottom when the last
message is from the assistant role (not just the user role), ensuring existing
chats that end with an assistant message properly anchor to the bottom. This
way, both user and assistant message changes will trigger appropriate scrolling
behavior.
In `@SynapLink/Presentation/Views/Settings/SettingsView.swift`:
- Around line 211-217: The onTapGesture closure in SettingsView currently
toggles only between .light and .dark appearance modes, making the .system mode
unreachable once changed. Update the toggle logic within the withAnimation block
to cycle through all three modes: .system, .light, and .dark. Replace the
current ternary expression that only handles two states with a conditional
statement (if-else or switch) that checks the current mode value and cycles to
the next mode in sequence, ensuring .system is reachable again after cycling
through the other modes.
---
Nitpick comments:
In `@SynapLink/Data/Repositories/AppearanceSettings.swift`:
- Around line 44-59: The AppearanceSettings class is missing explicit
thread-safety guarantees required for classes that interact with UserDefaults
and SwiftUI. Add the `@MainActor` attribute to the AppearanceSettings class
declaration (currently marked with `@Observable`) to explicitly require
main-thread access and prevent accidental background-thread usage.
In `@SynapLink/Presentation/Components/ChatInputBar.swift`:
- Line 36: The canSubmit computed property in ChatInputBar is no longer used
since the button logic now directly references hasDraft and canSend instead.
Remove the entire canSubmit property definition that computes hasDraft ||
hasAttachment to eliminate dead code and reduce maintenance burden.
In `@SynapLink/Presentation/Components/Shimmer.swift`:
- Around line 17-22: The shimmer animation in the body method does not respect
the user's accessibility reduce motion setting, which can cause discomfort for
users who have enabled this preference. Use the `@Environment` property wrapper to
access the accessibilityReduceMotion variable and conditionally apply either the
shimmer animation with the AnimatedMask modifier when reduce motion is disabled,
or return a static placeholder view (without the animation and onAppear
modifier) when reduce motion is enabled. Apply this same conditional logic at
all locations where shimmer animations are defined.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6f999f6f-13e7-4e57-ad9a-3cfeb9486983
📒 Files selected for processing (30)
SynapLink/App/SynapLinkApp.swiftSynapLink/Core/Engine/synap_sd.cppSynapLink/Core/Utils/LogStore.swiftSynapLink/Core/Utils/SynapLog.swiftSynapLink/Data/DataSources/Audio/VoiceRecorder.swiftSynapLink/Data/DataSources/Inference/ChatSession.swiftSynapLink/Data/DataSources/Inference/ImageGenerator.swiftSynapLink/Data/DataSources/Inference/RuntimeProfile.swiftSynapLink/Data/DataSources/Inference/VisionDescriber.swiftSynapLink/Data/DataSources/Inference/WhisperTranscriber.swiftSynapLink/Data/DataSources/ModelDownload/ModelDownloadManager.swiftSynapLink/Data/DataSources/ModelDownload/SpecialistCatalog.swiftSynapLink/Data/Repositories/AppearanceSettings.swiftSynapLink/Presentation/AppRouter.swiftSynapLink/Presentation/Components/ChatInputBar.swiftSynapLink/Presentation/Components/Shimmer.swiftSynapLink/Presentation/Components/StreamChunk.swiftSynapLink/Presentation/Components/StreamChunkView.swiftSynapLink/Presentation/Components/SynapTabBar.swiftSynapLink/Presentation/RootTabView.swiftSynapLink/Presentation/TopicSuggester.swiftSynapLink/Presentation/Views/Chat/ChatListView.swiftSynapLink/Presentation/Views/Chat/ChatScreen.swiftSynapLink/Presentation/Views/Chat/ChatView.swiftSynapLink/Presentation/Views/Chat/ImageGenSheet.swiftSynapLink/Presentation/Views/Debug/SmokeTestView.swiftSynapLink/Presentation/Views/Home/HomeView.swiftSynapLink/Presentation/Views/ModelLibrary/ModelLibraryView.swiftSynapLink/Presentation/Views/Settings/LogView.swiftSynapLink/Presentation/Views/Settings/SettingsView.swift
| private static let stamp: DateFormatter = { | ||
| let f = DateFormatter() | ||
| f.dateFormat = "HH:mm:ss.SSS" | ||
| return f | ||
| }() |
There was a problem hiding this comment.
DateFormatter is not thread-safe; concurrent record() calls can race.
DateFormatter instances are not thread-safe per Apple documentation. The static stamp formatter is accessed at line 76 after the lock is released, so concurrent calls to record() from different threads can corrupt the formatter's internal state.
Consider using ISO8601DateFormatter (which is thread-safe) or protecting access with a lock.
🔒 Suggested fix using ISO8601DateFormatter
- private static let stamp: DateFormatter = {
- let f = DateFormatter()
- f.dateFormat = "HH:mm:ss.SSS"
- return f
- }()
+ private static let stamp: ISO8601DateFormatter = {
+ let f = ISO8601DateFormatter()
+ f.formatOptions = [.withTime, .withFractionalSeconds, .withColonSeparatorInTime]
+ return f
+ }()Alternatively, use the lock to protect formatter access, or create a formatter per call (slower but safe).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private static let stamp: DateFormatter = { | |
| let f = DateFormatter() | |
| f.dateFormat = "HH:mm:ss.SSS" | |
| return f | |
| }() | |
| private static let stamp: ISO8601DateFormatter = { | |
| let f = ISO8601DateFormatter() | |
| f.formatOptions = [.withTime, .withFractionalSeconds, .withColonSeparatorInTime] | |
| return f | |
| }() |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SynapLink/Core/Utils/LogStore.swift` around lines 60 - 64, The static stamp
DateFormatter is not thread-safe and is being accessed after the lock is
released in the record() method, creating potential race conditions with
concurrent calls. Replace the DateFormatter-based stamp variable with
ISO8601DateFormatter (which is thread-safe per Apple documentation), or
alternatively move the stamp formatter access inside the lock protection to
ensure thread-safe access. If using ISO8601DateFormatter, update the date
formatting call site (around line 76 where the lock is released) to use the new
formatter's output method instead of the dateString(from:) call.
| if !attachments.isEmpty { | ||
| slog("media turn → \(resolved.directMedia.count) direct, user text: \"\(resolved.promptText)\"", .info) | ||
| } |
There was a problem hiding this comment.
Avoid logging raw user/transcript content to persistent diagnostics.
Line 285 and Line 342 log full user-derived text (resolved.promptText, transcript). With current slog behavior, that data is persisted/exportable, which is a privacy/compliance risk. Log metadata (counts/flags) instead of raw content.
🔧 Suggested change
- if !attachments.isEmpty {
- slog("media turn → \(resolved.directMedia.count) direct, user text: \"\(resolved.promptText)\"", .info)
- }
+ if !attachments.isEmpty {
+ slog("media turn → \(resolved.directMedia.count) direct, promptChars=\(resolved.promptText.count)", .info)
+ }
...
- slog("voice transcript: \"\(transcript)\"", transcript.isEmpty ? .warning : .info)
+ slog("voice transcript chars=\(transcript.count)", transcript.isEmpty ? .warning : .info)Also applies to: 342-343
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SynapLink/Data/DataSources/Inference/ChatSession.swift` around lines 284 -
286, The code logs raw user-derived text content (resolved.promptText in the
media turn logging block and transcript at another location) to persistent
diagnostics, creating a privacy and compliance risk. Instead of logging the
actual text content, replace these with metadata such as character counts, token
counts, or simple boolean flags to indicate that content was present, while
completely removing the raw text from the log statement.
| await ensureEngineLoaded() | ||
| guard case .ready = engineState, !isGenerating else { return [] } | ||
|
|
There was a problem hiding this comment.
Block topic suggestion while image generation is active.
Line 366 only checks !isGenerating. If isCreatingImage is true, suggestTopics can still call ensureEngineLoaded(), potentially loading the chat engine during SD generation and spiking memory pressure.
🔧 Suggested change
- guard case .ready = engineState, !isGenerating else { return [] }
+ guard case .ready = engineState, !isGenerating, !isCreatingImage else { return [] }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| await ensureEngineLoaded() | |
| guard case .ready = engineState, !isGenerating else { return [] } | |
| await ensureEngineLoaded() | |
| guard case .ready = engineState, !isGenerating, !isCreatingImage else { return [] } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SynapLink/Data/DataSources/Inference/ChatSession.swift` around lines 365 -
367, The guard statement in the suggestTopics method only checks the
isGenerating flag but does not check the isCreatingImage flag. Add a check for
!isCreatingImage to the guard condition on line 366 alongside the existing
!isGenerating check. This will prevent the method from calling
ensureEngineLoaded() while image generation is active, avoiding unnecessary
memory pressure during image creation operations.
| private static func imageOnly(_ line: String) -> (alt: String, url: String)? { | ||
| //  — url stops at whitespace or ). | ||
| guard line.hasPrefix("!["), let altEnd = line.firstIndex(of: "]"), | ||
| line.index(after: altEnd) < line.endIndex, | ||
| line[line.index(after: altEnd)] == "(" else { return nil } | ||
| let alt = String(line[line.index(line.startIndex, offsetBy: 2)..<altEnd]) | ||
| let afterParen = line[line.index(altEnd, offsetBy: 2)...] | ||
| guard let close = afterParen.firstIndex(of: ")") else { return nil } | ||
| let inside = afterParen[..<close] | ||
| let url = inside.split(separator: " ").first.map(String.init) ?? String(inside) | ||
| return (alt, url) |
There was a problem hiding this comment.
Handle image URLs that contain parentheses.
imageOnly stops at the first ), so valid markdown like .png) gets truncated and fails to render.
Proposed fix
private static func imageOnly(_ line: String) -> (alt: String, url: String)? {
@@
- let afterParen = line[line.index(altEnd, offsetBy: 2)...]
- guard let close = afterParen.firstIndex(of: ")") else { return nil }
+ let afterParen = line[line.index(altEnd, offsetBy: 2)...]
+ var nesting = 0
+ var close: String.Index?
+ var idx = afterParen.startIndex
+ while idx < afterParen.endIndex {
+ let ch = afterParen[idx]
+ if ch == "(" {
+ nesting += 1
+ } else if ch == ")" {
+ if nesting == 0 {
+ close = idx
+ break
+ }
+ nesting -= 1
+ }
+ idx = afterParen.index(after: idx)
+ }
+ guard let close else { return nil }
let inside = afterParen[..<close]
let url = inside.split(separator: " ").first.map(String.init) ?? String(inside)
return (alt, url)
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SynapLink/Presentation/Components/StreamChunk.swift` around lines 234 - 244,
The `imageOnly` function uses `firstIndex(of: ")")` to find the closing
parenthesis of the URL, but this incorrectly stops at the first `)` character,
breaking URLs that contain parentheses like `https://host/a_(b).png`. Instead of
finding the first closing parenthesis, find the correct one by counting
parenthesis nesting depth starting from the opening `(` after `]` and finding
where the depth returns to zero, or alternatively by scanning backwards from the
end of the line to find the last `)` character, which should be the actual
closing parenthesis of the markdown syntax. Update the logic where `close` is
assigned to implement one of these approaches.
| var body: some View { | ||
| if let parsed = URL(string: url), parsed.scheme == "http" || parsed.scheme == "https" { | ||
| AsyncImage(url: parsed) { phase in | ||
| switch phase { | ||
| case .success(let image): | ||
| image.resizable().scaledToFit() | ||
| .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) | ||
| case .failure: | ||
| placeholder | ||
| default: | ||
| ProgressView().frame(maxWidth: .infinity).frame(height: 80) | ||
| } | ||
| } | ||
| } else { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
find . -name "StreamChunkView.swift" -type fRepository: kevinliddel/SynapLink
Length of output: 123
🏁 Script executed:
wc -l ./SynapLink/Presentation/Components/StreamChunkView.swiftRepository: kevinliddel/SynapLink
Length of output: 127
🏁 Script executed:
cat -n ./SynapLink/Presentation/Components/StreamChunkView.swiftRepository: kevinliddel/SynapLink
Length of output: 7579
🏁 Script executed:
find . -name "*.swift" -type f | xargs grep -l "enum StreamChunk" | head -5Repository: kevinliddel/SynapLink
Length of output: 119
🏁 Script executed:
cat -n ./SynapLink/Presentation/Components/StreamChunk.swiftRepository: kevinliddel/SynapLink
Length of output: 11436
🏁 Script executed:
rg "StreamChunkParser.parse" -A 3 -B 3Repository: kevinliddel/SynapLink
Length of output: 1355
🏁 Script executed:
head -50 ./SynapLink/Presentation/Components/MarkdownText.swiftRepository: kevinliddel/SynapLink
Length of output: 1595
🏁 Script executed:
rg "MarkdownText(" -B 2 -A 2 | head -40Repository: kevinliddel/SynapLink
Length of output: 142
🏁 Script executed:
rg "MarkdownText\(" -B 2 -A 2 | head -60Repository: kevinliddel/SynapLink
Length of output: 924
🏁 Script executed:
rg "struct MessageBubble" -A 20Repository: kevinliddel/SynapLink
Length of output: 1831
🏁 Script executed:
rg "MessageBubble\(" -B 1 -A 3 | head -50Repository: kevinliddel/SynapLink
Length of output: 1762
🏁 Script executed:
rg "AsyncImage|URLSession|network|policy|image.*domain|allow.*list|trust" -i | head -40Repository: kevinliddel/SynapLink
Length of output: 625
🌐 Web query:
AsyncImage SwiftUI auto-fetch behavior when URL is provided on init
💡 Result:
In SwiftUI, the AsyncImage view initiates the image fetching process automatically upon appearing in the view hierarchy [1][2]. When you provide a URL during initialization, the view does not start the network request until the view is rendered and appears on screen [1]. Key behavioral details: - Triggered on appear: The download is triggered when the view appears [1][2]. - Lifecycle management: If the view is removed from the view hierarchy (e.g., a row in a scrollable list is scrolled off-screen) before the download completes, the underlying URL request is automatically canceled [3][2]. - Mechanism: It utilizes the shared URLSession instance to perform the network request [4][5]. - Caching: By default, AsyncImage relies on the system's shared URLSession, which handles standard URL loading, including memory caching [3][2]. However, for advanced use cases requiring specific persistent on-disk caching, custom headers, or fine-grained control, developers often implement a custom image loader [3][2]. For scenarios where you need to react to the loading state (e.g., displaying a progress view while loading or handling errors), you should use the phase-based initializer (init(url:scale:transaction:content:)), which provides an AsyncImagePhase enum to track the current state [4][6][7].
Citations:
- 1: https://talk.objc.io/episodes/S01E258-asyncimage
- 2: https://www.swiftyplace.com/blog/asyncimage-load-image-url-swiftui
- 3: https://www.soarias.com/swiftui/how-to-implement-async-image/
- 4: https://developer.apple.com/documentation/SwiftUI/AsyncImage
- 5: https://serialcoder.dev/text-tutorials/swiftui/asyncimage-in-swiftui/
- 6: https://www.hackingwithswift.com/quick-start/swiftui/how-to-load-a-remote-image-from-a-url
- 7: https://sosumi.ai/documentation/swiftui/asyncimage
🏁 Script executed:
rg "URLSession|imageLoader|ImageCache|image.*download" -i | head -30Repository: kevinliddel/SynapLink
Length of output: 232
Avoid automatic remote image fetches for untrusted markdown URLs.
AsyncImage initiates network requests when the view appears, without explicit user interaction. This sends client metadata (IP address, user-agent, request timing) to arbitrary hosts embedded in model-generated markdown, enabling tracking and reconnaissance attacks. Implement explicit user-triggered loading (e.g., "Tap to load image") or require a trusted-host allowlist before calling AsyncImage.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SynapLink/Presentation/Components/StreamChunkView.swift` around lines 163 -
176, The AsyncImage in the body property automatically fetches remote images
from untrusted markdown URLs when the view appears, potentially exposing client
metadata to malicious hosts. Replace the automatic AsyncImage loading with an
explicit user-triggered mechanism (such as a "Tap to load image" button) or
implement a trusted-host allowlist that validates the URL domain before calling
AsyncImage. Only proceed with AsyncImage instantiation if the host is verified
as safe, otherwise show a placeholder or button prompting user action instead of
automatically fetching.
| .onChange(of: session.messages.last?.id) { | ||
| guard let last = session.messages.last else { return } | ||
| // A new user message jumps to the top; a finished reply just | ||
| // settles where it is. | ||
| if last.role == .user { | ||
| withAnimation(.easeOut(duration: 0.25)) { | ||
| proxy.scrollTo("bottom", anchor: .bottom) | ||
| proxy.scrollTo(last.id, anchor: .top) | ||
| } | ||
| } | ||
| } | ||
| .onAppear { | ||
| if let last = session.messages.last { | ||
| proxy.scrollTo(last.id, anchor: .bottom) | ||
| } | ||
| } |
There was a problem hiding this comment.
Preserve initial bottom anchoring after chat data loads.
Line 226 can fire before the target chat messages are populated, and Line 220 only re-scrolls for .user. Existing chats that end with an assistant message can open at the top and never re-anchor.
Proposed fix
@@
`@State` private var specialists = SpecialistManager.shared
`@State` private var viewportHeight: CGFloat = 0
+ `@State` private var didInitialAnchor = false
@@
.onChange(of: session.messages.last?.id) {
guard let last = session.messages.last else { return }
+ if !didInitialAnchor {
+ didInitialAnchor = true
+ proxy.scrollTo(last.id, anchor: .bottom)
+ return
+ }
// A new user message jumps to the top; a finished reply just
// settles where it is.
if last.role == .user {
withAnimation(.easeOut(duration: 0.25)) {
proxy.scrollTo(last.id, anchor: .top)
@@
.onAppear {
if let last = session.messages.last {
+ didInitialAnchor = true
proxy.scrollTo(last.id, anchor: .bottom)
}
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SynapLink/Presentation/Views/Chat/ChatView.swift` around lines 216 - 230, The
onChange observer at line 220 only scrolls to the top when the last message is a
user message, leaving assistant messages unhandled. Additionally, onAppear at
line 226 may fire before chat data is loaded. To fix this, modify the onChange
closure to also scroll to the bottom when the last message is from the assistant
role (not just the user role), ensuring existing chats that end with an
assistant message properly anchor to the bottom. This way, both user and
assistant message changes will trigger appropriate scrolling behavior.
| .onTapGesture { | ||
| withAnimation(.snappy(duration: 0.22)) { mode = isDark ? .light : .dark } | ||
| } | ||
| .accessibilityElement() | ||
| .accessibilityLabel("Theme") | ||
| .accessibilityValue(isDark ? "Dark" : "Light") | ||
| .accessibilityAddTraits(.isButton) |
There was a problem hiding this comment.
Allow users to return to .system appearance mode.
Line 212 only toggles .light/.dark, so once changed, .system (which AppearanceSettings persists/supports) becomes unreachable from UI.
Proposed fix
@@
.onTapGesture {
- withAnimation(.snappy(duration: 0.22)) { mode = isDark ? .light : .dark }
+ withAnimation(.snappy(duration: 0.22)) {
+ switch mode {
+ case .system: mode = (systemScheme == .dark) ? .light : .dark
+ case .light: mode = .dark
+ case .dark: mode = .system
+ }
+ }
}
@@
- .accessibilityValue(isDark ? "Dark" : "Light")
+ .accessibilityValue(mode.label)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@SynapLink/Presentation/Views/Settings/SettingsView.swift` around lines 211 -
217, The onTapGesture closure in SettingsView currently toggles only between
.light and .dark appearance modes, making the .system mode unreachable once
changed. Update the toggle logic within the withAnimation block to cycle through
all three modes: .system, .light, and .dark. Replace the current ternary
expression that only handles two states with a conditional statement (if-else or
switch) that checks the current mode value and cycles to the next mode in
sequence, ensuring .system is reachable again after cycling through the other
modes.
…dio resampler logic, and fixed language tagging in CI pipeline.
This pull request introduces a project-wide structured logging system, improves diagnostics and error handling for inference and image generation, and adds a new feature for suggesting chat topics. The logging system now routes logs through both the OS and an in-app log viewer, making it easier to debug on-device issues. Additional safeguards and clearer error messages have been added to image generation, and the chat session now logs key events and can suggest topics for the home screen. Several minor usability and reliability improvements are included as well.
Logging and Diagnostics Improvements
slog) that routes logs to bothos.Loggerand a thread-safe in-appLogStore, enabling export and in-app viewing of logs for easier debugging of on-device issues. (SynapLink/Core/Utils/SynapLog.swift,SynapLink/Core/Utils/LogStore.swift) [1] [2]ChatSessionandImageGenerator, such as model loading, engine state transitions, errors, and media handling, to improve traceability and diagnostics. (SynapLink/Data/DataSources/Inference/ChatSession.swift,SynapLink/Data/DataSources/Inference/ImageGenerator.swift) [1] [2] [3] [4] [5] [6] [7] [8]SynapLink/App/SynapLinkApp.swift)Image Generation Reliability and Error Handling
SynapLink/Data/DataSources/Inference/ImageGenerator.swift,SynapLink/Data/DataSources/Inference/ImageGenError.swift) [1] [2]SynapLink/Data/DataSources/Inference/ImageGenerator.swift) [1] [2]New Features
suggestTopicsmethod toChatSessionto generate and parse diverse, short topics for surfacing on the home screen, enhancing user experience with dynamic suggestions. (SynapLink/Data/DataSources/Inference/ChatSession.swift)Audio and Model Compatibility
.measurementto.defaultto ensure proper microphone gain for voice recordings, fixing an issue with inaudible recordings and empty transcripts on iPhone. (SynapLink/Data/DataSources/Audio/VoiceRecorder.swift)SynapLink/Data/DataSources/Inference/RuntimeProfile.swift) [1] [2]Other Improvements
RootTabViewinstead ofChatListView, likely as part of a new navigation structure. (SynapLink/App/SynapLinkApp.swift)SynapLink/Core/Engine/synap_sd.cpp)Summary by CodeRabbit
New Features
Bug Fixes
Improvements