Repository navigation
Improve Claude GUI mode detection for iOS #7067
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
52 commits
Select commit
Hold shift + click to select a range
e619798
Add mobile chat observer regression coverage
azooz2003-bit cb9601a
Detect Claude mobile chat sessions from process details
azooz2003-bit 4004b39
Await mobile chat observation before listing sessions
azooz2003-bit bf4d102
Split mobile chat observation scanner files
azooz2003-bit 8f2e200
Merge remote-tracking branch 'origin/main' into feat-ios-claude-gui-d…
azooz2003-bit e2cef6c
Keep exit liveness matching to agent executables
azooz2003-bit 45ddba9
Make observed agent scan load process details lazily
azooz2003-bit b0a7edb
Avoid inherited env mobile chat detections
azooz2003-bit 944b3cb
Refresh mobile chat observations on each list pull
azooz2003-bit 2ab6bfd
Avoid unchanged observed session updates
azooz2003-bit e67ec27
Single-flight mobile chat observations
azooz2003-bit a7b56bb
Share mobile chat liveness detection
azooz2003-bit c592ca6
Avoid blocking mobile chat list observation
azooz2003-bit a63295d
Throttle scheduled mobile chat observation
azooz2003-bit 5b2a221
Use timestamp throttle for mobile chat observation
azooz2003-bit f8ca5b4
Surface fresh Claude sessions in mobile chat
azooz2003-bit 782228a
Reconcile pending Claude chat sessions
azooz2003-bit 247030a
Split agent chat observe scan helpers
azooz2003-bit 3767d49
Store seeded chat sessions by canonical id
azooz2003-bit 993e0c7
Fix pending Claude lifecycle reconciliation
azooz2003-bit 04e3e36
Notify clients when Claude aliases are removed
azooz2003-bit 640f3c5
Harden pending Claude session removal
azooz2003-bit d9d58b5
Preserve Claude hook-store lookup aliases
azooz2003-bit 3e8f3ea
Avoid permanent unversioned removal tombstones
azooz2003-bit 95e23f6
Fix stale Claude GUI chat rows
azooz2003-bit 57be801
Make Claude mobile chat detection deterministic
azooz2003-bit f330d20
Bound mobile chat agent observation
azooz2003-bit 4eb3baa
Fix scoped agent observation reuse
azooz2003-bit 552726f
Coalesce ignored mobile chat refreshes
azooz2003-bit 270f9ec
Preserve pending Claude history identity
azooz2003-bit ec9cf42
Gate Claude session identity by provenance
azooz2003-bit 80a8db5
Preserve pinned Claude chat during alias handoff
azooz2003-bit d910ac9
Gate agent chat detection to foreground processes
azooz2003-bit 9dc043b
Drain superseded agent observation waiters
azooz2003-bit 5ca0489
Require authoritative Claude chat identity
azooz2003-bit 277c067
Index live Claude session aliases
azooz2003-bit dd51e51
Match Claude liveness by session identity
azooz2003-bit ff0e70f
Isolate settings notification observer tests
azooz2003-bit 6a4cbc4
Harden Claude resume session parsing
azooz2003-bit 7027cdd
Stabilize chat store cache-head test
azooz2003-bit 5c552c3
Harden Claude liveness review paths
azooz2003-bit ca789c0
Merge remote-tracking branch 'origin/main' into feat-ios-claude-gui-d…
azooz2003-bit d1c28ce
Preserve launch-kind agent detection
azooz2003-bit 9234eec
Constrain Claude liveness fallback
azooz2003-bit 13bc715
Fix Claude GUI session surface binding
azooz2003-bit 7e84a11
Address Claude GUI detection review issues
azooz2003-bit 9bd543d
Reject inherited Claude launch env for child tools
azooz2003-bit dcf4f9a
Detect Claude exe runtime process
azooz2003-bit 4a46400
Create pending Claude sessions for unidentified GUI launches
azooz2003-bit a5190ce
Preserve live agent chat bindings during seed
azooz2003-bit ce3f092
Cancel superseded agent chat scans
azooz2003-bit 59aa121
Address agent chat policy review fixes
azooz2003-bit File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
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
125 changes: 125 additions & 0 deletions
125
...red/CmuxAgentChat/Tests/CmuxAgentChatTests/ChatConversationStoreSessionRemovalTests.swift
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| import Foundation | ||
| import Testing | ||
|
|
||
| @testable import CmuxAgentChat | ||
|
|
||
| @Suite("ChatConversationStore session removal") | ||
| @MainActor | ||
| struct ChatConversationStoreSessionRemovalTests { | ||
| private static nonisolated let baseTime = Date(timeIntervalSince1970: 1_781_006_400) | ||
|
|
||
| @Test("stale sessionRemoved does not end a newer focused descriptor") | ||
| func staleSessionRemovedDoesNotEndNewerDescriptor() async { | ||
| let source = EventSource() | ||
| let store = ChatConversationStore( | ||
| descriptor: ChatSessionDescriptor( | ||
| id: "session-1", | ||
| agentKind: .claude, | ||
| title: "Test", | ||
| state: .working(since: Self.baseTime), | ||
| version: 6 | ||
| ), | ||
| source: source, | ||
| now: { Self.baseTime } | ||
| ) | ||
| let runTask = Task { await store.run() } | ||
| defer { runTask.cancel() } | ||
|
|
||
| #expect(await Self.waitUntil { store.isConnected }) | ||
| await source.emit(.sessionRemoved(version: 5)) | ||
| await Task.yield() | ||
|
|
||
| #expect(store.agentState == .working(since: Self.baseTime)) | ||
| } | ||
|
|
||
| @Test("stale live events do not revive removed focused descriptor") | ||
| func staleLiveEventsDoNotReviveRemovedDescriptor() async { | ||
| let source = EventSource() | ||
| let store = ChatConversationStore( | ||
| descriptor: Self.descriptor(state: .working(since: Self.baseTime), version: 5), | ||
| source: source, | ||
| now: { Self.baseTime } | ||
| ) | ||
| let runTask = Task { await store.run() } | ||
| defer { runTask.cancel() } | ||
|
|
||
| #expect(await Self.waitUntil { store.isConnected }) | ||
| await source.emit(.sessionRemoved(version: 6)) | ||
| #expect(await Self.waitUntil { store.agentState == ChatAgentState.ended }) | ||
|
|
||
| await source.emit(.stateChanged(.idle)) | ||
| await source.emit(.descriptorChanged(Self.descriptor(state: .idle, version: 6))) | ||
| await Task.yield() | ||
| #expect(store.agentState == ChatAgentState.ended) | ||
|
|
||
| await source.emit(.descriptorChanged(Self.descriptor(state: .idle, version: 7))) | ||
| #expect(await Self.waitUntil { store.agentState == ChatAgentState.idle }) | ||
| } | ||
|
|
||
| @Test("sessionRemoved keeps the public descriptor state in sync") | ||
| func sessionRemovedUpdatesPublicDescriptorState() async { | ||
| let source = EventSource() | ||
| let store = ChatConversationStore( | ||
| descriptor: Self.descriptor(state: .working(since: Self.baseTime), version: 5), | ||
| source: source, | ||
| now: { Self.baseTime } | ||
| ) | ||
| let runTask = Task { await store.run() } | ||
| defer { runTask.cancel() } | ||
|
|
||
| #expect(await Self.waitUntil { store.isConnected }) | ||
| await source.emit(.sessionRemoved(version: 6)) | ||
| #expect(await Self.waitUntil { store.agentState == ChatAgentState.ended }) | ||
|
|
||
| #expect(store.descriptor.state == .ended) | ||
| #expect(store.descriptor.version == 6) | ||
| } | ||
|
|
||
| @Test("unversioned sessionRemoved allows equal-version descriptor revival") | ||
| func unversionedSessionRemovedAllowsEqualVersionDescriptorRevival() async { | ||
| let source = EventSource() | ||
| let store = ChatConversationStore( | ||
| descriptor: Self.descriptor(state: .working(since: Self.baseTime), version: 5), | ||
| source: source, | ||
| now: { Self.baseTime } | ||
| ) | ||
| let runTask = Task { await store.run() } | ||
| defer { runTask.cancel() } | ||
|
|
||
| #expect(await Self.waitUntil { store.isConnected }) | ||
| await source.emit(.sessionRemoved(version: Int.max)) | ||
| #expect(await Self.waitUntil { store.agentState == ChatAgentState.ended }) | ||
| #expect(store.descriptor.version == 5) | ||
|
|
||
| await source.emit(.descriptorChanged(Self.descriptor(state: .idle, version: 5))) | ||
| #expect(await Self.waitUntil { store.agentState == ChatAgentState.idle }) | ||
| #expect(store.descriptor.version == 5) | ||
| } | ||
|
|
||
| private static func descriptor( | ||
| state: ChatAgentState, | ||
| version: Int | ||
| ) -> ChatSessionDescriptor { | ||
| ChatSessionDescriptor( | ||
| id: "session-1", | ||
| agentKind: .claude, | ||
| title: "Test", | ||
| state: state, | ||
| version: version | ||
| ) | ||
| } | ||
|
|
||
| private static func waitUntil( | ||
| iterations: Int = 400, | ||
| _ condition: () -> Bool | ||
| ) async -> Bool { | ||
| for iteration in 0..<iterations { | ||
| if condition() { return true } | ||
| await Task.yield() | ||
| if iteration % 20 == 19 { | ||
| try? await Task.sleep(nanoseconds: 2_000_000) | ||
| } | ||
| } | ||
| return condition() | ||
| } | ||
| } |
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.