Skip to content
Open
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
2 changes: 2 additions & 0 deletions apps/apple/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.build/
.swiftpm/
22 changes: 22 additions & 0 deletions apps/apple/Apps/T4IOSApp/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
</dict>
</plist>
11 changes: 11 additions & 0 deletions apps/apple/Apps/T4IOSApp/T4IOSApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import SwiftUI
import T4UI

@main
struct T4IOSApp: App {
var body: some Scene {
WindowGroup {
T4RootView(composition: T4Composition.live())
}
}
}
26 changes: 26 additions & 0 deletions apps/apple/Apps/T4MacApp/Info.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>T4 Code</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>NSPrincipalClass</key>
<string>NSApplication</string>
</dict>
</plist>
47 changes: 47 additions & 0 deletions apps/apple/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// swift-tools-version: 6.0

import PackageDescription

let package = Package(
name: "T4Apple",
platforms: [
.macOS(.v14),
.iOS(.v17),
],
products: [
.library(name: "T4Protocol", targets: ["T4Protocol"]),
.library(name: "T4Client", targets: ["T4Client"]),
.library(name: "T4Platform", targets: ["T4Platform"]),
.library(name: "T4UI", targets: ["T4UI"]),
.executable(name: "T4MacApp", targets: ["T4MacApp"]),
],
targets: [
.target(
name: "T4Protocol"
),
.target(
name: "T4Client",
dependencies: ["T4Protocol"]
),
.target(
name: "T4Platform",
dependencies: ["T4Protocol"]
),
.target(
name: "T4UI",
dependencies: ["T4Protocol", "T4Client", "T4Platform"]
),
.executableTarget(
name: "T4MacApp",
dependencies: ["T4UI", "T4Client", "T4Platform"]
),
.testTarget(
name: "T4ProtocolTests",
dependencies: ["T4Protocol"]
),
.testTarget(
name: "T4ClientTests",
dependencies: ["T4Client", "T4Platform"]
),
]
)
160 changes: 160 additions & 0 deletions apps/apple/Sources/T4Client/AppState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import Foundation
import Observation
import T4Protocol

/// Connection lifecycle exposed to native surfaces.
public enum T4ConnectionState: String, Sendable, Equatable {
case disconnected
case connecting
case connected
case reconnecting
case failed
}

public enum T4AuthenticationState: String, Sendable, Equatable {
case unknown
case local
case pairingRequired
case paired
case failed
}

public struct T4Profile: Identifiable, Sendable, Equatable {
public let id: String
public var label: String
public var targetID: String
public var isEnabled: Bool
public var isSelected: Bool

public init(id: String, label: String, targetID: String = "local", isEnabled: Bool = true, isSelected: Bool = false) {
self.id = id
self.label = label
self.targetID = targetID
self.isEnabled = isEnabled
self.isSelected = isSelected
}
}

public struct T4Session: Identifiable, Sendable, Equatable {
public let id: String
public var hostID: String
public var title: String
public var status: String
public var updatedAt: Date?
public var isSelected: Bool

public init(id: String, hostID: String, title: String = "", status: String = "", updatedAt: Date? = nil, isSelected: Bool = false) {
self.id = id
self.hostID = hostID
self.title = title
self.status = status
self.updatedAt = updatedAt
self.isSelected = isSelected
}
}

public struct T4TranscriptItem: Identifiable, Sendable, Equatable {
public let id: String
public var role: String
public var text: String
public var cursor: TranscriptCursor?
public var revision: String?

public init(id: String, role: String, text: String, cursor: TranscriptCursor? = nil, revision: String? = nil) {
self.id = id
self.role = role
self.text = text
self.cursor = cursor
self.revision = revision
}
}

public struct T4AttentionItem: Identifiable, Sendable, Equatable {
public let id: String
public var kind: String
public var title: String
public var detail: String
public var commandID: String?

public init(id: String, kind: String, title: String, detail: String = "", commandID: String? = nil) {
self.id = id
self.kind = kind
self.title = title
self.detail = detail
self.commandID = commandID
}
}

public struct T4ComposerState: Sendable, Equatable {
public var text = ""
public var isSending = false
public var queuedCount = 0
public var error: String?

public init() {}
}

public struct T4DeveloperState: Sendable, Equatable {
public var isEnabled = false
public var lastRequestID: String?
public var lastCommandID: String?
public var messages: [String] = []

public init() {}
}

public struct T4SettingsState: Sendable, Equatable {
public var values: [String: String] = [:]

public init(values: [String: String] = [:]) {
self.values = values
}
}

/// The disposable native projection. It intentionally keeps host-index and live
/// transcript cursors separate: they have independent ordering domains.
@Observable @MainActor
public final class AppState {
public var connection: T4ConnectionState = .disconnected
public var authentication: T4AuthenticationState = .unknown
public var profiles: [T4Profile] = []
public var selectedProfileID: String?
public var sessions: [T4Session] = []
public var selectedSessionID: String?
public var transcript: [T4TranscriptItem] = []
public var transcriptCursor: TranscriptCursor?
public var sessionIndexCursor: SessionIndexCursor?
public var composer = T4ComposerState()
public var attention: [T4AttentionItem] = []
public var developer = T4DeveloperState()
public var settings = T4SettingsState()
public var errorMessage: String?
public private(set) var generation: UInt64 = 0

public init() {}

func beginGeneration() -> UInt64 {
generation &+= 1
return generation
}

func resetConnection(retainSelection: Bool = true) {
connection = .disconnected
authentication = .unknown
errorMessage = nil
composer.isSending = false
if !retainSelection {
sessions.removeAll(keepingCapacity: true)
transcript.removeAll(keepingCapacity: true)
transcriptCursor = nil
sessionIndexCursor = nil
composer.queuedCount = 0
attention.removeAll(keepingCapacity: true)
}
if !retainSelection {
selectedProfileID = nil
selectedSessionID = nil
profiles = profiles.map { T4Profile(id: $0.id, label: $0.label, targetID: $0.targetID, isEnabled: $0.isEnabled) }
}
}
}
Loading
Loading