Skip to content
Closed
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
8 changes: 8 additions & 0 deletions apps/ios/Litter.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@
25A6AE0E882A1230E8113D76 /* StableSafeAreaInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = C701571F55C11FBF7C1C29F3 /* StableSafeAreaInsets.swift */; };
25ACF260028E1293314A6AC3 /* LitterComplicationEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4578B3ADA4BFFC01A6910404 /* LitterComplicationEntry.swift */; };
25D96A70E576C0590C1B7C7A /* ApprovalScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6CDA77FA2F0C9DF92D098BDA /* ApprovalScreen.swift */; };
26123F9592B0A9878A9A4323 /* LiveTourUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1622B326A565CB0A52D1AF73 /* LiveTourUITests.swift */; };
2691245C74CDCB1714EADEC7 /* notion-light.json in Resources */ = {isa = PBXBuildFile; fileRef = 99BFC7A01A194E7508C5693C /* notion-light.json */; };
27B1C10E386286FA2A30A8DF /* MinigameOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B22B0C284B20FB135A3A35BE /* MinigameOverlayView.swift */; };
2825608DFF86022012E9CAAE /* SavedServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F4C10A9AB37FCCDDA975381 /* SavedServer.swift */; };
Expand Down Expand Up @@ -442,6 +443,7 @@
A83CFAE1805823A320144A89 /* LitterServerListPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9ECBFB0BFC09594F6E0814F /* LitterServerListPayload.swift */; };
A91B73BD99881AF4EF7388BE /* RustAlleycatBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93CDBDD02E344E7C903D5DEC /* RustAlleycatBridge.swift */; };
AA10EE43A455D870C2B01F65 /* DynamicTools.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4874A6712A57F780064DB0E /* DynamicTools.swift */; };
AA3132226FC9145F389A5960 /* LiveTypingAfterTurnUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85D14738F56EC016845C6921 /* LiveTypingAfterTurnUITests.swift */; };
AA31352E315C5C623403E4DA /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE71A015A197F8B7F6AE82B8 /* Security.framework */; };
AAB89F368EA6954B70DE0D09 /* MessageRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2325BAE1FF0D41A4D5ADE3AD /* MessageRecorder.swift */; };
AABDEAC2D1616649533BC57E /* VideoWallpaperPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B361A4FDCFF12B0FE7E893EC /* VideoWallpaperPlayerView.swift */; };
Expand Down Expand Up @@ -768,6 +770,7 @@
13BD448AC1B95ECDF642BF20 /* usgc-metalgate.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "usgc-metalgate.json"; sourceTree = "<group>"; };
14B7D7A9258301037FB7DBC0 /* night-owl.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "night-owl.json"; sourceTree = "<group>"; };
159DE69EDAAB27536744024C /* cat_transmission_06.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = cat_transmission_06.png; sourceTree = "<group>"; };
1622B326A565CB0A52D1AF73 /* LiveTourUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTourUITests.swift; sourceTree = "<group>"; };
1641427B3606638F140B4E9A /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = System/Library/Frameworks/SystemConfiguration.framework; sourceTree = SDKROOT; };
169A7268F57D1E75A523B40F /* usgc-epitaxy.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "usgc-epitaxy.json"; sourceTree = "<group>"; };
17099E066405571104CB9A37 /* SessionsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionsScreen.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -946,6 +949,7 @@
835AE4DDAB1DE3E7AEB3EA7F /* ComposerPermissionChip.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposerPermissionChip.swift; sourceTree = "<group>"; };
83D1805F2EFF51A7DD62CB88 /* github-dark-default.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "github-dark-default.json"; sourceTree = "<group>"; };
85743F1AC88293BE8BBD27DA /* ImageGenerationToolCallView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGenerationToolCallView.swift; sourceTree = "<group>"; };
85D14738F56EC016845C6921 /* LiveTypingAfterTurnUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTypingAfterTurnUITests.swift; sourceTree = "<group>"; };
86A94747FCE745BAA2DBEBF4 /* CrossServerToolResultView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrossServerToolResultView.swift; sourceTree = "<group>"; };
87E1D2E7E6CB4DA678C6605E /* github-dark.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "github-dark.json"; sourceTree = "<group>"; };
885259DDCC6E448B237FAA61 /* temple-dark.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "temple-dark.json"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1177,6 +1181,8 @@
children = (
46D841AF0FFB0241834F88D0 /* LitterUITests.swift */,
2D77BFF764026CE68A0C394F /* LiveE2EUITests.swift */,
1622B326A565CB0A52D1AF73 /* LiveTourUITests.swift */,
85D14738F56EC016845C6921 /* LiveTypingAfterTurnUITests.swift */,
E724333261AD4653114210E9 /* MessageLinkUITests.swift */,
);
name = LitterUITests;
Expand Down Expand Up @@ -2654,6 +2660,8 @@
files = (
7F7580E318B61556359A4EC1 /* LitterUITests.swift in Sources */,
23E8FBDBD957414ADADDAB18 /* LiveE2EUITests.swift in Sources */,
26123F9592B0A9878A9A4323 /* LiveTourUITests.swift in Sources */,
AA3132226FC9145F389A5960 /* LiveTypingAfterTurnUITests.swift in Sources */,
E21627B0742DBE674DC230C4 /* MessageLinkUITests.swift in Sources */,
05A1BF5FB8220D9D48C611D0 /* SnapshotHelper.swift in Sources */,
);
Expand Down
8 changes: 2 additions & 6 deletions apps/ios/Sources/Litter/LitterApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1978,7 +1978,6 @@ private struct ConversationDestinationScreen: View {
var body: some View {
Group {
if let conversationThread {
@Bindable var bindableScreenModel = screenModel
ConversationView(
thread: conversationThread,
activeThreadKey: resolvedThreadKey,
Expand All @@ -1989,8 +1988,7 @@ private struct ConversationDestinationScreen: View {
resolveTargetLabel: screenModel.resolveTargetLabel,
resolveThreadKey: screenModel.resolveThreadKey,
resolveLiveStatus: screenModel.resolveLiveStatus,
composerInputText: $bindableScreenModel.composerInputText,
composerAttachedImages: $bindableScreenModel.composerAttachedImages,
composerDraft: screenModel.composerDraft,
topInset: 4,
bottomInset: bottomInset,
onOpenConversation: onOpenConversation,
Expand Down Expand Up @@ -2108,7 +2106,6 @@ private struct ReplayDestinationScreen: View {
var body: some View {
Group {
if let thread = conversationThread, let key = replayThreadKey {
@Bindable var bindableScreenModel = screenModel
ConversationView(
thread: thread,
activeThreadKey: key,
Expand All @@ -2119,8 +2116,7 @@ private struct ReplayDestinationScreen: View {
resolveTargetLabel: screenModel.resolveTargetLabel,
resolveThreadKey: screenModel.resolveThreadKey,
resolveLiveStatus: screenModel.resolveLiveStatus,
composerInputText: $bindableScreenModel.composerInputText,
composerAttachedImages: $bindableScreenModel.composerAttachedImages,
composerDraft: screenModel.composerDraft,
topInset: 0,
bottomInset: bottomInset,
onOpenConversation: nil,
Expand Down
27 changes: 18 additions & 9 deletions apps/ios/Sources/Litter/Views/ConversationScreenModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,14 @@ final class ConversationScreenModel {
/// ConversationToolbarControls via param instead of
/// `appModel.snapshot` in body (which would create a per-token edge).
private(set) var serverSnapshot: AppServerSnapshot?
/// Live composer draft. Lifted out of `ConversationInputBar` so it
/// survives view teardown when `ConversationDestinationScreen` flips
/// through its `if let conversationThread` branch during foreground
/// refresh — otherwise typed-but-unsent text and pasted attachments
/// vanish on app switch.
var composerInputText: String = ""
var composerAttachedImages: [UIImage] = []
/// Live composer draft. Owned here so it survives view teardown when
/// `ConversationDestinationScreen` flips through its
/// `if let conversationThread` branch during foreground refresh —
/// otherwise typed-but-unsent text and pasted attachments vanish on app
/// switch. A separate object, passed down by reference, so only the
/// composer observes keystrokes: a binding to a property here made every
/// keystroke re-render the whole conversation screen and transcript.
let composerDraft = ConversationComposerDraft()

/// Precomputed closure that resolves agent target labels from a captured
/// snapshot of `sessionSummaries`. Reading `appModel.snapshot` inside a
Expand Down Expand Up @@ -165,8 +166,8 @@ final class ConversationScreenModel {
minigameTask?.cancel()
minigameTask = nil
minigameOverlay = .idle
composerInputText = ""
composerAttachedImages = []
composerDraft.text = ""
composerDraft.attachedImages = []
}

refreshState()
Expand Down Expand Up @@ -573,3 +574,11 @@ extension ConversationScreenModel {
}
}
#endif

/// The conversation composer's unsent text and attachments.
@MainActor
@Observable
final class ConversationComposerDraft {
var text: String = ""
var attachedImages: [UIImage] = []
}
8 changes: 4 additions & 4 deletions apps/ios/Sources/Litter/Views/ConversationTimelineView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1989,9 +1989,6 @@ struct ConversationPinnedContextStrip: View {

init(items: [ConversationItem]) {
self.items = items
_cachedCombinedPinnedDiff = State(
initialValue: Self.buildCombinedPinnedDiff(from: items)
)
}

var body: some View {
Expand Down Expand Up @@ -2023,7 +2020,10 @@ struct ConversationPinnedContextStrip: View {
sections: presentedDiff.sections
)
}
.onChange(of: pinnedDiffTaskKey, initial: false) { _, _ in
// Built only when the pinned items change. Building it in `init`
// ran on every parent re-render, although SwiftUI keeps only the
// first `@State` initial value.
.onChange(of: pinnedDiffTaskKey, initial: true) { _, _ in
cachedCombinedPinnedDiff = Self.buildCombinedPinnedDiff(from: items)
}
}
Expand Down
46 changes: 28 additions & 18 deletions apps/ios/Sources/Litter/Views/ConversationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ struct ConversationView: View {
var resolveTargetLabel: (String) -> String?
var resolveThreadKey: (String) -> ThreadKey?
var resolveLiveStatus: (ThreadKey) -> AppSubagentStatus?
@Binding var composerInputText: String
@Binding var composerAttachedImages: [UIImage]
let composerDraft: ConversationComposerDraft
var topInset: CGFloat = 0
var bottomInset: CGFloat = 0
var onOpenConversation: ((ThreadKey) -> Void)? = nil
Expand Down Expand Up @@ -146,8 +145,7 @@ struct ConversationView: View {
ConversationBottomChrome(
pinnedContextItems: pinnedContextItems,
composer: composer,
composerInputText: $composerInputText,
composerAttachedImages: $composerAttachedImages,
composerDraft: composerDraft,
onSend: sendMessage,
onFileSearch: searchComposerFiles,
bottomInset: bottomInset,
Expand Down Expand Up @@ -412,12 +410,22 @@ private extension AppThreadSnapshot {
}
}

/// Builds the draft bindings inside a leaf so keystrokes invalidate only the
/// composer, not the conversation screen that holds the draft.
private struct ComposerDraftBinder<Content: View>: View {
@Bindable var draft: ConversationComposerDraft
@ViewBuilder let content: (Binding<String>, Binding<[UIImage]>) -> Content

var body: some View {
content($draft.text, $draft.attachedImages)
}
}

private struct ConversationBottomChrome: View {
@Environment(AppModel.self) private var appModel
let pinnedContextItems: [ConversationItem]
let composer: ConversationComposerSnapshot
@Binding var composerInputText: String
@Binding var composerAttachedImages: [UIImage]
let composerDraft: ConversationComposerDraft
let onSend: (String, [UIImage], [ComposerFileAttachment], [SkillMentionSelection], [PluginMentionSelection]) -> Void
let onFileSearch: (String) async throws -> [FileSearchResult]
var bottomInset: CGFloat = 0
Expand All @@ -433,18 +441,20 @@ private struct ConversationBottomChrome: View {
ConversationPinnedContextStrip(
items: pinnedContextItems
)
ConversationInputBar(
snapshot: composer,
onSend: onSend,
onFileSearch: onFileSearch,
bottomInset: bottomInset,
showModeChip: !hasPinnedDiff,
onOpenModePicker: openCollaborationModePicker,
onOpenConversation: onOpenConversation,
onResumeSessions: onResumeSessions,
inputText: $composerInputText,
attachedImages: $composerAttachedImages
)
ComposerDraftBinder(draft: composerDraft) { text, images in
ConversationInputBar(
snapshot: composer,
onSend: onSend,
onFileSearch: onFileSearch,
bottomInset: bottomInset,
showModeChip: !hasPinnedDiff,
onOpenModePicker: openCollaborationModePicker,
onOpenConversation: onOpenConversation,
onResumeSessions: onResumeSessions,
inputText: text,
attachedImages: images
)
}
.background(.clear, ignoresSafeAreaEdges: .bottom)
}
// Line the composer up with the transcript column on wide surfaces.
Expand Down
Loading
Loading