Skip to content

Commit ac9894d

Browse files
committed
IPC refactor, many perf updates, latex, plan mode, user input requests
1 parent 36350ff commit ac9894d

File tree

74 files changed

+14876
-7737
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

74 files changed

+14876
-7737
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,4 +68,5 @@ node_modules/
6868
.factory/
6969
.claude/
7070
shared/third_party/codex/
71-
.wrangler/
71+
.wrangler/
72+
artifacts/

Makefile

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ IOS_SCRIPTS := $(IOS_DIR)/scripts
2323
IOS_FW_DIR := $(IOS_DIR)/Frameworks
2424
IOS_GENERATED := $(IOS_DIR)/GeneratedRust
2525
IOS_SOURCES := $(IOS_DIR)/Sources
26+
IOS_RUN_ARTIFACTS_DIR ?= $(ROOT)/artifacts/ios-device-run
27+
IOS_DEVICE_PROFILE ?= 1
28+
IOS_DEVICE_PROFILE_TEMPLATE ?= Time Profiler
29+
IOS_DEVICE_PROFILE_TIME_LIMIT ?=
30+
IOS_SIM_RUN_ARTIFACTS_DIR ?= $(ROOT)/artifacts/ios-sim-run
31+
IOS_SIM_PROFILE ?= 1
32+
IOS_SIM_PROFILE_TEMPLATE ?= Time Profiler
33+
IOS_SIM_PROFILE_TIME_LIMIT ?=
2634
ANDROID_DIR := $(ROOT)/apps/android
2735
ANDROID_JNI := $(ANDROID_DIR)/core/bridge/src/main/jniLibs
2836
GENERATED_DIR := $(RUST_DIR)/generated
@@ -42,6 +50,19 @@ ANDROID_EMULATOR_ABIS ?= $(if $(filter arm64 aarch64,$(HOST_ARCH)),arm64-v8a,x86
4250
# Source local env (credentials, SDK paths) if present — must precede ?= auto-detect
4351
-include .env
4452

53+
AWS_SHARED_CREDENTIALS_FILE ?= $(HOME)/.aws/credentials
54+
55+
define aws_profile_credential
56+
$(strip $(shell PROFILE='$(AWS_PROFILE)' CREDS_FILE='$(AWS_SHARED_CREDENTIALS_FILE)' KEY='$(1)' /bin/bash -lc '\
57+
if [ -n "$$PROFILE" ] && [ -f "$$CREDS_FILE" ]; then \
58+
awk -F" *= *" -v profile="$$PROFILE" -v key="$$KEY" '\''
59+
$$0 == "[" profile "]" { in_profile = 1; next } \
60+
/^\[/ { in_profile = 0 } \
61+
in_profile && $$1 == key { print $$2; exit }\
62+
'\'' "$$CREDS_FILE"; \
63+
fi'))
64+
endef
65+
4566
# Auto-detect Android SDK/NDK/JDK paths (macOS defaults, overridable via env or .env)
4667
ANDROID_SDK_ROOT ?= $(or $(ANDROID_HOME),$(wildcard $(HOME)/Library/Android/sdk))
4768
ANDROID_NDK_HOME ?= $(shell ls -d $(ANDROID_SDK_ROOT)/ndk/*/ 2>/dev/null | sort -V | tail -1 | sed 's:/*$$::')
@@ -61,14 +82,30 @@ export JAVA_HOME
6182

6283
SCCACHE := $(shell command -v sccache 2>/dev/null)
6384
ifneq ($(SCCACHE),)
85+
ifeq ($(strip $(AWS_ACCESS_KEY_ID)),)
86+
AWS_ACCESS_KEY_ID := $(call aws_profile_credential,aws_access_key_id)
87+
endif
88+
ifeq ($(strip $(AWS_SECRET_ACCESS_KEY)),)
89+
AWS_SECRET_ACCESS_KEY := $(call aws_profile_credential,aws_secret_access_key)
90+
endif
91+
ifeq ($(strip $(AWS_SESSION_TOKEN)),)
92+
AWS_SESSION_TOKEN := $(call aws_profile_credential,aws_session_token)
93+
endif
6494
export RUSTC_WRAPPER := $(SCCACHE)
6595
ifdef SCCACHE_BUCKET
6696
export SCCACHE_BUCKET
6797
export SCCACHE_ENDPOINT
6898
export SCCACHE_REGION
6999
export SCCACHE_S3_KEY_PREFIX
70-
export AWS_ACCESS_KEY_ID
71-
export AWS_SECRET_ACCESS_KEY
100+
ifneq ($(strip $(AWS_ACCESS_KEY_ID)),)
101+
export AWS_ACCESS_KEY_ID
102+
endif
103+
ifneq ($(strip $(AWS_SECRET_ACCESS_KEY)),)
104+
export AWS_SECRET_ACCESS_KEY
105+
endif
106+
ifneq ($(strip $(AWS_SESSION_TOKEN)),)
107+
export AWS_SESSION_TOKEN
108+
endif
72109
$(info [cache] Using sccache: $(SCCACHE) → s3://$(SCCACHE_BUCKET))
73110
else
74111
$(info [cache] Using sccache: $(SCCACHE) (local only))
@@ -89,8 +126,7 @@ BOUNDARY_SOURCES := \
89126
$(RUST_DIR)/codex-mobile-client/Cargo.toml \
90127
$(RUST_DIR)/codex-mobile-client/src/lib.rs \
91128
$(RUST_DIR)/codex-mobile-client/src/conversation_uniffi.rs \
92-
$(RUST_DIR)/codex-mobile-client/src/discovery_uniffi.rs \
93-
$(RUST_DIR)/codex-mobile-client/src/mobile_client_impl.rs
129+
$(RUST_DIR)/codex-mobile-client/src/discovery_uniffi.rs
94130

95131
BOUNDARY_SOURCES += $(shell find $(RUST_DIR)/codex-mobile-client/src -type f -name '*.rs' 2>/dev/null)
96132

@@ -140,24 +176,22 @@ ios-sim-fast: ios-build-sim-fast
140176
ios-device: ios-build-device
141177
ios-device-fast: ios-build-device-fast
142178
ios-sim-run: ios-sim-fast
143-
@echo "==> Installing and launching on booted simulator..."
144-
@APP_PATH=$$(/bin/ls -dt $(HOME)/Library/Developer/Xcode/DerivedData/Litter-*/Build/Products/Debug-iphonesimulator/Litter.app 2>/dev/null | head -1) && \
145-
if [ -z "$$APP_PATH" ]; then echo "ERROR: Litter.app not found in DerivedData"; exit 1; fi && \
146-
xcrun simctl install booted "$$APP_PATH" && \
147-
xcrun simctl launch booted com.sigkitten.litter
179+
@echo "==> Installing and launching on booted simulator with saved logs/profile..."
180+
@cd $(ROOT) && \
181+
IOS_SIM_PROFILE='$(IOS_SIM_PROFILE)' \
182+
IOS_SIM_PROFILE_TEMPLATE='$(IOS_SIM_PROFILE_TEMPLATE)' \
183+
IOS_SIM_PROFILE_TIME_LIMIT='$(IOS_SIM_PROFILE_TIME_LIMIT)' \
184+
IOS_SIM_RUN_ARTIFACTS_DIR='$(IOS_SIM_RUN_ARTIFACTS_DIR)' \
185+
$(IOS_SCRIPTS)/run-sim.sh
148186

149187
ios-device-run: ios-device-fast
150-
@echo "==> Installing and launching on connected device..."
151-
@set -o pipefail && \
152-
APP_PATH=$$(/bin/ls -dt $(HOME)/Library/Developer/Xcode/DerivedData/Litter-*/Build/Products/Debug-iphoneos/Litter.app 2>/dev/null | head -1) && \
153-
if [ -z "$$APP_PATH" ]; then echo "ERROR: Litter.app not found in DerivedData"; exit 1; fi && \
154-
DEVICE_ID=$$(xcrun devicectl list devices 2>/dev/null | grep -E 'available|connected' | grep -v 'Simulator' | grep -oE '[0-9A-F]{8}-([0-9A-F]{4}-){3}[0-9A-F]{12}' | head -1) && \
155-
if [ -z "$$DEVICE_ID" ]; then echo "ERROR: no connected device found"; exit 1; fi && \
156-
echo "==> Installing on device $$DEVICE_ID..." && \
157-
xcrun devicectl device install app --device "$$DEVICE_ID" "$$APP_PATH" && \
158-
echo "==> Launching with attached console and timestamps (Ctrl+C stops the app)..." && \
159-
xcrun devicectl device process launch --device "$$DEVICE_ID" --terminate-existing --console com.sigkitten.litter 2>&1 | \
160-
perl -MPOSIX=strftime -ne 'BEGIN { $$| = 1 } print strftime("[%Y-%m-%d %H:%M:%S] ", localtime), $$_'
188+
@echo "==> Installing and launching on connected device with saved logs/profile..."
189+
@cd $(ROOT) && \
190+
IOS_DEVICE_PROFILE='$(IOS_DEVICE_PROFILE)' \
191+
IOS_DEVICE_PROFILE_TEMPLATE='$(IOS_DEVICE_PROFILE_TEMPLATE)' \
192+
IOS_DEVICE_PROFILE_TIME_LIMIT='$(IOS_DEVICE_PROFILE_TIME_LIMIT)' \
193+
IOS_RUN_ARTIFACTS_DIR='$(IOS_RUN_ARTIFACTS_DIR)' \
194+
$(IOS_SCRIPTS)/run-device.sh
161195

162196
ios-run: ios
163197
@open $(IOS_DIR)/Litter.xcodeproj
@@ -251,10 +285,10 @@ help:
251285
@printf '%s\n' \
252286
'make ios full iOS package lane + simulator build' \
253287
'make ios-sim-fast fast simulator lane using raw staticlib outputs' \
254-
'make ios-sim-run fast sim build + install + launch on booted simulator' \
288+
'make ios-sim-run fast sim build + install + launch on booted simulator; saves console log and Time Profiler trace under artifacts/ios-sim-run (override IOS_SIM_PROFILE=0, IOS_SIM_PROFILE_TEMPLATE, IOS_SIM_PROFILE_TIME_LIMIT=30s to cap capture)' \
255289
'make ios-device full iOS package lane + device build' \
256290
'make ios-device-fast fast device lane using raw staticlib outputs' \
257-
'make ios-device-run fast device build + install + launch with attached console on connected device' \
291+
'make ios-device-run fast device build + install + launch on connected device; saves console log and Time Profiler trace for the whole run under artifacts/ios-device-run (override IOS_DEVICE_PROFILE=0, IOS_DEVICE_PROFILE_TEMPLATE, IOS_DEVICE_PROFILE_TIME_LIMIT=30s to cap capture)' \
258292
'make rust-ios-package full Rust iOS package lane (bindings + xcframework)' \
259293
'make rust-ios-sim-fast fast Rust iOS simulator lane (raw staticlib only)' \
260294
'make rust-ios-device-fast fast Rust iOS device lane (raw staticlib only)' \

apps/android/app/src/main/java/com/litter/android/state/AppLifecycleController.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -310,13 +310,8 @@ class AppLifecycleController {
310310
* Called when the app enters the foreground.
311311
*/
312312
suspend fun onResume(context: Context, appModel: AppModel) {
313-
val preResumeActiveSshServerIds = appModel.store.snapshot()
314-
.servers
315-
.filter { !it.isLocal && it.health != AppServerHealth.DISCONNECTED }
316-
.mapTo(mutableSetOf()) { it.serverId }
317313
ensureLocalServerConnected(appModel)
318314
reconnectSavedServers(context, appModel)
319-
reconnectActiveSshServers(context, appModel, preResumeActiveSshServerIds)
320315
probeActiveRemoteServers(appModel)
321316
backgroundedTurnKeys.clear()
322317
}

apps/android/app/src/main/java/com/litter/android/state/AppModel.kt

Lines changed: 56 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,8 @@ import uniffi.codex_mobile_client.AppThreadSnapshot
2323
import uniffi.codex_mobile_client.ThreadStreamingDeltaKind
2424
import uniffi.codex_mobile_client.AppStoreUpdateRecord
2525
import uniffi.codex_mobile_client.DiscoveryBridge
26-
import uniffi.codex_mobile_client.HydratedAssistantMessageData
27-
import uniffi.codex_mobile_client.HydratedCommandExecutionData
2826
import uniffi.codex_mobile_client.HydratedConversationItem
2927
import uniffi.codex_mobile_client.HydratedConversationItemContent
30-
import uniffi.codex_mobile_client.HydratedMcpToolCallData
31-
import uniffi.codex_mobile_client.HydratedProposedPlanData
32-
import uniffi.codex_mobile_client.HydratedReasoningData
3328
import uniffi.codex_mobile_client.HandoffManager
3429
import uniffi.codex_mobile_client.MessageParser
3530
import uniffi.codex_mobile_client.ServerBridge
@@ -504,23 +499,10 @@ class AppModel private constructor(context: android.content.Context) {
504499
when (update) {
505500
is AppStoreUpdateRecord.ThreadUpserted ->
506501
applyThreadUpsert(update.thread, update.sessionSummary, update.agentDirectoryVersion)
507-
is AppStoreUpdateRecord.ThreadStateUpdated ->
502+
is AppStoreUpdateRecord.ThreadMetadataChanged ->
508503
applyThreadStateUpdated(update.state, update.sessionSummary, update.agentDirectoryVersion)
509-
is AppStoreUpdateRecord.ThreadItemUpserted -> {
510-
if (!applyThreadItemUpsert(update.key, update.item)) {
511-
recoverThreadDeltaApplication(update.key)
512-
}
513-
}
514-
is AppStoreUpdateRecord.ThreadCommandExecutionUpdated -> {
515-
if (!applyThreadCommandExecutionUpdated(
516-
update.key,
517-
update.itemId,
518-
update.status,
519-
update.exitCode,
520-
update.durationMs,
521-
update.processId,
522-
)
523-
) {
504+
is AppStoreUpdateRecord.ThreadItemChanged -> {
505+
if (!applyThreadItemChanged(update.key, update.item)) {
524506
recoverThreadDeltaApplication(update.key)
525507
}
526508
}
@@ -706,11 +688,26 @@ class AppModel private constructor(context: android.content.Context) {
706688
val mergedThread = mergedThreadSnapshotPreservingHydratedItems(thread)
707689
val current = _snapshot.value ?: return
708690
val existingThreadIndex = current.threads.indexOfFirst { it.key == thread.key }
691+
692+
// Race condition guard: during active streaming, if the old thread has
693+
// longer assistant text that starts with the new text, preserve the old
694+
// (more complete) text to avoid flickering backwards.
695+
val finalThread = if (existingThreadIndex >= 0) {
696+
val oldThread = current.threads[existingThreadIndex]
697+
if (oldThread.hasActiveTurn) {
698+
preserveStreamingText(oldThread, mergedThread)
699+
} else {
700+
mergedThread
701+
}
702+
} else {
703+
mergedThread
704+
}
705+
709706
val updatedThreads = current.threads.toMutableList().apply {
710707
if (existingThreadIndex >= 0) {
711-
this[existingThreadIndex] = mergedThread
708+
this[existingThreadIndex] = finalThread
712709
} else {
713-
add(mergedThread)
710+
add(finalThread)
714711
}
715712
}
716713

@@ -732,10 +729,44 @@ class AppModel private constructor(context: android.content.Context) {
732729
sessionSummaries = updatedSummaries,
733730
agentDirectoryVersion = agentDirectoryVersion,
734731
)
735-
cacheThreadSnapshot(mergedThread)
732+
cacheThreadSnapshot(finalThread)
736733
_lastError.value = null
737734
}
738735

736+
private fun preserveStreamingText(
737+
oldThread: AppThreadSnapshot,
738+
newThread: AppThreadSnapshot,
739+
): AppThreadSnapshot {
740+
if (newThread.hydratedConversationItems.isEmpty()) return newThread
741+
val oldItemsById = oldThread.hydratedConversationItems.associateBy { it.id }
742+
var changed = false
743+
val mergedItems = newThread.hydratedConversationItems.map { newItem ->
744+
val oldItem = oldItemsById[newItem.id]
745+
if (oldItem != null) {
746+
val oldText = assistantText(oldItem.content)
747+
val newText = assistantText(newItem.content)
748+
if (oldText != null && newText != null &&
749+
oldText.length > newText.length &&
750+
oldText.startsWith(newText)
751+
) {
752+
changed = true
753+
oldItem
754+
} else {
755+
newItem
756+
}
757+
} else {
758+
newItem
759+
}
760+
}
761+
return if (changed) newThread.copy(hydratedConversationItems = mergedItems) else newThread
762+
}
763+
764+
private fun assistantText(content: HydratedConversationItemContent): String? =
765+
when (content) {
766+
is HydratedConversationItemContent.Assistant -> content.v1.text
767+
else -> null
768+
}
769+
739770
private fun applyThreadStateUpdated(
740771
state: uniffi.codex_mobile_client.AppThreadStateRecord,
741772
sessionSummary: AppSessionSummary,
@@ -787,7 +818,7 @@ class AppModel private constructor(context: android.content.Context) {
787818
_lastError.value = null
788819
}
789820

790-
private fun applyThreadItemUpsert(
821+
private fun applyThreadItemChanged(
791822
key: ThreadKey,
792823
item: HydratedConversationItem,
793824
): Boolean {
@@ -808,40 +839,6 @@ class AppModel private constructor(context: android.content.Context) {
808839
return true
809840
}
810841

811-
private fun applyThreadCommandExecutionUpdated(
812-
key: ThreadKey,
813-
itemId: String,
814-
status: uniffi.codex_mobile_client.AppOperationStatus,
815-
exitCode: Int?,
816-
durationMs: Long?,
817-
processId: String?,
818-
): Boolean {
819-
val current = _snapshot.value ?: return false
820-
val threadIndex = current.threads.indexOfFirst { it.key == key }
821-
if (threadIndex < 0) return false
822-
823-
val thread = current.threads[threadIndex]
824-
val itemIndex = thread.hydratedConversationItems.indexOfFirst { it.id == itemId }
825-
if (itemIndex < 0) return false
826-
827-
val item = thread.hydratedConversationItems[itemIndex]
828-
val content = item.content as? HydratedConversationItemContent.CommandExecution ?: return false
829-
val updatedItems = thread.hydratedConversationItems.toMutableList().apply {
830-
this[itemIndex] = item.copy(
831-
content = HydratedConversationItemContent.CommandExecution(
832-
content.v1.copy(
833-
status = status,
834-
exitCode = exitCode,
835-
durationMs = durationMs,
836-
processId = processId,
837-
),
838-
),
839-
)
840-
}
841-
applyThreadSnapshot(thread.copy(hydratedConversationItems = updatedItems))
842-
return true
843-
}
844-
845842
private fun applyThreadStreamingDelta(
846843
key: ThreadKey,
847844
itemId: String,

apps/android/app/src/main/java/com/litter/android/ui/ExperimentalFeatures.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ enum class LitterFeature(
2121
id = "ipc",
2222
displayName = "IPC",
2323
description = "Attach to desktop IPC over SSH for faster sync, approvals, and resume. Requires reconnecting the server.",
24-
defaultEnabled = true,
24+
defaultEnabled = false,
2525
),
2626
GENERATIVE_UI(
2727
id = "generative_ui",

apps/android/app/src/main/java/com/litter/android/ui/conversation/ComposerBar.kt

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -298,19 +298,6 @@ fun ComposerBar(
298298
}
299299
}
300300

301-
if (showCollaborationModeChip) {
302-
Row(
303-
modifier = Modifier
304-
.fillMaxWidth()
305-
.padding(horizontal = 12.dp, vertical = 8.dp),
306-
) {
307-
CollaborationModeChip(
308-
mode = collaborationMode,
309-
onClick = { onOpenCollaborationModePicker?.invoke() },
310-
)
311-
}
312-
}
313-
314301
activePlanProgress?.let { progress ->
315302
PlanProgressPanel(progress = progress)
316303
}

0 commit comments

Comments
 (0)