Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public actor TerminalSurfaceRuntimeTeardownCoordinator {
request.read()
}

/// Reads plain active-screen evidence on the same serialized native lane as
/// surface teardown, so the borrowed pointer cannot be freed mid-read.
func readActiveScreenTailText(_ request: TerminalSurfaceRuntimeVisibleTextRequest) -> String? {
request.read()
}

/// Queues a native-surface free from any isolation (the surface model's
/// `deinit` is nonisolated and cannot await).
///
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
internal import GhosttyKit

/// A row-bounded plain-text read serialized with native surface teardown.
///
/// The request selects only the current active-screen suffix. Scrollback and VT
/// styling never cross this boundary. `@unchecked Sendable` is limited to
/// transporting the borrowed surface pointer onto the teardown coordinator.
struct TerminalSurfaceRuntimeVisibleTextRequest: @unchecked Sendable {
let surface: ghostty_surface_t
let startRow: UInt32
let maxBytes: Int

func read() -> String? {
let selection = ghostty_selection_s(
top_left: ghostty_point_s(
tag: GHOSTTY_POINT_ACTIVE,
coord: GHOSTTY_POINT_COORD_EXACT,
x: 0,
y: startRow
),
bottom_right: ghostty_point_s(
tag: GHOSTTY_POINT_ACTIVE,
coord: GHOSTTY_POINT_COORD_BOTTOM_RIGHT,
x: 0,
y: 0
),
rectangle: false
)
var text = ghostty_text_s()
guard ghostty_surface_read_text(surface, selection, &text) else { return nil }
defer { ghostty_surface_free_text(surface, &text) }

guard let byteCount = Int(exactly: text.text_len), byteCount <= maxBytes else { return nil }
guard byteCount > 0, let bytes = text.text else { return "" }
return String(
decoding: UnsafeRawBufferPointer(start: bytes, count: byteCount),
as: UTF8.self
)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,28 @@
extension TerminalSurface {
/// Reads plain rendered text from the newest active-screen rows.
///
/// This deliberately excludes scrollback and terminal styling. Consumers
/// receive Ghostty's cell-model text rather than a VT serialization, so row
/// boundaries and control sequences cannot affect semantic matching.
@MainActor
public func boundedActiveScreenTailText(maxRows: Int, maxBytes: Int) async -> String? {
guard maxRows > 0,
maxBytes > 0,
let totalRows = rawSizingSample()?.rows,
totalRows > 0,
let surface = liveSurfaceForGhosttyAccess(reason: "boundedActiveScreenTailText"),
let startRow = UInt32(exactly: max(0, totalRows - min(totalRows, maxRows))) else {
return nil
}
return await runtimeTeardown.readActiveScreenTailText(
TerminalSurfaceRuntimeVisibleTextRequest(
surface: surface,
startRow: startRow,
maxBytes: maxBytes
)
)
}

/// Reads a byte-bounded VT reconstruction of the newest physical terminal rows.
///
/// Ghostty selects the history suffix and formats it into a fixed-size buffer
Expand Down
3 changes: 3 additions & 0 deletions Packages/macOS/CmuxTerminalCore/CLEAN_ROOM.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Clean-room provenance

Research and implementation were performed by separate roles. The implementation used only a sanitized behavioral specification and independently captured tmux behavior from locally installed agents. No Herdr source, code identifiers, or matching expressions were exposed to the implementation role or copied into cmux.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// Resolves lifecycle authority without allowing screen evidence to compete.
public struct AgentTerminalAuthorityResolver: Sendable {
/// Creates a stateless authority resolver.
public init() {}

/// Resolves one family's lifecycle and screen reports.
///
/// Complete integrations own state whenever present. Session-only
/// integrations remain a fallback because their events do not describe
/// every terminal interaction state.
public func resolve(
authoritative: AgentTerminalSemanticState?,
screen: AgentTerminalSemanticState?,
lifecycleAuthoritative: Bool = true
) -> AgentTerminalSemanticState {
if lifecycleAuthoritative { return authoritative ?? screen ?? .unknown }
return screen ?? authoritative ?? .unknown
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/// Injected monotonic time and cancellable delay operations for the detector scheduler.
public struct AgentTerminalDetectionClock: Sendable {
private let nowOperation: @Sendable () async -> Duration
private let sleepOperation: @Sendable (Duration) async throws -> Void

/// Creates a clock seam from monotonic operations.
public init(
now: @escaping @Sendable () async -> Duration,
sleep: @escaping @Sendable (Duration) async throws -> Void
) {
nowOperation = now
sleepOperation = sleep
}

/// Returns elapsed monotonic time from the clock's private origin.
public func now() async -> Duration {
await nowOperation()
}

/// Suspends for an intended debounce/deadline delay.
public func sleep(for duration: Duration) async throws {
try await sleepOperation(duration)
}

/// A production clock backed by ``ContinuousClock``.
public static func continuous() -> AgentTerminalDetectionClock {
let clock = ContinuousClock()
let origin = clock.now
return AgentTerminalDetectionClock(
now: { origin.duration(to: clock.now) },
sleep: { duration in try await clock.sleep(for: duration) }
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/// Timing bounds for coalesced terminal-state evaluation.
public struct AgentTerminalDetectionConfiguration: Sendable, Equatable {
/// Intended quiet period after the newest output invalidation.
public let quietWindow: Duration
/// Maximum time a sustained burst may defer evaluation.
public let maximumLatency: Duration

/// Creates bounded scheduler timing.
public init(quietWindow: Duration = .milliseconds(90), maximumLatency: Duration = .milliseconds(350)) {
self.quietWindow = quietWindow
self.maximumLatency = maximumLatency
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
public import Foundation

/// One effective state change published by the bounded scheduler.
public struct AgentTerminalDetectionUpdate: Sendable, Equatable {
/// The terminal surface receiving the change.
public let surfaceID: UUID
/// The terminal evidence revision that was classified.
public let revision: UInt64
/// The generation-safe semantic classification.
public let classification: AgentTerminalStateClassification

/// Creates an effective scheduler update.
public init(surfaceID: UUID, revision: UInt64, classification: AgentTerminalStateClassification) {
self.surfaceID = surfaceID
self.revision = revision
self.classification = classification
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import CmuxFoundation

/// A synchronous, thread-safe invalidation signal for a terminal output callback.
///
/// The raw callback only advances an atomic revision and yields into a
/// buffering-newest stream. It never captures a screen, classifies text, or
/// creates a task.
public final class AgentTerminalDirtySignal: Sendable {
private let generation = AtomicUInt64Generation()
private let continuation: AsyncStream<UInt64>.Continuation

/// The single-consumer stream of coalesced newest revisions.
public let revisions: AsyncStream<UInt64>

/// Creates an initially clean signal.
public init() {
let pair = AsyncStream<UInt64>.makeStream(bufferingPolicy: .bufferingNewest(1))
revisions = pair.stream
continuation = pair.continuation
}

deinit {
continuation.finish()
}

/// Marks newer terminal evidence from a synchronous PTY callback.
@inline(__always)
public func markDirty() {
continuation.yield(generation.advanceRelaxed())
}

/// Returns the newest invalidation revision without an actor hop.
@inline(__always)
public func currentRevision() -> UInt64 {
generation.loadRelaxed()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/// Validated, data-driven identity and terminal evidence for one agent family.
public struct AgentTerminalFamilyProfile: Sendable, Equatable {
/// Canonical detector identifier.
public let id: String
/// Existing cmux lifecycle/status key used by sidebar integration.
public let statusKey: String
/// Human-readable family name for diagnostics.
public let displayName: String
/// Whether a complete official lifecycle integration may own semantic state.
public let lifecycleAuthoritative: Bool
/// Executable basenames that directly identify this family.
public let executableBasenames: Set<String>
/// Command-line fragments that reveal this family behind a generic runtime.
public let argumentNeedles: [String]
/// Accepted values of cmux-owned wrapper hints.
public let hintAliases: Set<String>
/// Current live-bottom fragments that strongly indicate an idle composer.
public let idleNeedles: [String]
/// Current live-bottom evidence groups that indicate active work.
///
/// Every fragment in one inner group must match. Any complete group is
/// sufficient. This keeps generic words from becoming standalone signals.
public let workingEvidenceGroups: [[String]]
/// Strict current-interaction evidence groups that require human input.
public let blockedEvidenceGroups: [[String]]
/// High-confidence prompts that require human input when they occupy a rendered line.
public let blockedExactLines: [String]
/// Agent-owned history/transcript fragments that suppress new classification.
public let historyViewNeedles: [String]

/// Creates one profile. Catalog validation rejects empty or duplicate identity data.
public init(
id: String,
statusKey: String,
displayName: String,
lifecycleAuthoritative: Bool = false,
executableBasenames: Set<String>,
argumentNeedles: [String] = [],
hintAliases: Set<String> = [],
idleNeedles: [String] = [],
workingEvidenceGroups: [[String]] = [],
blockedEvidenceGroups: [[String]] = [],
blockedExactLines: [String] = [],
historyViewNeedles: [String] = []
) {
self.id = id
self.statusKey = statusKey
self.displayName = displayName
self.lifecycleAuthoritative = lifecycleAuthoritative
self.executableBasenames = executableBasenames
self.argumentNeedles = argumentNeedles
self.hintAliases = hintAliases
self.idleNeedles = idleNeedles
self.workingEvidenceGroups = workingEvidenceGroups
self.blockedEvidenceGroups = blockedEvidenceGroups
self.blockedExactLines = blockedExactLines
self.historyViewNeedles = historyViewNeedles
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// Stable identity for one foreground process and terminal-runtime generation.
public struct AgentTerminalProcessIdentity: Sendable, Equatable, Hashable {
/// The operating-system process identifier.
public let pid: Int32
/// Process start time seconds from the kernel process record.
public let startSeconds: Int64
/// Process start time microseconds from the kernel process record.
public let startMicroseconds: Int64
/// The native terminal runtime generation that observed the process.
public let runtimeGeneration: UInt64

/// Creates a generation-safe foreground process identity.
public init(pid: Int32, startSeconds: Int64, startMicroseconds: Int64, runtimeGeneration: UInt64) {
self.pid = pid
self.startSeconds = startSeconds
self.startMicroseconds = startMicroseconds
self.runtimeGeneration = runtimeGeneration
}
}
Loading
Loading