feat(sessions): add tmux persistence and terminal context tracking
Add extensible remote session backends, safe ownership and restore handling, and pane-scoped shell integration over the existing SSH connection. Keep iOS workspaces in memory and rebuild expired Activity targets on demand. Integrate session selection into Connection Journey with consistent controls, cancellation behavior, and localized counts.
This commit is contained in:
1 parent
babb6711b5
commit
8e3f1128da
79 files changed
+7345
-944
No files matched your search
@@ -3,14 +3,34 @@ import Foundation
|
||||
|
||||
/// One activity summarizes all mobile workspaces. Never contains credentials or terminal output.
|
||||
nonisolated struct WorkspaceActivityAttributes: ActivityAttributes {
|
||||
struct Recovery: Codable, Hashable {
|
||||
var version = 1
|
||||
var sourceWorkspaceID: UUID
|
||||
var hostID: UUID
|
||||
var kind: String
|
||||
/// Opaque, versioned domain reference. Never included in deep links.
|
||||
var remoteSession: Data?
|
||||
}
|
||||
|
||||
struct Session: Codable, Hashable, Identifiable {
|
||||
var id: UUID
|
||||
var title: String
|
||||
var kind: String
|
||||
var connected: Bool
|
||||
var iconAsset: String? = nil
|
||||
var recovery: Recovery? = nil
|
||||
|
||||
var destinationURL: URL { WorkspaceActivityAttributes.destinationURL(for: id) }
|
||||
var destinationURL: URL {
|
||||
guard let recovery else { return WorkspaceActivityAttributes.destinationURL(for: id) }
|
||||
var components = URLComponents(
|
||||
url: WorkspaceActivityAttributes.destinationURL(for: id), resolvingAgainstBaseURL: false)!
|
||||
components.queryItems = [
|
||||
URLQueryItem(name: "v", value: "1"),
|
||||
URLQueryItem(name: "host", value: recovery.hostID.uuidString),
|
||||
URLQueryItem(name: "kind", value: recovery.kind),
|
||||
]
|
||||
return components.url!
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentState: Codable, Hashable {
|
||||
@@ -31,13 +51,23 @@ nonisolated struct WorkspaceActivityAttributes: ActivityAttributes {
|
||||
session.title = String(decoding: String(session.title.prefix(32)).utf8.prefix(96), as: UTF8.self)
|
||||
return session
|
||||
}
|
||||
return Self(
|
||||
var result = Self(
|
||||
workspaceCount: workspaces.count, connectedCount: workspaces.filter(\.connected).count,
|
||||
sessions: visible, focusedWorkspaceID: focused?.id)
|
||||
// Leave room for Activity attributes and system encoding overhead. Drop whole entries,
|
||||
// never part of an identity. The focused entry is first and therefore retained longest.
|
||||
while !result.sessions.isEmpty, (try? JSONEncoder().encode(result).count) ?? Int.max > 3500 {
|
||||
result.sessions.removeLast()
|
||||
}
|
||||
if !result.sessions.contains(where: { $0.id == result.focusedWorkspaceID }) {
|
||||
result.focusedWorkspaceID = result.sessions.first?.id
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
var destinationURL: URL {
|
||||
focusedWorkspaceID.map(WorkspaceActivityAttributes.destinationURL(for:))
|
||||
sessions.first(where: { $0.id == focusedWorkspaceID })?.destinationURL
|
||||
?? focusedWorkspaceID.map(WorkspaceActivityAttributes.destinationURL(for:))
|
||||
?? WorkspaceActivityAttributes.destinationURL
|
||||
}
|
||||
}
|
||||
@@ -57,8 +87,25 @@ nonisolated struct WorkspaceActivityAttributes: ActivityAttributes {
|
||||
|
||||
static func workspaceID(from url: URL) -> UUID? {
|
||||
guard url.scheme == "termind", url.host == "workspace",
|
||||
url.query == nil, url.fragment == nil, url.pathComponents.count == 2
|
||||
url.user == nil, url.password == nil, url.port == nil,
|
||||
url.fragment == nil, url.pathComponents.count == 2
|
||||
else { return nil }
|
||||
if url.query != nil, recovery(from: url) == nil { return nil }
|
||||
return UUID(uuidString: url.lastPathComponent)
|
||||
}
|
||||
|
||||
static func recovery(from url: URL) -> Recovery? {
|
||||
guard url.scheme == "termind", url.host == "workspace",
|
||||
url.user == nil, url.password == nil, url.port == nil, url.fragment == nil,
|
||||
url.pathComponents.count == 2, let id = UUID(uuidString: url.lastPathComponent),
|
||||
let items = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems,
|
||||
items.count == 3, Set(items.map(\.name)) == ["v", "host", "kind"],
|
||||
items.first(where: { $0.name == "v" })?.value == "1",
|
||||
let rawHost = items.first(where: { $0.name == "host" })?.value,
|
||||
let host = UUID(uuidString: rawHost),
|
||||
let kind = items.first(where: { $0.name == "kind" })?.value,
|
||||
["ssh", "sftp", "telnet"].contains(kind)
|
||||
else { return nil }
|
||||
return Recovery(sourceWorkspaceID: id, hostID: host, kind: kind)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user