Skip to content

Commit 8a06173

Browse files
grokifyclaude
andcommitted
fix(desktop): derive stable Session identity from tmux name
parseSessionOutput minted a fresh random UUID for every Session on every 5s refresh, breaking SwiftUI ForEach identity and the session-picker checkmark and forcing needless view rebuilds. Derive a deterministic UUIDv5 from the tmux session name so a session keeps one identity across refreshes. Refs: RMI-PLEXUSONEAPP-003 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6ec3de2 commit 8a06173

3 files changed

Lines changed: 56 additions & 0 deletions

File tree

apps/desktop/Sources/PlexusOneDesktop/Models/Session.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,35 @@
11
import Foundation
2+
import CryptoKit
23
import AssistantKit
34

5+
extension UUID {
6+
/// Fixed namespace for PlexusOne tmux-session identities (RFC 4122).
7+
private static let tmuxSessionNamespace = UUID(uuidString: "b3f0e6a2-1c4d-5e6f-8a9b-0c1d2e3f4a5b")!
8+
9+
/// Derive a stable, deterministic UUID (version 5, RFC 4122) from a tmux
10+
/// session name. The same name always yields the same UUID, so a session
11+
/// keeps one identity across periodic refreshes instead of getting a fresh
12+
/// random UUID each cycle (which would churn SwiftUI identity).
13+
static func forTmuxSession(named name: String) -> UUID {
14+
var hasher = Insecure.SHA1()
15+
withUnsafeBytes(of: tmuxSessionNamespace.uuid) { hasher.update(bufferPointer: $0) }
16+
hasher.update(data: Data(name.utf8))
17+
var digest = Array(hasher.finalize()) // 20 bytes; use the first 16
18+
19+
// Set the version (5) and variant (RFC 4122) bits.
20+
digest[6] = (digest[6] & 0x0F) | 0x50
21+
digest[8] = (digest[8] & 0x3F) | 0x80
22+
23+
let bytes = (
24+
digest[0], digest[1], digest[2], digest[3],
25+
digest[4], digest[5], digest[6], digest[7],
26+
digest[8], digest[9], digest[10], digest[11],
27+
digest[12], digest[13], digest[14], digest[15]
28+
)
29+
return UUID(uuid: bytes)
30+
}
31+
}
32+
433
/// Represents a tmux session that can be attached to a pane
534
struct Session: Identifiable, Codable, Hashable {
635
let id: UUID

apps/desktop/Sources/PlexusOneDesktop/Services/SessionManager.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ final class SessionManager {
195195
let status = determineStatus(lastActivity: lastActivity)
196196

197197
let session = Session(
198+
id: .forTmuxSession(named: name),
198199
name: name,
199200
tmuxSession: name,
200201
status: status,

apps/desktop/Tests/PlexusOneDesktopTests/SessionManagerTests.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,32 @@ final class SessionManagerTests: XCTestCase {
160160
XCTAssertEqual(sessions[0].status, .running)
161161
}
162162

163+
func testParseSessionOutputIdStableAcrossRefreshes() {
164+
let manager = SessionManager()
165+
let now = Date()
166+
let ts = Int(now.timeIntervalSince1970 - 10)
167+
168+
// Same session parsed on two separate refresh cycles (different activity
169+
// timestamps, as tmux would report) must keep the same identity.
170+
let first = manager.parseSessionOutput("my-session|\(ts)|1", referenceDate: now)
171+
let second = manager.parseSessionOutput("my-session|\(ts + 5)|1", referenceDate: now)
172+
173+
XCTAssertEqual(first.count, 1)
174+
XCTAssertEqual(second.count, 1)
175+
XCTAssertEqual(first[0].id, second[0].id, "Session id must be stable across refreshes")
176+
}
177+
178+
func testParseSessionOutputIdDiffersByName() {
179+
let manager = SessionManager()
180+
let now = Date()
181+
let ts = Int(now.timeIntervalSince1970 - 10)
182+
183+
let sessions = manager.parseSessionOutput("alpha|\(ts)|1\nbeta|\(ts)|1", referenceDate: now)
184+
185+
XCTAssertEqual(sessions.count, 2)
186+
XCTAssertNotEqual(sessions[0].id, sessions[1].id, "Different sessions must have distinct ids")
187+
}
188+
163189
func testParseSessionOutputMultipleSessions() {
164190
let manager = SessionManager()
165191
let now = Date()

0 commit comments

Comments
 (0)