Conversation
7066c5a removed `ConversationLiveDetailRetentionPolicy`, which tracked the in-progress item and fed `shouldPreserveRichDetail` so the running tool call stayed open. Nothing replaced it. Command executions kept their in-flight expansion inline (`commandDefaultExpanded` returns `data.isInProgress`), but tool calls did not — `defaultExpanded` only considered `isFailed`. Display mode defaults to `.collapsed`, so a running tool call rendered collapsed and silent and only surfaced once `ItemCompleted` delivered the whole payload. Reasoning and assistant text stream down a different path, which is why a turn looked like it reasoned fine and then froze on every tool call. `liveDetailStatus` survived as dead code with zero call sites — the in-progress signal was still computed and thrown away. `defaultExpanded` now takes `isInProgress` and honours it in `.collapsed`, mirroring commands. Completed-and-not-failed still collapses, which is what 7066c5a actually wanted. The three call sites pass flags rather than a status because they hand in two different enums: `ToolCallStatus` for card models and `AppOperationStatus` for the MCP and image-generation rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Owner
Author
|
Closing as duplicate: the same correctness fix is already on |
Older Pi/Local Studio bridges emit item/started and item/completed notifications without the mandatory startedAtMs/completedAtMs timestamps that v0.129 made required, and commandExecution items with an empty cwd that AbsolutePathBuf rejects. Upstream strict-decodes and silently drops the entire notification, taking tool calls/results with it. Normalize at the raw JSONL boundary before RemoteAppServerClient decodes. Also register SavedServerStoreTests in the iOS test target Sources phase.
…on edges Root cause: AppModel.snapshot (@observable) was reassigned on every streaming text token, causing every view that read it in body to re-render per-token. The home screen, top-level shell, conversation screen, sessions list, and project picker all stalled during streaming. Fix B (AppModel): route .threadStreamingDelta through a coalescer that accumulates text per (thread, item, kind) and flushes at ~8fps (120ms). StreamingRendererCoordinator.appendDelta stays immediate so the visible streaming bubble remains smooth at token rate. Final delta flushed on turn completion (threadMetadataChanged/threadItemChanged), thread removal, and full resync so no token is lost. Removed dead applyThreadStreamingDelta. Fix A1 (HomeNavigationView): pinnedThreadHydrationSignature moved from a body-read computed property (reading appModel.snapshot) to a debounced HomeDashboardModel property. .onChange(of: appModel.snapshot?.activeThread) replaced with debounced homeDashboardModel.activeThread. Fix A2 (ContentView): new OverlayProjectionModel observes snapshotRevision via withObservationTracking + 100ms debounce and publishes only petAvatarState, petAvatarMessage, pendingApproval. standardOverlays reads the projection instead of appModel.snapshot. .onChange(of: appModel.snapshot) replaced with .onChange(of: snapshotRevision) + handleSnapshotRevisionChange(). Fix A3 (ConversationDestinationScreen): collapsed 5 per-token onChange handlers into 2 (snapshotRevision + composerPrefillRequest). Removed dead pendingUserInputsForThread and relevantServerSnapshot computed props. Fix C (SessionsModel): added 120ms debounce mirroring HomeDashboardModel's scheduleObservedRefresh pattern so SessionsDerivation.build stops re-running per token. Fix D (HomeDashboardView): visibleSessions.map { ... } onChange signals (re-allocated + stringified per body eval) replaced with precomputed visibleHydrationSignature/visibleActivitySignature on HomeDashboardModel. Fix E (ProjectPickerSheet): per-row appModel.isLocalServer (N observation edges) replaced with a precomputed localServerIds: Set<String> param. Verified: make ios-sim-fast BUILD SUCCEEDED, 219 iOS tests passed (0 failures), app installs and launches on simulator without checksum crash.
ConversationView (highest impact — main chat screen on-screen during streaming): - supportsTurnPagination: was a body-read computed property reading appModel.snapshot.serverSnapshot(for:).capabilities. Now precomputed in ConversationScreenModel.refreshState (non-body context) and passed as a param. ConversationView.body no longer reads appModel.snapshot. - resolveTargetLabel: was a private func reading appModel.snapshot. resolvedAgentTargetLabel(for:serverId:). Now precomputed as a closure in ConversationScreenModel that captures sessionSummaries at refresh time. Passed as a param — no body observation edge. - ConversationInputBar.hasFixedFullAccess: was reading appModel.snapshot.threads.first(...).agentRuntimeKind. Now reads from the precomputed composer snapshot field. ConversationInfoView: - thread/server computed properties read appModel.snapshot in body (heroSection, statusColor, serverInfoSection, serverChartsSection). Replaced with @State resolvedThread/resolvedServer refreshed from .onAppear + .onChange(of: appModel.snapshotRevision). Body now observes @State, not appModel.snapshot. - Removed dead code: allServerThreads (never referenced), statusLabel (never referenced). DiscoveryView: - .onChange(of: appModel.snapshot) re-rendered the entire discovery screen on every snapshot bump. Replaced with .onChange(of: appModel.snapshotRevision). DirectoryPickerView: - selectedServerSnapshot read appModel.snapshot.servers.first(where:) in body (via selectedServerIsLocal, canSelectPath, .disabled). Replaced with localServerIds/browseableServerIds Set<String> params precomputed in HomeDashboardModel and SessionsModel debounced refresh. Verified: make ios-sim-fast BUILD SUCCEEDED, 219 unit tests + 6 UI tests passed (0 failures).
InlineHandoffView.thread was a computed property reading appModel.snapshot?.threadSnapshot(for:) directly in body, creating a per-token observation edge during voice sessions. Replaced with @State resolvedThread refreshed from .onAppear + .onChange(of: appModel.snapshotRevision), matching the ConversationInfoView pattern. Identified by glm-5.2 code review via pi consultation. Verified: make ios-sim-fast BUILD SUCCEEDED, 219 unit tests passed.
HeaderView, ConversationModelPickerPanel, and ConversationToolbarControls all read appModel.snapshot?.serverSnapshot(for:) in body. Since these views are in the conversation toolbar, they were re-rendering on every coalesced snapshot bump (~8fps during streaming). Added serverSnapshot published property to ConversationScreenModel (computed in refreshState, non-body context) and passed it as a param to all three toolbar views. Verified: make ios-sim-fast BUILD SUCCEEDED, 219 unit tests passed.
HomeModelChip (home composer bar) read appModel.snapshot in body via server and metadataLoadID computed properties, creating observation edges that re-rendered the home composer on every coalesced snapshot bump during background streaming. Added serverSnapshotsById dictionary to HomeDashboardModel (debounced, computed in refreshState from rawServers) and passed it through HomeDashboardView and NewThreadHeroView as a param. HomeModelChip now receives the precomputed server snapshot directly. Verified: make ios-sim-fast BUILD SUCCEEDED, 219 unit tests passed.
1. flushPendingStreamingDeltas: re-arm coalesced timer after a targeted flush if deltas remain for other threads. Without this, concurrent streaming threads (subagents/handoff) could lose the tail of text when one thread's flush cancelled the shared timer. 2. flushPendingStreamingDeltas: fall back to scheduleThreadSnapshotRefresh for batches whose thread/item disappeared between enqueue and flush. The old per-token code had this fallback; the coalescer was silently dropping the text. 3. enqueueStreamingDelta: clear pending deltas for a thread when falling back to a full-thread refresh, preventing token duplication if the refresh lands with full item text while a batch is still pending. 4. OverlayProjectionModel: observe PetOverlayController.isLoading/isDragging in addition to snapshotRevision. Pet drag/loading state changes independently of snapshot bumps and was going stale. Also increment observationGeneration on bind() so stale tracking closures from a prior bind are properly invalidated. 5. ConversationScreenModel: reset serverSnapshot to nil in the early- return guard so switching threads doesn't leave stale server data. Identified by claude code (opus) review via ~/.local/bin/claude. Verified: make ios-sim-fast BUILD SUCCEEDED, 219 unit tests passed.
SubagentCardView resolvedLabel/resolvedThreadKey/liveStatus all read appModel.snapshot in body via agentRowView called per-row from ForEach. Each row created N observation edges that re-rendered the entire card on every coalesced snapshot bump. Added resolveThreadKey and resolveLiveStatus closures to ConversationScreenModel (precomputed in refreshState from captured sessionSummaries), passed through ConversationView → ConversationMessageList → ConversationTimelineView → SubagentCardView. SubagentCardView.body no longer reads appModel.snapshot. Verified: make ios-sim-fast BUILD SUCCEEDED, 219 unit tests passed.
Removed 12 confirmed-unused private functions, computed properties, stored properties, and types across 11 iOS source files. Each was verified as having zero call sites outside its declaration. Total: -137 lines. Removed: - LitterApp.openServerSessions(_:) — uncalled member of open* family - AppModel.applyThreadCommandExecutionUpdated — uncalled reducer - ConversationInfoView.timestampLabel(_:timestamp:) — unused view builder - ConversationView.lastTurnIsUserOnly — unused computed property - ConversationView.isStreamingLastTurn — unused computed property - HomeComposerView.isDisabled — unused computed property - HomeDashboardView.SessionCanvasLine.metaLine — unused view builder - HomeSessionsScrollView.peakBlurProgress — leftover from removed feature - SubagentCardView.isInProgress — unused computed property - WallpaperAdjustView.isServerOnly — unused computed property - ConversationTimelineView.DiffLine struct + nested Kind enum - NearbyMacPairing.NICodingError enum — never thrown Verified: make ios-sim-fast BUILD SUCCEEDED, 219 unit tests passed.
Deep performance audit found 5 issues on the conversation scroll and home list paths. All fixed: 1. ConversationMessageList.mergedRenderableTurns: the O(n) build-key hash (hashing every turn's id/renderDigest/isLive/isCollapsedByDefault on every body eval) was defeating the renderedTurns cache. Now reads the cached renderedTurns directly — the cache is already maintained by applyTranscriptTurns/syncTranscriptTurns. Falls back to source- derived merge only when the cache is empty (first render). 2. ConversationMessageList.shouldShowScrollToBottom: distanceFromBottom was read in body, causing the entire message-list body (including the LazyVStack diff setup) to re-evaluate on every scroll frame. Now driven by a boolean @State (showScrollToBottomButton) that only flips at the threshold, so scroll geometry changes don't trigger body re-evaluation unless the button needs to appear/disappear. 3. HeaderView.sessionModelLabel + ConversationModelPickerPanel: residual appModel.availableModels(for:) calls (which read appModel.snapshot) in the header body path. Replaced with server?.availableModels ?? [] using the already-passed server param. Header no longer re-renders at ~8fps during streaming. 4. Extensions.relativeDate: RelativeDateTimeFormatter was allocated on every call. Hoisted to a file-level static let (matching the SessionsScreen pattern). Affects every home card + search row render. 5. ConversationTimelineView.timelineContent: VStack → LazyVStack so expanding a turn with many items doesn't eagerly materialize all rows. Verified: make ios-sim-fast BUILD SUCCEEDED, 219 unit tests passed.
Added PerfTracker utility with os_signpost + LLog for timing critical
paths in DEBUG builds. Instrumented applySnapshot, flushStreamingDeltas,
handleStoreUpdate, startTurn, ConversationMessageList.body,
ConversationScreenModel.refreshState, and HomeDashboardModel.refreshState.
Added 14 XCTest measure{} performance tests across two suites:
PerformanceMeasurementTests (8 tests):
- TranscriptTurn.build: small (10 turns), large (200 turns), live stream
- Merge exploration turns (100 turns)
- StreamingAssistantRenderCache: 1000 tokens, stable-prefix reuse
- ConversationScreenModel projection (100 turns)
- relativeDate formatter (100 timestamps)
InteractionTimingTests (6 tests):
- Full render pipeline: 200 items, 1000 items
- Conversation projection: 800 hydrated items
- Streaming projection: 500 token increments
- TranscriptTurn.build: 2500 items (stress)
- relativeDate: 200 timestamps
All 233 tests pass (219 original + 14 new).
Live simulator perf logs show:
- applySnapshot: 1.47ms cold, 0.12ms avg steady-state
- flushStreamingDeltas: 0.00ms avg
- Streaming projection (500 tokens): 0.037ms per call avg
When sessions load on the home screen, each row's UIHostingController height was measured synchronously via layoutIfNeeded() + sizeThatFits(). With 10+ sessions this caused 10+ synchronous SwiftUI layout passes on the main thread, freezing keyboard input until all measurements completed. Now heightAnchor() returns cheap static fallback heights during the initial layout pass and schedules the real measurement on the next runloop via DispatchQueue.main.async. This lets the keyboard stay responsive while session rows measure their natural heights in the background. Pinch paths still measure synchronously for smooth tracking.
- Support up to 4 image attachments in the composer (PhotosPicker with maxSelectionCount, camera capture stacks instead of replacing) - Lift home composer attachment state onto a stable @observable reference so the sheet/picker/importer/cover chain is not rebuilt per keystroke - Add per-image thumbnail strip with remove buttons in the composer - Enable text selection in assistant/system message bubbles - Wire image rendering for http(s) sources referenced by the model - Bump version to 2.1.0 (iOS + Android) - Add built-in agent catalog seeding AgentMetadataStore - Honor include_turns=false on metadata-only thread reads - SSH bridge + reconnect hardening for mobile transports Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…mented bubbles - UserBubble.imageRequest now accepts http(s):// sources, delegating the network fetch to Nuke's URLSession pipeline instead of silently dropping remote URLs - Extract ExternalBrowserURLHandler so both LitterMarkdownView and AssistantBlocksBubble share the same open-in-Safari handler - Add .environment(\.openURL) to AssistantBlocksBubble so links in segmented assistant messages are tappable Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The home composer only supported @plugin autocomplete; $skill mentions were available in the conversation composer but not when starting a new thread from home. This mirrors the conversation composer's pattern: - Add skill state (skills, skillsLoading, activeDollarToken, showSkillPopup) - Load skills via client.listSkills on first $ token activation - Add $ token detection in refreshHomePopup alongside existing @ handling - Add HomeSkillAutocompletePopup with fuzzy matching + loading state - Collect SkillMentionSelections on send and pass as AppUserInput.skill additionalInputs alongside existing plugin mentions Also promotes fuzzyScore, isMentionQueryValid, and extractMentionNames from private to internal so the home composer can reuse them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…=false Two fixes for Local Studio skill inheritance: 1. runtime_for_request: catalog RPCs (SkillsList, PluginList, etc.) fell through to a hard-coded "codex" default. On a server that only has the local-studio runtime, this sent the request to a non-existent "codex" channel, which silently fell back to the default transport that doesn't implement skills/list. Now mirrors the local-studio special case from runtime_for_thread_start. 2. apply_thread_read_response: now accepts include_turns from the caller. When include_turns=false, embedded turns in the response are treated as non-authoritative and the store's existing paged items + cursor are preserved, even if a bridge violates the contract by returning turns anyway. This prevents metadata-only thread reads from clobbering hydrated history. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
10 body-level appModel.snapshot reads across 4 views were creating observation edges that triggered re-renders on every store update. Each is now cached in @State and refreshed via .onChange(of: appModel.snapshotRevision), matching the pattern already used in ConversationInfoView and HomeDashboardModel. - SettingsView: localServer + connectedServers (3 reads) - PetSettingsView: connectedServers (1 read) - RealtimeVoiceScreen: server used by shouldShowApiKeyPrompt (1 read) - SubagentDetailSheet: threadSnapshot + title resolution (4 reads) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- MacCommands/SessionShortcutsMenu: cache sessionSummaries in @State refreshed via .onChange(of: appModel.snapshotRevision) — removes the last body-level appModel.snapshot read in the codebase - RealtimeVoiceScreen: wrap transcript ForEach in LazyVStack so only visible transcript entries render during long voice sessions - HeaderView: hoist modelProviderGroups(for:) into a local let in both InlineModelSelectorView and ModelSelectorSheet bodies so the Dictionary(grouping:) + sort runs once per body pass instead of re-evaluating inside the LazyVStack content builder on each keystroke - AppsListView: move sortedApps from a body-evaluated computed property to @State refreshed on appear + store.apps change Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI reads this file at release time; it still described the 2.0 Local Studio identity work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…code StreamingAssistantBubble.shouldUseSegmentedRenderer called MessageContentBridge.containsMath(text) on every body evaluation, which dispatches a full extractSegmentsTyped Rust FFI parse over the entire streaming text. For long replies this is O(n) per token with FFI marshalling overhead. Now routed through StreamingAssistantRenderCache.containsMath, which memoizes the result keyed by itemId+text and shares the existing LRU eviction — body re-evaluations that don't change the text (display-mode toggles, layout passes) hit the cache instead of re-parsing. UserBubble had no Equatable conformance, so SwiftUI couldn't skip its body when the parent ConversationTimelineItemRow re-diffed with unchanged user rows. Added == comparing text/images/compact/maxVisibleCharacters. ResolvedChatImageView.svgAspectRatio compiled an NSRegularExpression from a compile-time literal pattern on every invocation. Hoisted to a static let so compilation happens once. Removed dead code: - ConversationTimelineItemRow.renderCache (stored but never read) - ConversationItem.liveDetailStatus extension (defined, never called) - InlineSelectableMarkdownMessage stored properties (markdown/style/bodySize/ codeSize written at construction but never read in body) - LitterMarkdownStyleVariant.cacheKey extension (defined, never called) make ios-sim-fast: BUILD SUCCEEDED xcodebuild test: 233 tests, 0 failures
…umbnails + memoize exploration entries + memoize tool-result decode ConversationTimelineItemRow declared @Environment(ThemeManager.self) and read themeManager.themeVersion inside assistantRow() (called from body) to pass it as themeVersion: to StreamingAssistantBubble. But StreamingAssistantBubble stores themeVersion and never reads it — the prop is dead. The @Environment read created a ThemeManager observation edge on every visible assistant row, so any themeVersion bump (theme switch, appearance change) re-evaluated all assistant row bodies. Removed the @Environment declaration, the themeManager.themeVersion read, and the dead themeVersion prop/init param from StreamingAssistantBubble. WallpaperManager.generateThumbnail had no cache (unlike generateWallpaper which caches into imageCache). WallpaperSelectionView's ForEach over 85 themes called generateThumbnail per row on every body re-evaluation, so each tap on a thumbnail triggered 85 synchronous Core Graphics renders. Added an @ObservationIgnored thumbnailCache keyed by entry.slug. ConversationExplorationGroupRow.body computed explorationEntries (a flatMap over items) three times per body pass: once in body, once in collapsedPreviewScrollSignature, and once in isActive. Hoisted all three into local lets computed once in body, and removed the now-deed computed properties (summaryText, collapsedPreviewScrollSignature, isActive). CrossServerToolResultView.body called decode() on every evaluation, running JSONSerialization + JSONDecoder on data.contentSummary each time. Moved decoding into init so it runs once per view instance. make ios-sim-fast: BUILD SUCCEEDED xcodebuild test: 233 tests, 0 failures
LitterMarkdownView declared @State private var debugSettings = DebugSettings.shared and read debugSettings.enabled && debugSettings.disableMarkdown in body. DebugSettings is @observable and its computed properties read overrides (a non-@ObservationIgnored var), so each read registered an observation edge from every visible message row to the DebugSettings singleton — N edges scaling with visible message count. Marked overrides as @ObservationIgnored (it's backed by UserDefaults and never needs SwiftUI invalidation) and added a static isMarkdownDisabled accessor that reads the ignored dict without creating an edge. LitterMarkdownView.body now uses DebugSettings.isMarkdownDisabled instead of the @State observable, removing the per-row edge entirely. The debug settings UI still mutates enabled/disableMarkdown via the observable computed setters; since overrides is now @ObservationIgnored, those mutations won't trigger SwiftUI re-renders automatically. This is acceptable — the debug-disable-markdown toggle is a developer-only feature that requires restarting the view to take effect, and the existing fontPreferenceObserver.revision invalidation already handles font/theme changes that would re-render the markdown. make ios-sim-fast: BUILD SUCCEEDED xcodebuild test: 233 tests, 0 failures
Owner
Author
|
Landed in #319 (merged as |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose
Fix severe per-token UI lag across the entire app. The home screen, sessions list, project picker, conversation composer, and top-level shell all stalled during streaming because
AppModel.snapshot(@Observable) was reassigned on every streaming text token, causing every view reading it inbodyto re-render per-token.Key changes
Fix B — Coalesce streaming deltas (AppModel)
.threadStreamingDeltanow accumulates text inpendingStreamingDeltasand flushes at ~8fps (120ms coalescing) instead of mutatingsnapshotper token.StreamingRendererCoordinator.appendDeltastays immediate so the visible streaming bubble remains smooth at token rate.threadMetadataChanged/threadItemChanged), thread removal, and full resync — no token lost.applyThreadStreamingDeltamethod.Fix A1 — HomeNavigationView
pinnedThreadHydrationSignaturemoved from body-read computed property to debouncedHomeDashboardModelproperty (120ms)..onChange(of: appModel.snapshot?.activeThread)→.onChange(of: homeDashboardModel.activeThread).Fix A2 — ContentView.standardOverlays
OverlayProjectionModelobservessnapshotRevisionviawithObservationTracking+ 100ms debounce, publishes onlypetAvatarState,petAvatarMessage,pendingApproval.standardOverlaysreads the projection instead ofappModel.snapshot..onChange(of: appModel.snapshot)→.onChange(of: snapshotRevision)+handleSnapshotRevisionChange().Fix A3 — ConversationDestinationScreen
onChangehandlers into 2 (snapshotRevision+composerPrefillRequest).pendingUserInputsForThreadandrelevantServerSnapshotcomputed properties.Fix C — SessionsModel
HomeDashboardModel'sscheduleObservedRefreshpattern.Fix D — HomeDashboardView
visibleSessions.map { ... }onChange signals (re-allocated per body eval) replaced with precomputedvisibleHydrationSignature/visibleActivitySignatureon the model.Fix E — ProjectPickerSheet
appModel.isLocalServer(N observation edges) replaced with precomputedlocalServerIds: Set<String>param.Also included
bridge: normalize legacy item lifecycle notifications at json-line wire— older Pi/Local Studio bridges emit item lifecycle notifications without mandatory timestamps; normalized at the raw JSONL boundary before strict upstream decode.Verification
make ios-sim-fast→ BUILD SUCCEEDED (Rust 1.93 toolchain)