From 312a1e6993b3be7e9dece0b58e48f0626b70a50d Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sat, 26 Sep 2026 12:35:29 -0400 Subject: [PATCH 01/13] Add off-main agent usage sampling for Claude Code and Codex transcripts AgentUsageSampler (actor) reads an agent transcript incrementally in bounded chunks and folds usage lines into AgentUsageTranscriptAccumulator: - Claude Code: message.model and message.usage on assistant lines, counted once per message id; context = input + cache creation + cache read of the latest main-chain request. - Codex: model from turn_context; cumulative and last-request usage and model_context_window from event_msg token_count. AgentModelCatalog maps model ids to a short display name, context window and a small published list-price table; unknown models get no cost. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Usage/AgentModelCatalog.swift | 127 +++++++++ .../CmuxAgentChat/Usage/AgentModelInfo.swift | 21 ++ .../Usage/AgentModelPricing.swift | 71 +++++ .../Usage/AgentUsageSampler.swift | 165 +++++++++++ .../Usage/AgentUsageSnapshot.swift | 40 +++ .../Usage/AgentUsageSource.swift | 30 ++ .../Usage/AgentUsageTokenCounts.swift | 54 ++++ .../AgentUsageTranscriptAccumulator.swift | 268 ++++++++++++++++++ .../AgentModelCatalogTests.swift | 60 ++++ .../AgentUsageFixtures.swift | 99 +++++++ .../AgentUsageSamplerTests.swift | 60 ++++ ...AgentUsageTranscriptAccumulatorTests.swift | 120 ++++++++ 12 files changed, 1115 insertions(+) create mode 100644 Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelCatalog.swift create mode 100644 Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelInfo.swift create mode 100644 Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelPricing.swift create mode 100644 Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSampler.swift create mode 100644 Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSnapshot.swift create mode 100644 Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSource.swift create mode 100644 Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTokenCounts.swift create mode 100644 Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptAccumulator.swift create mode 100644 Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentModelCatalogTests.swift create mode 100644 Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageFixtures.swift create mode 100644 Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageSamplerTests.swift create mode 100644 Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageTranscriptAccumulatorTests.swift diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelCatalog.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelCatalog.swift new file mode 100644 index 000000000000..42daecf0b328 --- /dev/null +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelCatalog.swift @@ -0,0 +1,127 @@ +import Foundation + +/// Resolves model ids found in agent transcripts to a display name, context +/// window, and published list prices. +/// +/// The price table is deliberately small and explicit: a model that is not +/// listed gets no cost estimate instead of a guessed one. Prices are USD per +/// million tokens from the providers' public API price lists (Anthropic +/// prices as of 2026-06; OpenAI GPT-5 family list prices). +/// +/// ```swift +/// let info = AgentModelCatalog().info(forModelID: "claude-opus-4-8") +/// // info?.displayName == "Opus 4.8", info?.contextWindow == 1_000_000 +/// ``` +public struct AgentModelCatalog: Sendable { + /// Context window assumed for Claude models not known to support 1M. + public static let defaultClaudeContextWindow = 200_000 + /// Context window for 1M-token models (and `[1m]` model ids). + public static let oneMillionContextWindow = 1_000_000 + + /// Claude model families (id without the `claude-` prefix and any date + /// suffix) that run with a 1M-token context window by default. + static let claudeOneMillionFamilies: Set = [ + "fable-5-1", "fable-5", "mythos-5-1", "mythos-5", + "opus-5-5", "opus-5", "opus-4-8", "opus-4-7", "opus-4-6", + "sonnet-5", "sonnet-4-6", + ] + + /// Anthropic list prices keyed by normalized family. + static let claudePrices: [String: AgentModelPricing] = [ + "fable-5-1": .anthropic(input: 10, output: 50, cacheRead: 0.25), + "mythos-5-1": .anthropic(input: 10, output: 50, cacheRead: 0.25), + "fable-5": .anthropic(input: 10, output: 50, cacheRead: 1), + "mythos-5": .anthropic(input: 10, output: 50, cacheRead: 1), + "opus-5-5": .anthropic(input: 4, output: 20, cacheRead: 0.2), + "opus-5": .anthropic(input: 5, output: 25, cacheRead: 0.5), + "opus-4-8": .anthropic(input: 5, output: 25, cacheRead: 0.5), + "opus-4-7": .anthropic(input: 5, output: 25, cacheRead: 0.5), + "opus-4-6": .anthropic(input: 5, output: 25, cacheRead: 0.5), + "opus-4-5": .anthropic(input: 5, output: 25, cacheRead: 0.5), + "opus-4-1": .anthropic(input: 15, output: 75, cacheRead: 1.5), + "opus-4": .anthropic(input: 15, output: 75, cacheRead: 1.5), + "sonnet-5": .anthropic(input: 2, output: 10, cacheRead: 0.2), + "sonnet-4-6": .anthropic(input: 3, output: 15, cacheRead: 0.3), + "sonnet-4-5": .anthropic(input: 3, output: 15, cacheRead: 0.3), + "sonnet-4": .anthropic(input: 3, output: 15, cacheRead: 0.3), + "haiku-4-5": .anthropic(input: 1, output: 5, cacheRead: 0.1), + ] + + /// OpenAI list prices keyed by exact model id. + static let openAIPrices: [String: AgentModelPricing] = [ + "gpt-5": .openAI(input: 1.25, output: 10, cachedInput: 0.125), + "gpt-5-codex": .openAI(input: 1.25, output: 10, cachedInput: 0.125), + "gpt-5.1": .openAI(input: 1.25, output: 10, cachedInput: 0.125), + "gpt-5.1-codex": .openAI(input: 1.25, output: 10, cachedInput: 0.125), + "gpt-5.1-codex-max": .openAI(input: 1.25, output: 10, cachedInput: 0.125), + "gpt-5-mini": .openAI(input: 0.25, output: 2, cachedInput: 0.025), + "gpt-5.1-codex-mini": .openAI(input: 0.25, output: 2, cachedInput: 0.025), + ] + + /// Creates a catalog over the built-in tables. + public init() {} + + /// Describes a model id as written in a transcript. + /// + /// Claude ids may carry a provider prefix (`us.anthropic.`), a date + /// suffix (`-20251001`), or a `[1m]` suffix; all are normalized before + /// lookup. `[1m]` forces a 1M context window. + /// + /// - Parameters: + /// - modelID: The raw model id. + /// - reportedContextWindow: A context window the transcript itself + /// reported (Codex `model_context_window`); wins over the table. + /// - Returns: The model description, or `nil` for an empty id. + public func info(forModelID modelID: String, reportedContextWindow: Int? = nil) -> AgentModelInfo? { + let trimmed = modelID.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty else { return nil } + let lowered = trimmed.lowercased() + if let family = Self.claudeFamily(fromModelID: lowered) { + let forcesOneMillion = lowered.contains("[1m]") + let tableWindow = forcesOneMillion || Self.claudeOneMillionFamilies.contains(family) + ? Self.oneMillionContextWindow + : Self.defaultClaudeContextWindow + return AgentModelInfo( + displayName: Self.claudeDisplayName(family: family) ?? trimmed, + contextWindow: reportedContextWindow ?? tableWindow, + pricing: Self.claudePrices[family] + ) + } + return AgentModelInfo( + displayName: trimmed, + contextWindow: reportedContextWindow, + pricing: Self.openAIPrices[lowered] + ) + } + + /// Extracts the normalized Claude family (`opus-4-8`) from an id such as + /// `us.anthropic.claude-opus-4-8-20260101-v1:0[1m]`, or `nil` when the id + /// is not a Claude id. + static func claudeFamily(fromModelID lowered: String) -> String? { + guard let range = lowered.range(of: "claude-") else { return nil } + var rest = String(lowered[range.upperBound...]) + if let bracket = rest.firstIndex(of: "[") { + rest = String(rest[.. String? { + let parts = family.split(separator: "-") + guard let name = parts.first, name.first?.isLetter == true else { return nil } + let version = parts.dropFirst().prefix { $0.allSatisfy(\.isNumber) } + let title = name.prefix(1).uppercased() + name.dropFirst() + guard !version.isEmpty else { return title } + return title + " " + version.joined(separator: ".") + } +} diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelInfo.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelInfo.swift new file mode 100644 index 000000000000..a97c473385ab --- /dev/null +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelInfo.swift @@ -0,0 +1,21 @@ +import Foundation + +/// What cmux knows about one model id: a short display name, its context +/// window, and (when listed) its published prices. +public struct AgentModelInfo: Sendable, Equatable { + /// Short display name, e.g. `Opus 4.8` or `gpt-5-codex`. + public let displayName: String + /// Context window in tokens, or `nil` when neither the transcript nor + /// the built-in table knows it (the context percentage is then omitted). + public let contextWindow: Int? + /// Published list prices, or `nil` when the model is not in the price + /// table (cost is then omitted rather than guessed). + public let pricing: AgentModelPricing? + + /// Creates a model description. + public init(displayName: String, contextWindow: Int?, pricing: AgentModelPricing?) { + self.displayName = displayName + self.contextWindow = contextWindow + self.pricing = pricing + } +} diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelPricing.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelPricing.swift new file mode 100644 index 000000000000..a6a0c325b13e --- /dev/null +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelPricing.swift @@ -0,0 +1,71 @@ +import Foundation + +/// Published per-million-token list prices for one model, in US dollars. +/// +/// These are public API list prices used only to *estimate* what a session +/// would cost at pay-as-you-go rates. They ignore subscriptions, batch or +/// priority tiers, regional/partner pricing, and fast mode, so the result is +/// labelled as an estimate everywhere it is shown. +public struct AgentModelPricing: Sendable, Equatable { + /// Base input price per million tokens. + public let inputPerMTok: Double + /// Output price per million tokens. + public let outputPerMTok: Double + /// 5-minute cache-write price per million tokens. + public let cacheWrite5mPerMTok: Double + /// 1-hour cache-write price per million tokens. + public let cacheWrite1hPerMTok: Double + /// Cache-read price per million tokens. + public let cacheReadPerMTok: Double + + /// Creates a price row. + public init( + inputPerMTok: Double, + outputPerMTok: Double, + cacheWrite5mPerMTok: Double, + cacheWrite1hPerMTok: Double, + cacheReadPerMTok: Double + ) { + self.inputPerMTok = inputPerMTok + self.outputPerMTok = outputPerMTok + self.cacheWrite5mPerMTok = cacheWrite5mPerMTok + self.cacheWrite1hPerMTok = cacheWrite1hPerMTok + self.cacheReadPerMTok = cacheReadPerMTok + } + + /// Anthropic pricing shape: cache writes cost 1.25x (5m) and 2x (1h) + /// the input price; the cache-read price is model specific. + static func anthropic(input: Double, output: Double, cacheRead: Double) -> Self { + Self( + inputPerMTok: input, + outputPerMTok: output, + cacheWrite5mPerMTok: input * 1.25, + cacheWrite1hPerMTok: input * 2, + cacheReadPerMTok: cacheRead + ) + } + + /// OpenAI pricing shape: cache writes are billed as ordinary input. + static func openAI(input: Double, output: Double, cachedInput: Double) -> Self { + Self( + inputPerMTok: input, + outputPerMTok: output, + cacheWrite5mPerMTok: input, + cacheWrite1hPerMTok: input, + cacheReadPerMTok: cachedInput + ) + } + + /// Estimated cost of `tokens` at these prices, in US dollars. + /// + /// - Parameter tokens: Token counts split by billing class. + /// - Returns: The dollar estimate. + public func estimatedCostUSD(for tokens: AgentUsageTokenCounts) -> Double { + let dollars = Double(tokens.uncachedInput) * inputPerMTok + + Double(tokens.cacheWrite5m) * cacheWrite5mPerMTok + + Double(tokens.cacheWrite1h) * cacheWrite1hPerMTok + + Double(tokens.cacheRead) * cacheReadPerMTok + + Double(tokens.output) * outputPerMTok + return dollars / 1_000_000 + } +} diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSampler.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSampler.swift new file mode 100644 index 000000000000..c75240848415 --- /dev/null +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSampler.swift @@ -0,0 +1,165 @@ +import Foundation + +/// Samples coding-agent usage from transcript files off the main actor. +/// +/// Each transcript is read incrementally: the first sample scans the file +/// once in bounded chunks, and every later sample reads only the bytes +/// appended since the previous one, folding complete lines into a per-file +/// ``AgentUsageTranscriptAccumulator``. A truncated or atomically replaced +/// file (smaller size or new inode) restarts from the beginning. +/// +/// The sampler performs no polling and owns no timers; callers invoke +/// ``sample(transcriptPath:source:)`` when an agent hook event says the +/// transcript probably changed. +/// +/// ```swift +/// let sampler = AgentUsageSampler() +/// let snapshot = await sampler.sample(transcriptPath: path, source: .claude) +/// ``` +public actor AgentUsageSampler { + private struct TrackedTranscript { + var accumulator: AgentUsageTranscriptAccumulator + var byteOffset: UInt64 = 0 + var inode: UInt64? + var fragment = Data() + var discardingOversizedLine = false + var lastUse: UInt64 = 0 + } + + private struct Key: Hashable { + let path: String + let source: AgentUsageSource + } + + private var tracked: [Key: TrackedTranscript] = [:] + private var useCounter: UInt64 = 0 + private let maxTrackedTranscripts: Int + private let chunkSize: Int + private let maxLineBytes: Int + private let catalog: AgentModelCatalog + + /// Creates a sampler. + /// + /// - Parameters: + /// - maxTrackedTranscripts: How many transcripts keep incremental state; + /// the least recently sampled is dropped beyond this (it is re-scanned + /// from the start if sampled again). + /// - chunkSize: Bytes read per `read` call. + /// - maxLineBytes: A single line longer than this is skipped rather than + /// buffered (huge tool outputs never carry usage worth the memory). + /// - catalog: Model table for display names, windows and prices. + public init( + maxTrackedTranscripts: Int = 32, + chunkSize: Int = 1 << 20, + maxLineBytes: Int = 32 << 20, + catalog: AgentModelCatalog = AgentModelCatalog() + ) { + self.maxTrackedTranscripts = max(1, maxTrackedTranscripts) + self.chunkSize = max(1, chunkSize) + self.maxLineBytes = max(1, maxLineBytes) + self.catalog = catalog + } + + /// Reads any new transcript content and returns the current usage. + /// + /// - Parameters: + /// - transcriptPath: Absolute path of the agent transcript JSONL. + /// - source: The transcript format. + /// - Returns: The usage snapshot, or `nil` when the file is unreadable or + /// carries no model/usage yet. + public func sample(transcriptPath: String, source: AgentUsageSource) -> AgentUsageSnapshot? { + let key = Key(path: transcriptPath, source: source) + useCounter &+= 1 + var state = tracked[key] ?? TrackedTranscript( + accumulator: AgentUsageTranscriptAccumulator(source: source, catalog: catalog) + ) + state.lastUse = useCounter + guard let handle = FileHandle(forReadingAtPath: transcriptPath) else { + tracked[key] = nil + return nil + } + defer { try? handle.close() } + let size = (try? handle.seekToEnd()) ?? 0 + let inode = Self.inode(ofPath: transcriptPath) + let replaced = state.inode != nil && inode != nil && state.inode != inode + if size < state.byteOffset || replaced { + state = TrackedTranscript( + accumulator: AgentUsageTranscriptAccumulator(source: source, catalog: catalog), + lastUse: useCounter + ) + } + state.inode = inode + if size > state.byteOffset { + readAppended(from: handle, into: &state) + } + let snapshot = state.accumulator.snapshot() + tracked[key] = state + evictIfNeeded() + return snapshot + } + + /// Drops the incremental state for one transcript (for example when its + /// session ends). + /// + /// - Parameter transcriptPath: The transcript path passed to ``sample(transcriptPath:source:)``. + public func forget(transcriptPath: String) { + tracked = tracked.filter { $0.key.path != transcriptPath } + } + + /// Drops all incremental state (for example when the feature is turned off). + public func reset() { + tracked.removeAll() + } + + private func readAppended(from handle: FileHandle, into state: inout TrackedTranscript) { + do { + try handle.seek(toOffset: state.byteOffset) + } catch { + return + } + while let chunk = try? handle.read(upToCount: chunkSize), !chunk.isEmpty { + state.byteOffset += UInt64(chunk.count) + consume(chunk, into: &state) + } + } + + private func consume(_ chunk: Data, into state: inout TrackedTranscript) { + var lineStart = chunk.startIndex + while let newline = chunk[lineStart...].firstIndex(of: 0x0A) { + let piece = chunk[lineStart.. maxLineBytes { + state.fragment = Data() + state.discardingOversizedLine = true + } + } + + private func evictIfNeeded() { + while tracked.count > maxTrackedTranscripts, + let oldest = tracked.min(by: { $0.value.lastUse < $1.value.lastUse })?.key { + tracked[oldest] = nil + } + } + + /// The inode of a path, or `nil` when it can't be stat'd; detects an + /// atomic replacement that size alone would miss. + private static func inode(ofPath path: String) -> UInt64? { + guard let attributes = try? FileManager.default.attributesOfItem(atPath: path), + let number = attributes[.systemFileNumber] as? NSNumber else { + return nil + } + return number.uint64Value + } +} diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSnapshot.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSnapshot.swift new file mode 100644 index 000000000000..4056aaeea7bf --- /dev/null +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSnapshot.swift @@ -0,0 +1,40 @@ +import Foundation + +/// A point-in-time summary of one agent session's usage, sampled from its +/// transcript: which model it runs, how full the context window is, and an +/// estimated pay-as-you-go API cost. +public struct AgentUsageSnapshot: Sendable, Equatable { + /// The raw model id from the transcript. + public let modelID: String + /// Short model name for display (`Opus 4.8`, `gpt-5-codex`). + public let modelDisplayName: String + /// Tokens occupying the context window as of the latest request. + public let contextTokens: Int + /// The context window size, or `nil` when unknown. + public let contextWindow: Int? + /// Estimated cost of the whole session at published list prices, or + /// `nil` when the model has no price row. + public let estimatedCostUSD: Double? + + /// Creates a snapshot. + public init( + modelID: String, + modelDisplayName: String, + contextTokens: Int, + contextWindow: Int?, + estimatedCostUSD: Double? + ) { + self.modelID = modelID + self.modelDisplayName = modelDisplayName + self.contextTokens = contextTokens + self.contextWindow = contextWindow + self.estimatedCostUSD = estimatedCostUSD + } + + /// Fraction of the context window in use, clamped to `0...1`, or `nil` + /// when the window is unknown. + public var contextFraction: Double? { + guard let contextWindow, contextWindow > 0 else { return nil } + return min(max(Double(contextTokens) / Double(contextWindow), 0), 1) + } +} diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSource.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSource.swift new file mode 100644 index 000000000000..807207bd6c6b --- /dev/null +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSource.swift @@ -0,0 +1,30 @@ +import Foundation + +/// The coding-agent transcript formats whose usage cmux can sample. +/// +/// Only agents whose on-disk transcript records per-request token usage are +/// listed; every other hook source yields `nil` from ``init(hookSource:)``. +public enum AgentUsageSource: String, Sendable, Equatable, Hashable, CaseIterable { + /// Claude Code JSONL transcripts: `assistant` lines carry `message.model` + /// and `message.usage`. + case claude + /// Codex rollout JSONL: `turn_context` lines carry `model` and + /// `event_msg` `token_count` lines carry cumulative and last-request usage. + case codex + + /// Maps a hook event `_source` string to a usage source. + /// + /// - Parameter hookSource: The hook event source (`"claude"`, `"codex"`, …). + public init?(hookSource: String) { + self.init(rawValue: hookSource) + } + + /// The sidebar status-entry key the agent's hook integration writes + /// (`claude_code` for Claude Code, `codex` for Codex). + public var sidebarStatusKey: String { + switch self { + case .claude: return "claude_code" + case .codex: return "codex" + } + } +} diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTokenCounts.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTokenCounts.swift new file mode 100644 index 000000000000..be8d6f55175f --- /dev/null +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTokenCounts.swift @@ -0,0 +1,54 @@ +import Foundation + +/// Token counts split by billing class, used for cost estimation. +/// +/// `uncachedInput` excludes cache reads and cache writes, matching the +/// Anthropic `usage.input_tokens` convention. Codex totals (where +/// `input_tokens` includes cached input) are normalized to this shape by the +/// parser. +public struct AgentUsageTokenCounts: Sendable, Equatable { + /// Input tokens billed at the base input price. + public var uncachedInput: Int + /// Input tokens written to a 5-minute prompt cache. + public var cacheWrite5m: Int + /// Input tokens written to a 1-hour prompt cache. + public var cacheWrite1h: Int + /// Input tokens served from the prompt cache. + public var cacheRead: Int + /// Output tokens (including any reasoning/thinking tokens). + public var output: Int + + /// Creates a count; every class defaults to zero. + public init( + uncachedInput: Int = 0, + cacheWrite5m: Int = 0, + cacheWrite1h: Int = 0, + cacheRead: Int = 0, + output: Int = 0 + ) { + self.uncachedInput = uncachedInput + self.cacheWrite5m = cacheWrite5m + self.cacheWrite1h = cacheWrite1h + self.cacheRead = cacheRead + self.output = output + } + + /// All zeros. + public static let zero = Self() + + /// Every input class summed: the prompt size of one request. + public var totalInput: Int { + uncachedInput + cacheWrite5m + cacheWrite1h + cacheRead + } + + /// Adds two counts class by class. + public static func + (lhs: Self, rhs: Self) -> Self { + Self( + uncachedInput: lhs.uncachedInput + rhs.uncachedInput, + cacheWrite5m: lhs.cacheWrite5m + rhs.cacheWrite5m, + cacheWrite1h: lhs.cacheWrite1h + rhs.cacheWrite1h, + cacheRead: lhs.cacheRead + rhs.cacheRead, + output: lhs.output + rhs.output + ) + } +} diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptAccumulator.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptAccumulator.swift new file mode 100644 index 000000000000..11f6d028856e --- /dev/null +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptAccumulator.swift @@ -0,0 +1,268 @@ +import Foundation + +/// Folds transcript JSONL lines, in order, into running usage totals for one +/// agent session. +/// +/// The accumulator is incremental: feed it each complete line exactly once +/// (``ingest(line:)``), then read ``snapshot(catalog:)`` at any point. Lines +/// that carry no usage are rejected by a cheap byte search before any JSON +/// parsing. +/// +/// - Claude Code writes one `assistant` line per content block, repeating the +/// same `message.id` and `message.usage` (the final line carries the final +/// output count). Usage is therefore counted once per message id, using the +/// last line seen for it. Context size is the prompt size of the latest +/// main-chain request: `input_tokens + cache_creation_input_tokens + +/// cache_read_input_tokens`. Sidechain (subagent) lines count toward cost +/// but not the main context. +/// - Codex `token_count` events already carry cumulative totals +/// (`info.total_token_usage`), the latest request (`info.last_token_usage`), +/// and the window (`info.model_context_window`); the latest event wins. The +/// model comes from `turn_context`. +public struct AgentUsageTranscriptAccumulator: Sendable { + /// Which transcript format this accumulator parses. + public let source: AgentUsageSource + + private var modelID: String? + + // Claude state. + private var committedMessageIDs: Set = [] + private var committedCostUSD: Double = 0 + private var hasUnpricedUsage = false + private var pendingMessageID: String? + private var pendingModelID: String? + private var pendingUsage: AgentUsageTokenCounts = .zero + private var claudeContextTokens: Int? + + // Codex state. + private var codexTotals: AgentUsageTokenCounts? + private var codexContextTokens: Int? + private var codexContextWindow: Int? + + private let catalog: AgentModelCatalog + + /// Creates an empty accumulator. + /// + /// - Parameters: + /// - source: The transcript format. + /// - catalog: Model table used to price Claude messages as they are + /// committed (a session can switch models mid-way). + public init(source: AgentUsageSource, catalog: AgentModelCatalog = AgentModelCatalog()) { + self.source = source + self.catalog = catalog + } + + private static let claudeUsageMarker = Data(#""usage""#.utf8) + private static let codexTokenCountMarker = Data(#""token_count""#.utf8) + private static let codexTurnContextMarker = Data(#""turn_context""#.utf8) + + /// Folds one complete JSONL line (without its trailing newline). + /// + /// - Parameter line: The raw line bytes. Malformed or irrelevant lines + /// are ignored. + public mutating func ingest(line: Data) { + switch source { + case .claude: + guard line.range(of: Self.claudeUsageMarker) != nil else { return } + ingestClaude(line: line) + case .codex: + guard line.range(of: Self.codexTokenCountMarker) != nil + || line.range(of: Self.codexTurnContextMarker) != nil else { return } + ingestCodex(line: line) + } + } + + /// Convenience for tests and callers holding text. + /// + /// - Parameter line: One JSONL line. + public mutating func ingest(line: String) { + ingest(line: Data(line.utf8)) + } + + /// The usage summary so far, or `nil` before any model or usage is known. + /// + /// - Returns: A snapshot combining model, context, and estimated cost. + public func snapshot() -> AgentUsageSnapshot? { + switch source { + case .claude: return claudeSnapshot() + case .codex: return codexSnapshot() + } + } + + // MARK: Claude + + private mutating func ingestClaude(line: Data) { + guard let object = Self.jsonObject(line), + object["type"] as? String == "assistant", + let message = object["message"] as? [String: Any], + let usage = message["usage"] as? [String: Any] else { return } + let model = (message["model"] as? String).flatMap { Self.meaningfulModelID($0) } + let counts = Self.claudeCounts(usage) + let isSidechain = object["isSidechain"] as? Bool ?? false + let messageID = message["id"] as? String + + if let messageID, committedMessageIDs.contains(messageID) { return } + if messageID == nil || messageID != pendingMessageID { + commitPendingClaudeMessage() + pendingMessageID = messageID + } + pendingUsage = counts + pendingModelID = model ?? pendingModelID + if messageID == nil { + // No id to de-duplicate on: count this line on its own. + commitPendingClaudeMessage() + } + guard !isSidechain else { return } + if let model { modelID = model } + if counts.totalInput > 0 { claudeContextTokens = counts.totalInput } + } + + private mutating func commitPendingClaudeMessage() { + defer { + pendingMessageID = nil + pendingModelID = nil + pendingUsage = .zero + } + guard pendingUsage != .zero else { return } + if let pendingMessageID { committedMessageIDs.insert(pendingMessageID) } + if let messageCost = cost(of: pendingUsage, modelID: pendingModelID ?? modelID) { + committedCostUSD += messageCost + } else { + hasUnpricedUsage = true + } + } + + private func cost(of counts: AgentUsageTokenCounts, modelID: String?) -> Double? { + guard let modelID, let pricing = catalog.info(forModelID: modelID)?.pricing else { return nil } + return pricing.estimatedCostUSD(for: counts) + } + + private func claudeSnapshot() -> AgentUsageSnapshot? { + guard let modelID, let info = catalog.info(forModelID: modelID) else { return nil } + let contextTokens = claudeContextTokens ?? 0 + var window = info.contextWindow + // A request larger than the table's window proves a 1M window + // (for example a `[1m]` model whose id carries no suffix). + if let current = window, contextTokens > current { + window = AgentModelCatalog.oneMillionContextWindow + } + var cost: Double? = committedCostUSD + var unpriced = hasUnpricedUsage + if pendingUsage != .zero { + if let pendingCost = self.cost(of: pendingUsage, modelID: pendingModelID ?? modelID) { + cost = committedCostUSD + pendingCost + } else { + unpriced = true + } + } + if unpriced { cost = nil } + return AgentUsageSnapshot( + modelID: modelID, + modelDisplayName: info.displayName, + contextTokens: contextTokens, + contextWindow: window, + estimatedCostUSD: cost + ) + } + + private static func claudeCounts(_ usage: [String: Any]) -> AgentUsageTokenCounts { + let cacheCreation = int(usage["cache_creation_input_tokens"]) + var write5m = cacheCreation + var write1h = 0 + if let split = usage["cache_creation"] as? [String: Any] { + let fiveMinute = int(split["ephemeral_5m_input_tokens"]) + let oneHour = int(split["ephemeral_1h_input_tokens"]) + if fiveMinute + oneHour > 0 { + write5m = fiveMinute + write1h = oneHour + } + } + return AgentUsageTokenCounts( + uncachedInput: int(usage["input_tokens"]), + cacheWrite5m: write5m, + cacheWrite1h: write1h, + cacheRead: int(usage["cache_read_input_tokens"]), + output: int(usage["output_tokens"]) + ) + } + + // MARK: Codex + + private mutating func ingestCodex(line: Data) { + guard let object = Self.jsonObject(line), + let payload = object["payload"] as? [String: Any] else { return } + switch object["type"] as? String { + case "turn_context": + if let model = (payload["model"] as? String).flatMap({ Self.meaningfulModelID($0) }) { + modelID = model + } + case "event_msg": + guard payload["type"] as? String == "token_count", + let info = payload["info"] as? [String: Any] else { return } + if let total = info["total_token_usage"] as? [String: Any] { + codexTotals = Self.codexCounts(total) + } + if let last = info["last_token_usage"] as? [String: Any] { + let total = Self.int(last["total_tokens"]) + codexContextTokens = total > 0 + ? total + : Self.int(last["input_tokens"]) + Self.int(last["output_tokens"]) + } + let window = Self.int(info["model_context_window"]) + if window > 0 { codexContextWindow = window } + default: + return + } + } + + private func codexSnapshot() -> AgentUsageSnapshot? { + guard let modelID, + let info = catalog.info(forModelID: modelID, reportedContextWindow: codexContextWindow) else { + return nil + } + return AgentUsageSnapshot( + modelID: modelID, + modelDisplayName: info.displayName, + contextTokens: codexContextTokens ?? 0, + contextWindow: info.contextWindow, + estimatedCostUSD: codexTotals.flatMap { totals in + info.pricing.map { $0.estimatedCostUSD(for: totals) } + } + ) + } + + /// Codex/OpenAI `input_tokens` includes cached input; split it so cached + /// tokens are priced at the cached rate only once. + private static func codexCounts(_ usage: [String: Any]) -> AgentUsageTokenCounts { + let input = int(usage["input_tokens"]) + let cached = int(usage["cached_input_tokens"]) + let cacheWrite = int(usage["cache_write_input_tokens"]) + return AgentUsageTokenCounts( + uncachedInput: max(0, input - cached - cacheWrite), + cacheWrite5m: cacheWrite, + cacheRead: cached, + output: int(usage["output_tokens"]) + ) + } + + // MARK: Helpers + + private static func jsonObject(_ line: Data) -> [String: Any]? { + (try? JSONSerialization.jsonObject(with: line)) as? [String: Any] + } + + /// Drops placeholder ids such as Claude Code's ``. + private static func meaningfulModelID(_ raw: String) -> String? { + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmed.isEmpty, !trimmed.hasPrefix("<") else { return nil } + return trimmed + } + + private static func int(_ value: Any?) -> Int { + switch value { + case let number as NSNumber: return max(0, number.intValue) + case let string as String: return max(0, Int(string) ?? 0) + default: return 0 + } + } +} diff --git a/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentModelCatalogTests.swift b/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentModelCatalogTests.swift new file mode 100644 index 000000000000..945d38fde12d --- /dev/null +++ b/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentModelCatalogTests.swift @@ -0,0 +1,60 @@ +import Foundation +import Testing + +@testable import CmuxAgentChat + +@Suite("AgentModelCatalog") +struct AgentModelCatalogTests { + private let catalog = AgentModelCatalog() + + @Test(arguments: [ + ("claude-opus-4-8", "Opus 4.8", 1_000_000), + ("claude-opus-5-5", "Opus 5.5", 1_000_000), + ("claude-haiku-4-5-20251001", "Haiku 4.5", 200_000), + ("claude-sonnet-4-5-20250929", "Sonnet 4.5", 200_000), + ("claude-sonnet-4-5-20250929[1m]", "Sonnet 4.5", 1_000_000), + ("us.anthropic.claude-opus-4-1-20250805-v1:0", "Opus 4.1", 200_000), + ]) + func claudeIDsNormalize(modelID: String, displayName: String, window: Int) throws { + let info = try #require(catalog.info(forModelID: modelID)) + #expect(info.displayName == displayName) + #expect(info.contextWindow == window) + #expect(info.pricing != nil) + } + + @Test func familyPrefixesDoNotBorrowAnotherModelsPrice() throws { + let opus4 = try #require(catalog.info(forModelID: "claude-opus-4-20250514")?.pricing) + let opus48 = try #require(catalog.info(forModelID: "claude-opus-4-8")?.pricing) + #expect(opus4.inputPerMTok == 15) + #expect(opus48.inputPerMTok == 5) + #expect(catalog.info(forModelID: "gpt-5.2")?.pricing == nil) + #expect(catalog.info(forModelID: "gpt-5")?.pricing?.inputPerMTok == 1.25) + } + + @Test func legacyAndUnknownIDsFallBackToRawName() throws { + let legacy = try #require(catalog.info(forModelID: "claude-3-5-sonnet-20241022")) + #expect(legacy.displayName == "claude-3-5-sonnet-20241022") + #expect(legacy.pricing == nil) + let codex = try #require(catalog.info(forModelID: "gpt-6-astra")) + #expect(codex.contextWindow == nil) + #expect(catalog.info(forModelID: " ") == nil) + } + + @Test func pricingMathUsesPerMillionRates() { + let pricing = AgentModelPricing( + inputPerMTok: 3, outputPerMTok: 15, cacheWrite5mPerMTok: 3.75, cacheWrite1hPerMTok: 6, cacheReadPerMTok: 0.3 + ) + let tokens = AgentUsageTokenCounts( + uncachedInput: 1_000_000, cacheWrite5m: 1_000_000, cacheWrite1h: 1_000_000, cacheRead: 1_000_000, output: 1_000_000 + ) + #expect(abs(pricing.estimatedCostUSD(for: tokens) - 28.05) < 1e-9) + #expect(pricing.estimatedCostUSD(for: .zero) == 0) + } + + @Test func snapshotFractionClampsAndHandlesUnknownWindow() { + let over = AgentUsageSnapshot(modelID: "m", modelDisplayName: "m", contextTokens: 300, contextWindow: 200, estimatedCostUSD: nil) + #expect(over.contextFraction == 1) + let unknown = AgentUsageSnapshot(modelID: "m", modelDisplayName: "m", contextTokens: 300, contextWindow: nil, estimatedCostUSD: nil) + #expect(unknown.contextFraction == nil) + } +} diff --git a/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageFixtures.swift b/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageFixtures.swift new file mode 100644 index 000000000000..ba992cd043a5 --- /dev/null +++ b/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageFixtures.swift @@ -0,0 +1,99 @@ +import Foundation + +/// JSONL fixture builders mirroring real Claude Code transcripts and Codex +/// rollouts (field names and nesting as written by the CLIs; values made up). +struct AgentUsageFixtures { + static func json(_ object: [String: Any]) -> String { + let data = try! JSONSerialization.data(withJSONObject: object, options: [.sortedKeys]) + return String(decoding: data, as: UTF8.self) + } + + static func claudeAssistant( + id: String?, + model: String = "claude-opus-4-8", + input: Int, + cacheCreation: Int = 0, + cacheCreation1h: Int? = nil, + cacheRead: Int = 0, + output: Int, + isSidechain: Bool = false + ) -> String { + var usage: [String: Any] = [ + "input_tokens": input, + "cache_creation_input_tokens": cacheCreation, + "cache_read_input_tokens": cacheRead, + "output_tokens": output, + "service_tier": "standard", + ] + if let cacheCreation1h { + usage["cache_creation"] = [ + "ephemeral_5m_input_tokens": cacheCreation - cacheCreation1h, + "ephemeral_1h_input_tokens": cacheCreation1h, + ] + } + var message: [String: Any] = [ + "model": model, + "role": "assistant", + "type": "message", + "content": [["type": "text", "text": "ok"]], + "usage": usage, + ] + if let id { message["id"] = id } + return json([ + "type": "assistant", + "isSidechain": isSidechain, + "sessionId": "s1", + "message": message, + ]) + } + + static func claudeUserToolResult() -> String { + json([ + "type": "user", + "isSidechain": false, + "message": ["role": "user", "content": [["type": "tool_result", "content": "usage: none"]]], + ]) + } + + static func codexTurnContext(model: String) -> String { + json([ + "timestamp": "2026-09-25T23:56:00.889Z", + "type": "turn_context", + "payload": ["turn_id": "t1", "cwd": "/tmp", "model": model, "effort": "low"], + ]) + } + + static func codexTokenCount( + totalInput: Int, + totalCached: Int, + totalOutput: Int, + lastTotal: Int, + window: Int + ) -> String { + json([ + "timestamp": "2026-09-25T23:56:04.885Z", + "type": "event_msg", + "payload": [ + "type": "token_count", + "info": [ + "total_token_usage": [ + "input_tokens": totalInput, + "cached_input_tokens": totalCached, + "cache_write_input_tokens": 0, + "output_tokens": totalOutput, + "reasoning_output_tokens": 0, + "total_tokens": totalInput + totalOutput, + ], + "last_token_usage": [ + "input_tokens": lastTotal - 100, + "cached_input_tokens": 0, + "output_tokens": 100, + "total_tokens": lastTotal, + ], + "model_context_window": window, + ], + "rate_limits": ["primary": ["used_percent": 12.0, "window_minutes": 300]], + ], + ]) + } +} diff --git a/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageSamplerTests.swift b/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageSamplerTests.swift new file mode 100644 index 000000000000..51cb01620547 --- /dev/null +++ b/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageSamplerTests.swift @@ -0,0 +1,60 @@ +import Foundation +import Testing + +@testable import CmuxAgentChat + +@Suite("AgentUsageSampler") +struct AgentUsageSamplerTests { + private typealias Fixture = AgentUsageFixtures + + private func makeTranscriptURL() -> URL { + FileManager.default.temporaryDirectory + .appendingPathComponent("agent-usage-\(UUID().uuidString).jsonl") + } + + private func append(_ text: String, to url: URL) throws { + let handle = try FileHandle(forWritingTo: url) + defer { try? handle.close() } + try handle.seekToEnd() + try handle.write(contentsOf: Data(text.utf8)) + } + + @Test func readsIncrementallyAcrossPartialLines() async throws { + let url = makeTranscriptURL() + defer { try? FileManager.default.removeItem(at: url) } + let first = Fixture.claudeAssistant(id: "msg_a", input: 10, cacheRead: 1000, output: 1) + let second = Fixture.claudeAssistant(id: "msg_b", input: 20, cacheRead: 2000, output: 2) + let splitIndex = second.index(second.startIndex, offsetBy: second.count / 2) + try Data((first + "\n" + second[..", input: 0, output: 0)) + accumulator.ingest(line: "not json \"usage\"") + #expect(accumulator.snapshot() == nil) + } + + @Test func codexUsesLatestTokenCountAndTurnContextModel() throws { + var accumulator = AgentUsageTranscriptAccumulator(source: .codex) + accumulator.ingest(line: Fixture.codexTurnContext(model: "gpt-5-codex")) + accumulator.ingest(line: Fixture.codexTokenCount( + totalInput: 20000, totalCached: 0, totalOutput: 500, lastTotal: 20500, window: 272_000 + )) + accumulator.ingest(line: Fixture.codexTokenCount( + totalInput: 120_000, totalCached: 100_000, totalOutput: 5000, lastTotal: 30000, window: 272_000 + )) + + let snapshot = try #require(accumulator.snapshot()) + #expect(snapshot.modelDisplayName == "gpt-5-codex") + #expect(snapshot.contextTokens == 30000) + #expect(snapshot.contextWindow == 272_000) + // Cached input is part of input_tokens: 20000*$1.25 + 100000*$0.125 + 5000*$10 = $0.0875 + let cost = try #require(snapshot.estimatedCostUSD) + #expect(abs(cost - 0.0875) < 1e-9) + } + + @Test func codexUnknownModelKeepsReportedWindowWithoutCost() throws { + var accumulator = AgentUsageTranscriptAccumulator(source: .codex) + accumulator.ingest(line: Fixture.codexTurnContext(model: "gpt-6-astra")) + accumulator.ingest(line: Fixture.codexTokenCount( + totalInput: 18345, totalCached: 0, totalOutput: 5, lastTotal: 18350, window: 258_400 + )) + + let snapshot = try #require(accumulator.snapshot()) + #expect(snapshot.estimatedCostUSD == nil) + #expect(snapshot.contextWindow == 258_400) + #expect(snapshot.contextTokens == 18350) + } + + @Test func codexWithoutModelYieldsNoSnapshot() { + var accumulator = AgentUsageTranscriptAccumulator(source: .codex) + accumulator.ingest(line: Fixture.codexTokenCount( + totalInput: 1, totalCached: 0, totalOutput: 1, lastTotal: 2, window: 100 + )) + #expect(accumulator.snapshot() == nil) + } +} From bf13ca7772777f833b1fcd331842e268689bb02f Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sat, 26 Sep 2026 12:35:34 -0400 Subject: [PATCH 02/13] Show opt-in coding-agent usage next to the sidebar agent status MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New setting sidebar.showAgentUsage (default off) appends the model, context-window percentage and an estimated API cost to the Claude Code and Codex status entries, e.g. "Running · Opus 4.8 · 42% · ~$1.20". It follows sidebar.hideAllDetails like the other detail rows. SidebarAgentUsageCoordinator listens to accepted hook events, coalesces them per session, samples the transcript through AgentUsageSampler off the main actor, and stores a Sendable snapshot on the workspace's sidebar metadata. Nothing is read while the setting is off. The setting is registered in the catalog, cmux.json schema and supported paths, Settings > Sidebar, settings search, and the command palette. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../CmuxConfigSchema.generated.swift | 279 +++++++++--------- .../Keys/SidebarCatalogSection.swift | 10 + .../CuratedSettingEntry+AgentUsage.swift | 19 ++ .../CuratedSettingEntry+Default.swift | 2 +- .../SidebarSection+AgentActivity.swift | 2 + .../Sections/SidebarSection+AgentUsage.swift | 21 ++ .../Sections/SidebarSection.swift | 9 +- .../SettingsRowAnchorResolutionTests.swift | 1 + ...arWorkspaceAuxiliaryDetailVisibility.swift | 18 +- .../Status/SidebarAgentUsage.swift | 25 ++ .../WorkspaceSidebarMetadataModel.swift | 23 ++ .../SidebarDetailVisibilityTests.swift | 16 + .../WorkspaceSidebarMetadataModelTests.swift | 15 + Resources/Localizable.xcstrings | 236 +++++++++++++++ Sources/AppDelegate.swift | 2 +- ...CmuxSettingsFileStore+SupportedPaths.swift | 1 + Sources/CmuxSettingsJSONPathSupport.swift | 7 + .../CommandPaletteSettingsToggle.swift | 2 +- ...tteSettingsToggleCommands+AgentUsage.swift | 24 ++ ...rdShortcutSettingsFileStore+Template.swift | 1 + Sources/SettingsSearchAliases.swift | 1 + Sources/SettingsSearchIndex.swift | 2 + .../SidebarAgentUsageCoordinator.swift | 177 +++++++++++ .../Sidebar/SidebarAgentUsageFormatter.swift | 64 ++++ Sources/SidebarTabItemSettingsSnapshot.swift | 1 + Sources/SidebarWorkspaceDetailSettings.swift | 2 + Sources/SidebarWorkspaceSnapshotFactory.swift | 7 +- Sources/WorkspaceSidebarObservation.swift | 10 +- cmux.xcodeproj/project.pbxproj | 16 + cmuxTests/SidebarAgentUsageTests.swift | 92 ++++++ skills/cmux-settings/references/all-keys.md | 1 + web/data/cmux.schema.json | 5 + 32 files changed, 937 insertions(+), 154 deletions(-) create mode 100644 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+AgentUsage.swift create mode 100644 Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection+AgentUsage.swift create mode 100644 Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Status/SidebarAgentUsage.swift create mode 100644 Sources/CommandPalette/CommandPaletteSettingsToggleCommands+AgentUsage.swift create mode 100644 Sources/Sidebar/SidebarAgentUsageCoordinator.swift create mode 100644 Sources/Sidebar/SidebarAgentUsageFormatter.swift create mode 100644 cmuxTests/SidebarAgentUsageTests.swift diff --git a/Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/ConfigValidation/CmuxConfigSchema.generated.swift b/Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/ConfigValidation/CmuxConfigSchema.generated.swift index 55ea889aa534..039ff2e6cd49 100644 --- a/Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/ConfigValidation/CmuxConfigSchema.generated.swift +++ b/Packages/macOS/CmuxFoundation/Sources/CmuxFoundation/ConfigValidation/CmuxConfigSchema.generated.swift @@ -200,145 +200,146 @@ enum CmuxEmbeddedConfigSchema { "IiwKICAgICAgICAgICJkZWZhdWx0IjogdHJ1ZSwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJPcGVuIHNpZGViYXIgcHVsbCByZXF1ZXN0IGxpbmtzIGluIHRoZSBlbWJlZGRlZCBjbXV4IGJyb3dzZXIuIgogICAgICAgIH0sCiAgICAgICAgIm9wZW5Qb3J0TGlua3NJbkNtdXhCcm93c2VyIjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiT3BlbiBzaWRlYmFyIHBvcnQgbGlua3MgaW4gdGhlIGVtYmVkZGVkIGNtdXggYnJv", "d3Nlci4iCiAgICAgICAgfSwKICAgICAgICAic2hvd1NTSCI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiB0cnVlLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlNob3cgU1NIIGNvbm5lY3Rpb24gZGV0YWlscy4iCiAgICAgICAgfSwKICAgICAgICAic2hvd1BvcnRzIjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiU2hvdyBsaXN0ZW5pbmcgcG9ydHMuIgogICAgICAgIH0s", "CiAgICAgICAgInNob3dMb2ciOiB7CiAgICAgICAgICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogdHJ1ZSwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTaG93IHJlY2VudCBsb2cgc25pcHBldHMuIgogICAgICAgIH0sCiAgICAgICAgInNob3dQcm9ncmVzcyI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiB0cnVlLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlNob3cgcHJvZ3Jlc3MgaW5kaWNhdG9ycy4iCiAgICAgICAgfSwKICAgICAgICAic2hv", - "d0FnZW50QWN0aXZpdHkiOiB7CiAgICAgICAgICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogdHJ1ZSwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTaG93IHRoZSBsb2FkaW5nIHNwaW5uZXIgb24gd29ya3NwYWNlcyB3aXRoIHJ1bm5pbmcgY29kaW5nIGFnZW50cyBvciBhY3RpdmUgbG9hZGVycy4iCiAgICAgICAgfSwKICAgICAgICAibG9hZGluZ1NwaW5uZXJQb3NpdGlvbiI6IHsKICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAiZW51bSI6IFsibGVhZGluZyIsICJ0cmFpbGlu", - "ZyJdLAogICAgICAgICAgImRlZmF1bHQiOiAibGVhZGluZyIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiV2hpY2ggc2lkZSBvZiB0aGUgd29ya3NwYWNlIHJvdyB0aGUgbG9hZGluZyBzcGlubmVyIGFwcGVhcnMgb246IGxlYWRpbmcgKGxlZnQsIHNoYXJpbmcgdGhlIHVucmVhZC1iYWRnZSBzbG90KSBvciB0cmFpbGluZyAocmlnaHQpLiIKICAgICAgICB9LAogICAgICAgICJub3RpZmljYXRpb25CYWRnZVBvc2l0aW9uIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJlbnVtIjogWyJsZWFkaW5n", - "IiwgInRyYWlsaW5nIl0sCiAgICAgICAgICAiZGVmYXVsdCI6ICJsZWFkaW5nIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJXaGljaCBzaWRlIG9mIHRoZSB3b3Jrc3BhY2Ugcm93IHRoZSB1bnJlYWQgbm90aWZpY2F0aW9uIGJhZGdlIGFwcGVhcnMgb246IGxlYWRpbmcgKGxlZnQpIG9yIHRyYWlsaW5nIChyaWdodCkuIgogICAgICAgIH0sCiAgICAgICAgInNob3dDdXN0b21NZXRhZGF0YSI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiB0cnVlLAogICAgICAgICAgImRlc2Ny", - "aXB0aW9uIjogIlNob3cgY3VzdG9tIG1ldGFkYXRhIHBpbGxzLiIKICAgICAgICB9LAogICAgICAgICJyaWdodE1heFdpZHRoIjogewogICAgICAgICAgInR5cGUiOiAibnVtYmVyIiwKICAgICAgICAgICJleGNsdXNpdmVNaW5pbXVtIjogMCwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMuc2lkZWJhci5yaWdodE1heFdpZHRoIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJNYXhpbXVtIHdpZHRoIGluIHBvaW50cyBmb3IgdGhlIHJpZ2h0IHNpZGViYXIuIFdoZW4gb21pdHRlZCwgdGhl", - "IGJ1aWx0LWluIGR5bmFtaWMgY2FwIGFwcGxpZXMuIgogICAgICAgIH0KICAgICAgfQogICAgfSwKICAgICJ3b3Jrc3BhY2VDb2xvcnMiOiB7CiAgICAgICJ4LWNtdXgtc2NvcGVzIjogWyJnbG9iYWwiXSwKICAgICAgInRpdGxlIjogIndvcmtzcGFjZUNvbG9ycyIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJXb3Jrc3BhY2UgdGFiIGFuZCBiYWRnZSBjb2xvcnMgZnJvbSBTZXR0aW5ncyA+IFdvcmtzcGFjZSBDb2xvcnMuIiwKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjogZmFsc2Us", - "CiAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICJpbmRpY2F0b3JTdHlsZSI6IHsKICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAiZW51bSI6IFsKICAgICAgICAgICAgImxlZnRSYWlsIiwKICAgICAgICAgICAgInNvbGlkRmlsbCIsCiAgICAgICAgICAgICJyYWlsIiwKICAgICAgICAgICAgImJvcmRlciIsCiAgICAgICAgICAgICJ3YXNoIiwKICAgICAgICAgICAgImxpZnQiLAogICAgICAgICAgICAidHlwb2dyYXBoeSIsCiAgICAgICAgICAgICJ3YXNoUmFpbCIsCiAgICAgICAgICAgICJibHVlV2Fz", - "aENvbG9yUmFpbCIKICAgICAgICAgIF0sCiAgICAgICAgICAiZGVmYXVsdCI6ICJsZWZ0UmFpbCIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiQWN0aXZlIHdvcmtzcGFjZSBpbmRpY2F0b3Igc3R5bGUuIExlZ2FjeSBhbGlhc2VzIGFyZSBhY2NlcHRlZCBhbmQgbm9ybWFsaXplZC4iCiAgICAgICAgfSwKICAgICAgICAic2VsZWN0aW9uQ29sb3IiOiB7CiAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL2NvbG9ySGV4T3JOdWxsIiwKICAgICAgICAgICJkZWZhdWx0IjogbnVsbCwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJP", - "dmVycmlkZSB0aGUgc2VsZWN0ZWQgd29ya3NwYWNlIGJhY2tncm91bmQgY29sb3IuIgogICAgICAgIH0sCiAgICAgICAgIm5vdGlmaWNhdGlvbkJhZGdlQ29sb3IiOiB7CiAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL2NvbG9ySGV4T3JOdWxsIiwKICAgICAgICAgICJkZWZhdWx0IjogbnVsbCwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJPdmVycmlkZSB0aGUgdW5yZWFkIG5vdGlmaWNhdGlvbiBiYWRnZSBjb2xvci4iCiAgICAgICAgfSwKICAgICAgICAiY29sb3JzIjogewogICAgICAgICAgInR5cGUiOiAib2JqZWN0IiwK", - "ICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJGdWxsIG5hbWVkIHdvcmtzcGFjZSBjb2xvciBwYWxldHRlLiBJbmNsdWRlIGJ1aWx0LWluIGVudHJpZXMgeW91IHdhbnQgdG8ga2VlcCwgcmVtb3ZlIGtleXMgdG8gcmVtb3ZlIGNvbG9ycywgYW5kIGFkZCBtb3JlIG5hbWVkIGVudHJpZXMgdG8gZXh0ZW5kIHRoZSBwaWNrZXIuIiwKICAgICAgICAgICJkZWZhdWx0IjogewogICAgICAgICAgICAiUmVkIjogIiNDMDM5MkIiLAogICAgICAgICAgICAiQ3JpbXNvbiI6ICIjOTIyQjIxIiwKICAgICAgICAgICAgIk9yYW5nZSI6ICIjQTA0", - "MDAwIiwKICAgICAgICAgICAgIkFtYmVyIjogIiM3RDY2MDgiLAogICAgICAgICAgICAiT2xpdmUiOiAiIzRBNUMxOCIsCiAgICAgICAgICAgICJHcmVlbiI6ICIjMTk2RjNEIiwKICAgICAgICAgICAgIlRlYWwiOiAiIzAwNkI2QiIsCiAgICAgICAgICAgICJBcXVhIjogIiMwRTZCOEMiLAogICAgICAgICAgICAiQmx1ZSI6ICIjMTU2NUMwIiwKICAgICAgICAgICAgIk5hdnkiOiAiIzFBNTI3NiIsCiAgICAgICAgICAgICJJbmRpZ28iOiAiIzI4MzU5MyIsCiAgICAgICAgICAgICJQdXJwbGUiOiAiIzZBMUI5QSIsCiAgICAgICAg", - "ICAgICJNYWdlbnRhIjogIiNBRDE0NTciLAogICAgICAgICAgICAiUm9zZSI6ICIjODgwRTRGIiwKICAgICAgICAgICAgIkJyb3duIjogIiM3QjNGMDAiLAogICAgICAgICAgICAiQ2hhcmNvYWwiOiAiIzNFNEI1RSIKICAgICAgICAgIH0sCiAgICAgICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvY29sb3JIZXgiCiAgICAgICAgICB9CiAgICAgICAgfSwKICAgICAgICAicGFsZXR0ZU92ZXJyaWRlcyI6IHsKICAgICAgICAgICJ0eXBlIjogIm9iamVjdCIsCiAgICAgICAg", - "ICAiZGVzY3JpcHRpb24iOiAiTGVnYWN5IHdvcmtzcGFjZSBjb2xvciBvdmVycmlkZXMgZm9yIGJ1aWx0LWluIHBhbGV0dGUgbmFtZXMuIFByZWZlciB3b3Jrc3BhY2VDb2xvcnMuY29sb3JzIGZvciBuZXcgY29uZmlncy4iLAogICAgICAgICAgImRlZmF1bHQiOiB7fSwKICAgICAgICAgICJkZXByZWNhdGVkIjogdHJ1ZSwKICAgICAgICAgICJhZGRpdGlvbmFsUHJvcGVydGllcyI6IHsKICAgICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9jb2xvckhleCIKICAgICAgICAgIH0KICAgICAgICB9LAogICAgICAgICJjdXN0b21Db2xv", - "cnMiOiB7CiAgICAgICAgICAidHlwZSI6ICJhcnJheSIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiTGVnYWN5IGxpc3Qgb2YgY3VzdG9tIHdvcmtzcGFjZSBjb2xvcnMuIFByZWZlciB3b3Jrc3BhY2VDb2xvcnMuY29sb3JzIGZvciBuZXcgY29uZmlncy4iLAogICAgICAgICAgImRlZmF1bHQiOiBbXSwKICAgICAgICAgICJkZXByZWNhdGVkIjogdHJ1ZSwKICAgICAgICAgICJpdGVtcyI6IHsKICAgICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9jb2xvckhleCIKICAgICAgICAgIH0KICAgICAgICB9CiAgICAgIH0KICAgIH0s", - "CiAgICAic2lkZWJhckFwcGVhcmFuY2UiOiB7CiAgICAgICJ4LWNtdXgtc2NvcGVzIjogWyJnbG9iYWwiXSwKICAgICAgInRpdGxlIjogInNpZGViYXJBcHBlYXJhbmNlIiwKICAgICAgImRlc2NyaXB0aW9uIjogIlNpZGViYXIgdGludCBzZXR0aW5ncyBmcm9tIFNldHRpbmdzID4gU2lkZWJhciBBcHBlYXJhbmNlLiIsCiAgICAgICJ0eXBlIjogIm9iamVjdCIsCiAgICAgICJhZGRpdGlvbmFsUHJvcGVydGllcyI6IGZhbHNlLAogICAgICAicHJvcGVydGllcyI6IHsKICAgICAgICAibWF0Y2hUZXJtaW5hbEJhY2tncm91bmQiOiB7", - "CiAgICAgICAgICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogZmFsc2UsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiVXNlIHRoZSB0ZXJtaW5hbCBiYWNrZ3JvdW5kIGluc3RlYWQgb2YgdGhlIHNpZGViYXIgdGludC4iCiAgICAgICAgfSwKICAgICAgICAidGludENvbG9yIjogewogICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9jb2xvckhleCIsCiAgICAgICAgICAiZGVmYXVsdCI6ICIjMDAwMDAwIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJCYXNlIHNpZGViYXIgdGludCBjb2xvciB1c2Vk", - "IHdoZW4gbGlnaHQvZGFyayBvdmVycmlkZXMgYXJlIG5vdCBzZXQuIgogICAgICAgIH0sCiAgICAgICAgImxpZ2h0TW9kZVRpbnRDb2xvciI6IHsKICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvY29sb3JIZXhPck51bGwiLAogICAgICAgICAgImRlZmF1bHQiOiBudWxsLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlNpZGViYXIgdGludCBvdmVycmlkZSBmb3IgbGlnaHQgYXBwZWFyYW5jZS4iCiAgICAgICAgfSwKICAgICAgICAiZGFya01vZGVUaW50Q29sb3IiOiB7CiAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL2NvbG9y", - "SGV4T3JOdWxsIiwKICAgICAgICAgICJkZWZhdWx0IjogbnVsbCwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTaWRlYmFyIHRpbnQgb3ZlcnJpZGUgZm9yIGRhcmsgYXBwZWFyYW5jZS4iCiAgICAgICAgfSwKICAgICAgICAidGludE9wYWNpdHkiOiB7CiAgICAgICAgICAidHlwZSI6ICJudW1iZXIiLAogICAgICAgICAgIm1pbmltdW0iOiAwLAogICAgICAgICAgIm1heGltdW0iOiAxLAogICAgICAgICAgImRlZmF1bHQiOiAwLjE4LAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlNpZGViYXIgdGludCBvcGFjaXR5IGZyb20g", - "MCB0byAxLiBOb3RlOiB0aGlzIG9ubHkgY29udHJvbHMgdGhlIHNpZGViYXIgdGludCwgbm90IHRlcm1pbmFsL3dpbmRvdyB0cmFuc3BhcmVuY3kuIEZvciB0ZXJtaW5hbCBiYWNrZ3JvdW5kIHRyYW5zcGFyZW5jeSBvciBibHVyLCBzZXQgYGJhY2tncm91bmQtb3BhY2l0eWAgYW5kIGBiYWNrZ3JvdW5kLWJsdXJgIGluIGB+Ly5jb25maWcvZ2hvc3R0eS9jb25maWdgIGFuZCBydW4gYGNtdXggcmVsb2FkLWNvbmZpZ2AuIgogICAgICAgIH0KICAgICAgfQogICAgfSwKICAgICJhdXRvbWF0aW9uIjogewogICAgICAieC1jbXV4LXNj", - "b3BlcyI6IFsiZ2xvYmFsIl0sCiAgICAgICJ0aXRsZSI6ICJhdXRvbWF0aW9uIiwKICAgICAgImRlc2NyaXB0aW9uIjogIlNvY2tldCBjb250cm9sIGFuZCBhdXRvbWF0aW9uIHNldHRpbmdzIGZyb20gU2V0dGluZ3MgPiBBdXRvbWF0aW9uLiIsCiAgICAgICJ0eXBlIjogIm9iamVjdCIsCiAgICAgICJhZGRpdGlvbmFsUHJvcGVydGllcyI6IGZhbHNlLAogICAgICAicHJvcGVydGllcyI6IHsKICAgICAgICAic29ja2V0Q29udHJvbE1vZGUiOiB7CiAgICAgICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAgICAgImVudW0iOiBb", - "CiAgICAgICAgICAgICJvZmYiLAogICAgICAgICAgICAiY211eE9ubHkiLAogICAgICAgICAgICAiYXV0b21hdGlvbiIsCiAgICAgICAgICAgICJwYXNzd29yZCIsCiAgICAgICAgICAgICJhbGxvd0FsbCIsCiAgICAgICAgICAgICJvcGVuQWNjZXNzIiwKICAgICAgICAgICAgImZ1bGxPcGVuQWNjZXNzIiwKICAgICAgICAgICAgIm5vdGlmaWNhdGlvbnMiLAogICAgICAgICAgICAiZnVsbCIKICAgICAgICAgIF0sCiAgICAgICAgICAiZGVmYXVsdCI6ICJjbXV4T25seSIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiU29ja2V0", - "IGNvbnRyb2wgbW9kZS4gTGVnYWN5IGFsaWFzZXMgYXJlIGFjY2VwdGVkIGFuZCBub3JtYWxpemVkLiIKICAgICAgICB9LAogICAgICAgICJzb2NrZXRQYXNzd29yZCI6IHsKICAgICAgICAgICJvbmVPZiI6IFsKICAgICAgICAgICAgewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgewogICAgICAgICAgICAgICJ0eXBlIjogIm51bGwiCiAgICAgICAgICAgIH0KICAgICAgICAgIF0sCiAgICAgICAgICAiZGVmYXVsdCI6ICIiLAogICAgICAgICAgImRlc2NyaXB0aW9uIjog", - "IlBhc3N3b3JkIGZvciBwYXNzd29yZC1tb2RlIHNvY2tldCBhY2Nlc3MuIFVzZSBudWxsIG9yIGFuIGVtcHR5IHN0cmluZyB0byBjbGVhciBpdC4iCiAgICAgICAgfSwKICAgICAgICAiY2xhdWRlQ29kZUludGVncmF0aW9uIjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiRW5hYmxlIGNtdXggaW50ZWdyYXRpb24gaG9va3MgZm9yIENsYXVkZSBDb2RlLiIKICAgICAgICB9LAogICAgICAgICJjb2RleEludGVncmF0aW9uIjog", - "ewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiRW5hYmxlIGNtdXggaW50ZWdyYXRpb24gaG9va3MgZm9yIENvZGV4LiBXaGVuIGRpc2FibGVkLCBjbXV4IG5vIGxvbmdlciB3cmFwcyB0aGUgY29kZXggY29tbWFuZCBidXQgc3RpbGwgdHJhY2tzIGxpdmUgQ29kZXggc2Vzc2lvbnMgaXQgY2FuIG9ic2VydmUuIgogICAgICAgIH0sCiAgICAgICAgImNsYXVkZUJpbmFyeVBhdGgiOiB7CiAgICAgICAgICAidHlwZSI6ICJzdHJpbmci", - "LAogICAgICAgICAgImRlZmF1bHQiOiAiIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJDdXN0b20gcGF0aCB0byB0aGUgY2xhdWRlIGJpbmFyeS4iCiAgICAgICAgfSwKICAgICAgICAid29ya3NwYWNlQXV0b05hbWluZyI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiBmYWxzZSwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMuYXV0b21hdGlvbi53b3Jrc3BhY2VBdXRvTmFtaW5nIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJPcHQt", - "aW4gQUkgYXV0by1uYW1pbmcgb2Ygd29ya3NwYWNlcyBhbmQgdGFicyBmcm9tIGFnZW50IGNvbnZlcnNhdGlvbiBjb250ZW50LiBXaGVuIGVuYWJsZWQsIGNtdXggc3VtbWFyaXplcyBzdXBwb3J0ZWQgYWdlbnQgc2Vzc2lvbnMgaW50byBzaG9ydCB0aXRsZXMgdXNpbmcgZWFjaCBhZ2VudCdzIG93biBiaW5hcnk7IG1hbnVhbCByZW5hbWVzIGFsd2F5cyB3aW4uIgogICAgICAgIH0sCiAgICAgICAgImF1dG9OYW1pbmdBZ2VudCI6IHsKICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAiZGVmYXVsdCI6ICJhdXRv", - "IiwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMuYXV0b21hdGlvbi5hdXRvTmFtaW5nQWdlbnQiLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIldoaWNoIGFnZW50IGdlbmVyYXRlcyBhdXRvLW5hbWVzIGZvciBldmVyeSBzZXNzaW9uLiBcImF1dG9cIiAoZGVmYXVsdCkgbmFtZXMgZWFjaCBzZXNzaW9uIHdpdGggaXRzIG93biBhZ2VudDsgYW55IGFnZW50IHNsdWcgKGNsYXVkZSwgY29kZXgsIGdyb2ssIG9wZW5jb2RlLCBwaSwgb21wLCDigKYpIG92ZXJyaWRlcyBuYW1pbmcgZm9yIGFs", - "bCBzZXNzaW9ucywgZXZlbiBvdGhlciBhZ2VudHMnIHNlc3Npb25zLiBVbmRyaXZlYWJsZSBvciB1bmluc3RhbGxlZCBhZ2VudHMgZmFsbCBiYWNrIHRvIHRoZSBzZXNzaW9uJ3Mgb3duIGFnZW50LCBzbyBuYW1pbmcgbmV2ZXIgYnJlYWtzLiIKICAgICAgICB9LAogICAgICAgICJyaXBncmVwQmluYXJ5UGF0aCI6IHsKICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAiZGVmYXVsdCI6ICIiLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkN1c3RvbSBwYXRoIHRvIHRoZSByaXBncmVwIChyZykgYmluYXJ5IHVz", - "ZWQgYnkgcHJvamVjdCBzZWFyY2guIgogICAgICAgIH0sCiAgICAgICAgInN1cHByZXNzU3ViYWdlbnROb3RpZmljYXRpb25zIjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLmF1dG9tYXRpb24uc3VwcHJlc3NTdWJhZ2VudE5vdGlmaWNhdGlvbnMiLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlN1cHByZXNzIHZpc2libGUgY29tcGxldGlvbiBub3RpZmljYXRpb25zIGFuZCBzdGF0", - "dXMgbXV0YXRpb25zIGZyb20gbmVzdGVkIENvZGV4IG9yIENsYXVkZSBjaGlsZCBhZ2VudHMgd2hpbGUga2VlcGluZyB0aGVpciBldmVudHMgaW4gRmVlZCB0ZWxlbWV0cnkuIgogICAgICAgIH0sCiAgICAgICAgImFtcEludGVncmF0aW9uIjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLmF1dG9tYXRpb24uYW1wSW50ZWdyYXRpb24iLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkVu", - "YWJsZSBjbXV4IGludGVncmF0aW9uIGhvb2tzIGZvciBBbXAuIFdoZW4gZGlzYWJsZWQsIHRoZSBidW5kbGVkIHBsdWdpbiBzdGF5cyBpbmFjdGl2ZSB3aXRob3V0IG5lZWRpbmcgdG8gYmUgcmVtb3ZlZC4iCiAgICAgICAgfSwKICAgICAgICAiY3Vyc29ySW50ZWdyYXRpb24iOiB7CiAgICAgICAgICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogdHJ1ZSwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJFbmFibGUgY211eCBpbnRlZ3JhdGlvbiBob29rcyBmb3IgQ3Vyc29yLiIKICAgICAgICB9LAogICAg", - "ICAgICJnZW1pbmlJbnRlZ3JhdGlvbiI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiB0cnVlLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkVuYWJsZSBjbXV4IGludGVncmF0aW9uIGhvb2tzIGZvciBHZW1pbmkuIgogICAgICAgIH0sCiAgICAgICAgImtpcm9JbnRlZ3JhdGlvbiI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiB0cnVlLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkVuYWJsZSBjbXV4IGludGVncmF0aW9uIGhv", - "b2tzIGZvciBLaXJvIENMSS4iCiAgICAgICAgfSwKICAgICAgICAia2lyb05vdGlmaWNhdGlvbkxldmVsIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJlbnVtIjogWwogICAgICAgICAgICAibWluaW1hbCIsCiAgICAgICAgICAgICJzdGFuZGFyZCIsCiAgICAgICAgICAgICJ2ZXJib3NlIgogICAgICAgICAgXSwKICAgICAgICAgICJkZWZhdWx0IjogInN0YW5kYXJkIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJDb250cm9scyBob3cgbWFueSBLaXJvIHRvb2wgZXZlbnRzIGFwcGVhciBpbiBG", - "ZWVkLiIKICAgICAgICB9LAogICAgICAgICJwb3J0QmFzZSI6IHsKICAgICAgICAgICJ0eXBlIjogImludGVnZXIiLAogICAgICAgICAgIm1pbmltdW0iOiAxLAogICAgICAgICAgImRlZmF1bHQiOiA5MTAwLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlN0YXJ0aW5nIHZhbHVlIGZvciB3b3Jrc3BhY2UgQ01VWF9QT1JUIGFzc2lnbm1lbnRzLiIKICAgICAgICB9LAogICAgICAgICJwb3J0UmFuZ2UiOiB7CiAgICAgICAgICAidHlwZSI6ICJpbnRlZ2VyIiwKICAgICAgICAgICJtaW5pbXVtIjogMSwKICAgICAgICAgICJkZWZh", - "dWx0IjogMTAsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiTnVtYmVyIG9mIHBvcnRzIHJlc2VydmVkIHBlciB3b3Jrc3BhY2UuIgogICAgICAgIH0KICAgICAgfQogICAgfSwKICAgICJicm93c2VyIjogewogICAgICAieC1jbXV4LXNjb3BlcyI6IFsiZ2xvYmFsIl0sCiAgICAgICJ0aXRsZSI6ICJicm93c2VyIiwKICAgICAgImRlc2NyaXB0aW9uIjogIkVtYmVkZGVkIGJyb3dzZXIgc2V0dGluZ3MgZnJvbSBTZXR0aW5ncyA+IEJyb3dzZXIuIiwKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgImFkZGl0aW9uYWxQcm9w", - "ZXJ0aWVzIjogZmFsc2UsCiAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICJkZWZhdWx0U2VhcmNoRW5naW5lIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJlbnVtIjogWyJnb29nbGUiLCAiZHVja2R1Y2tnbyIsICJiaW5nIiwgImthZ2kiLCAic3RhcnRwYWdlIiwgImJyYXZlIiwgInBlcnBsZXhpdHkiLCAiZXhhIiwgInlhaG9vIiwgImVjb3NpYSIsICJxd2FudCIsICJtb2plZWsiLCAid2lraXBlZGlhIiwgImdpdGh1YiIsICJiYWlkdSIsICJ5YW5kZXgiLCAiY3VzdG9tIl0sCiAgICAgICAg", - "ICAiZGVmYXVsdCI6ICJnb29nbGUiLAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5icm93c2VyLmRlZmF1bHRTZWFyY2hFbmdpbmUiLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkRlZmF1bHQgc2VhcmNoIGVuZ2luZSBmb3Igbm9uLVVSTCBicm93c2VyIGFkZHJlc3MgYmFyIHF1ZXJpZXMuIFVzZSBjdXN0b20gd2l0aCBjdXN0b21TZWFyY2hFbmdpbmVVUkxUZW1wbGF0ZSBmb3IgYXJiaXRyYXJ5IHByb3ZpZGVycy4iCiAgICAgICAgfSwKICAgICAgICAiY3VzdG9tU2VhcmNoRW5naW5l", - "TmFtZSI6IHsKICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAiZGVmYXVsdCI6ICIiLAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5icm93c2VyLmN1c3RvbVNlYXJjaEVuZ2luZU5hbWUiLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkRpc3BsYXkgbmFtZSB1c2VkIHdoZW4gZGVmYXVsdFNlYXJjaEVuZ2luZSBpcyBjdXN0b20uIgogICAgICAgIH0sCiAgICAgICAgImN1c3RvbVNlYXJjaEVuZ2luZVVSTFRlbXBsYXRlIjogewogICAgICAgICAgInR5cGUiOiAic3Ry", - "aW5nIiwKICAgICAgICAgICJkZWZhdWx0IjogImh0dHBzOi8vd3d3Lmdvb2dsZS5jb20vc2VhcmNoP3E9e3F1ZXJ5fSIsCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLmJyb3dzZXIuY3VzdG9tU2VhcmNoRW5naW5lVVJMVGVtcGxhdGUiLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlNlYXJjaCBVUkwgdXNlZCB3aGVuIGRlZmF1bHRTZWFyY2hFbmdpbmUgaXMgY3VzdG9tLiBJbmNsdWRlIHtxdWVyeX0gb3IgJXMgZm9yIHRoZSBlbmNvZGVkIHF1ZXJ5LiBJZiBvbWl0dGVkLCBjbXV4IGFw", - "cGVuZHMgcT0gdG8gdGhlIFVSTC4iCiAgICAgICAgfSwKICAgICAgICAic2hvd1NlYXJjaFN1Z2dlc3Rpb25zIjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiU2hvdyBvbW5pYmFyIHNlYXJjaCBzdWdnZXN0aW9ucy4iCiAgICAgICAgfSwKICAgICAgICAidGhlbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAgICAgImVudW0iOiBbInN5c3RlbSIsICJsaWdodCIsICJkYXJrIl0sCiAgICAgICAgICAi", - "ZGVmYXVsdCI6ICJzeXN0ZW0iLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkVtYmVkZGVkIGJyb3dzZXIgdGhlbWUuIgogICAgICAgIH0sCiAgICAgICAgImRlZmF1bHRab29tTGV2ZWwiOiB7CiAgICAgICAgICAidHlwZSI6ICJudW1iZXIiLAogICAgICAgICAgIm1pbmltdW0iOiAwLjI1LAogICAgICAgICAgIm1heGltdW0iOiA1LAogICAgICAgICAgImRlZmF1bHQiOiAxLAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5icm93c2VyLmRlZmF1bHRab29tTGV2ZWwiLAogICAgICAgICAg", - "ImRlc2NyaXB0aW9uIjogIkRlZmF1bHQgcGFnZSB6b29tIGZhY3RvciBhcHBsaWVkIHRvIG5ld2x5IG9wZW5lZCBicm93c2VyIHBhZ2VzICgxID0gMTAwJSkuIFpvb20gSW4sIFpvb20gT3V0LCBhbmQgQWN0dWFsIFNpemUgc3RpbGwgYWRqdXN0IGVhY2ggcGFnZS4iCiAgICAgICAgfSwKICAgICAgICAiZGlzY2FyZEhpZGRlbldlYlZpZXdzIjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiQWxsb3cgaGlkZGVuIGJyb3dzZXIg", - "dGFicyB0byByZWxlYXNlIHBhZ2UgbWVtb3J5IGFuZCByZXN0b3JlIHdoZW4gc2hvd24gYWdhaW4uIgogICAgICAgIH0sCiAgICAgICAgImhpZGRlbldlYlZpZXdEaXNjYXJkRGVsYXlTZWNvbmRzIjogewogICAgICAgICAgInR5cGUiOiAibnVtYmVyIiwKICAgICAgICAgICJtaW5pbXVtIjogMCwKICAgICAgICAgICJtYXhpbXVtIjogMzYwMCwKICAgICAgICAgICJkZWZhdWx0IjogMzAwLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlNlY29uZHMgYSBicm93c2VyIHRhYiBtdXN0IHN0YXkgaGlkZGVuIGJlZm9yZSBjbXV4IGZy", - "ZWVzIGl0cyBwYWdlIG1lbW9yeS4iCiAgICAgICAgfSwKICAgICAgICAiYXNrV2hlcmVUb1NhdmVEb3dubG9hZHMiOiB7CiAgICAgICAgICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogZmFsc2UsCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLmJyb3dzZXIuYXNrV2hlcmVUb1NhdmVEb3dubG9hZHMiLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlNob3cgYSBzYXZlIHBhbmVsIGZvciBicm93c2VyIGRvd25sb2FkcyBpbnN0ZWFkIG9mIHNhdmluZyBkaXJlY3Rs", - "eSB0byBEb3dubG9hZHMuIgogICAgICAgIH0sCiAgICAgICAgIm9wZW5UZXJtaW5hbExpbmtzSW5DbXV4QnJvd3NlciI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiB0cnVlLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIk9wZW4gY2xpY2tlZCB0ZXJtaW5hbCBsaW5rcyBpbiB0aGUgZW1iZWRkZWQgYnJvd3Nlci4iCiAgICAgICAgfSwKICAgICAgICAiaW50ZXJjZXB0VGVybWluYWxPcGVuQ29tbWFuZEluQ211eEJyb3dzZXIiOiB7CiAgICAgICAgICAidHlwZSI6ICJib29sZWFu", - "IiwKICAgICAgICAgICJkZWZhdWx0IjogdHJ1ZSwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJJbnRlcmNlcHQgdGVybWluYWwgb3BlbiBodHRwKHMpIGNvbW1hbmRzIGFuZCByb3V0ZSB0aGVtIHRocm91Z2ggdGhlIGVtYmVkZGVkIGJyb3dzZXIuIgogICAgICAgIH0sCiAgICAgICAgImhvc3RzVG9PcGVuSW5FbWJlZGRlZEJyb3dzZXIiOiB7CiAgICAgICAgICAidHlwZSI6ICJhcnJheSIsCiAgICAgICAgICAiaXRlbXMiOiB7CiAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgIH0sCiAgICAgICAgICAiZGVm", - "YXVsdCI6IFtdLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkFsbG93bGlzdCBvZiBob3N0cyB0aGF0IHNob3VsZCBzdGF5IGluc2lkZSB0aGUgZW1iZWRkZWQgYnJvd3Nlci4iCiAgICAgICAgfSwKICAgICAgICAidXJsc1RvQWx3YXlzT3BlbkV4dGVybmFsbHkiOiB7CiAgICAgICAgICAidHlwZSI6ICJhcnJheSIsCiAgICAgICAgICAiaXRlbXMiOiB7CiAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIKICAgICAgICAgIH0sCiAgICAgICAgICAiZGVmYXVsdCI6IFtdLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlJ1bGVz", - "IHRoYXQgYWx3YXlzIG9wZW4gbWF0Y2hpbmcgVVJMcyBpbiB0aGUgc3lzdGVtIGJyb3dzZXIuIgogICAgICAgIH0sCiAgICAgICAgImluc2VjdXJlSHR0cEhvc3RzQWxsb3dlZEluRW1iZWRkZWRCcm93c2VyIjogewogICAgICAgICAgInR5cGUiOiAiYXJyYXkiLAogICAgICAgICAgIml0ZW1zIjogewogICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICB9LAogICAgICAgICAgImRlZmF1bHQiOiBbCiAgICAgICAgICAgICJsb2NhbGhvc3QiLAogICAgICAgICAgICAiKi5sb2NhbGhvc3QiLAogICAgICAgICAgICAi", - "MTI3LjAuMC4xIiwKICAgICAgICAgICAgIjo6MSIsCiAgICAgICAgICAgICIwLjAuMC4wIiwKICAgICAgICAgICAgIioubG9jYWx0ZXN0Lm1lIgogICAgICAgICAgXSwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJIVFRQIGhvc3RzIGFsbG93ZWQgaW4gdGhlIGVtYmVkZGVkIGJyb3dzZXIgd2l0aG91dCBhIHdhcm5pbmcgcHJvbXB0LiIKICAgICAgICB9LAogICAgICAgICJ1cmxBbGxvd2xpc3QiOiB7CiAgICAgICAgICAidHlwZSI6ICJhcnJheSIsCiAgICAgICAgICAiaXRlbXMiOiB7CiAgICAgICAgICAgICJ0eXBlIjogInN0", - "cmluZyIKICAgICAgICAgIH0sCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLmJyb3dzZXIudXJsQWxsb3dsaXN0IiwKICAgICAgICAgICJkZWZhdWx0IjogWwogICAgICAgICAgICAibG9jYWxob3N0IiwKICAgICAgICAgICAgIioubG9jYWxob3N0IiwKICAgICAgICAgICAgIjEyNy4wLjAuMSIsCiAgICAgICAgICAgICI6OjEiLAogICAgICAgICAgICAiMC4wLjAuMCIsCiAgICAgICAgICAgICIqLmxvY2FsdGVzdC5tZSIKICAgICAgICAgIF0sCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAi", - "SG9zdCBvciBVUkwgcGF0dGVybnMgdGhhdCByZXN0cmljdCBlbWJlZGRlZC1icm93c2VyIG5hdmlnYXRpb24uIFRoZSBTZXR0aW5ncyBVSSBzdWdnZXN0cyBsb2NhbCBkZXZlbG9wbWVudCBvcmlnaW5zOyBzYXZpbmcgYSBsaXN0IG9wdHMgaW50byB0aGUgb3B0aW9uYWwgcmVzdHJpY3Rpb24uIFJlbW92ZSBlbnRyaWVzIHRvIGJsb2NrIHRoZW0sIG9yIGxlYXZlIHRoZSB1c2VyIHZhbHVlIGVtcHR5IHRvIGRpc2FibGUgaXQgd2hlbiBubyBtYW5hZ2VkIHBvbGljeSBhcHBsaWVzLiIKICAgICAgICB9LAogICAgICAgICJzaG93SW1w", - "b3J0SGludE9uQmxhbmtUYWJzIjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiU2hvdyB0aGUgYnJvd3NlciBpbXBvcnQgaGludCBvbiBibGFuayB0YWJzLiIKICAgICAgICB9LAogICAgICAgICJyZWFjdEdyYWJWZXJzaW9uIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJkZWZhdWx0IjogIjAuMS4yOSIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiUGlubmVkIHJlYWN0LWdyYWIgdmVyc2lv", - "biBmb3IgdGhlIGJyb3dzZXIgdG9vbGJhciBoZWxwZXIuIgogICAgICAgIH0KICAgICAgfQogICAgfSwKICAgICJtb2JpbGUiOiB7CiAgICAgICJ4LWNtdXgtc2NvcGVzIjogWyJnbG9iYWwiXSwKICAgICAgInRpdGxlIjogIm1vYmlsZSIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJNYWMtc2lkZSBzZXR0aW5ncyBmb3IgY211eCBvbiBpT1MuIiwKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjogZmFsc2UsCiAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICJhcnRpZmFjdEZvbGRl", - "ckFjY2VzcyI6IHsKICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAiZW51bSI6IFsic3VidHJlZSIsICJvbmVMZXZlbCJdLAogICAgICAgICAgImRlZmF1bHQiOiAic3VidHJlZSIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiQ29udHJvbHMgd2hldGhlciBhIHJlZmVyZW5jZWQgb3IgdGVybWluYWwtdmlzaWJsZSBkaXJlY3RvcnkgYXV0aG9yaXplcyBpdHMgZnVsbCBjYW5vbmljYWwgc3VidHJlZSBvciBvbmx5IGltbWVkaWF0ZSBjaGlsZHJlbi4iCiAgICAgICAgfQogICAgICB9CiAgICB9LAogICAgIm1h", - "cmtkb3duIjogewogICAgICAieC1jbXV4LXNjb3BlcyI6IFsiZ2xvYmFsIl0sCiAgICAgICJ0aXRsZSI6ICJtYXJrZG93biIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJCdWlsdC1pbiBtYXJrZG93biB2aWV3ZXIgc2V0dGluZ3MuIiwKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjogZmFsc2UsCiAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICJmb250U2l6ZSI6IHsKICAgICAgICAgICJ0eXBlIjogImludGVnZXIiLAogICAgICAgICAgIm1pbmltdW0iOiA4LAogICAgICAgICAg", - "Im1heGltdW0iOiA5NiwKICAgICAgICAgICJkZWZhdWx0IjogMTUsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiRGVmYXVsdCBib2R5IGZvbnQgc2l6ZSwgaW4gcG9pbnRzLCBmb3IgbmV3bHkgb3BlbmVkIG1hcmtkb3duIHZpZXdlcnMuIFpvb20gYSB2aWV3ZXIgbGl2ZSB3aXRoIENtZC0rIC8gQ21kLS0gLyBDbWQtMC4iLAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5tYXJrZG93bi5mb250U2l6ZSIKICAgICAgICB9LAogICAgICAgICJmb250RmFtaWx5IjogewogICAgICAgICAgInR5", - "cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJkZWZhdWx0IjogIiIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiRGVmYXVsdCBib2R5IGZvbnQgZmFtaWx5IGZvciBuZXdseSBvcGVuZWQgbWFya2Rvd24gdmlld2Vycy4gTGVhdmUgZW1wdHkgZm9yIHRoZSBzeXN0ZW0gbWFya2Rvd24gZm9udCBzdGFjay4iLAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5tYXJrZG93bi5mb250RmFtaWx5IgogICAgICAgIH0sCiAgICAgICAgIm1heFdpZHRoIjogewogICAgICAgICAgInR5cGUiOiAiaW50", - "ZWdlciIsCiAgICAgICAgICAibWluaW11bSI6IDMyMCwKICAgICAgICAgICJtYXhpbXVtIjogMjQwMCwKICAgICAgICAgICJkZWZhdWx0IjogOTgwLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkRlZmF1bHQgbWF4aW11bSByZWFkaW5nIGNvbHVtbiB3aWR0aCwgaW4gQ1NTIHBpeGVscywgZm9yIG5ld2x5IG9wZW5lZCBtYXJrZG93biB2aWV3ZXJzLiIsCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLm1hcmtkb3duLm1heFdpZHRoIgogICAgICAgIH0KICAgICAgfQogICAgfSwKICAgICJj", - "YW52YXMiOiB7CiAgICAgICJ4LWNtdXgtc2NvcGVzIjogWyJnbG9iYWwiXSwKICAgICAgInRpdGxlIjogImNhbnZhcyIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJGcmVlZm9ybSBjYW52YXMgd29ya3NwYWNlIGxheW91dCBzZXR0aW5ncy4iLAogICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiBmYWxzZSwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgInBhbmVHYXAiOiB7CiAgICAgICAgICAidHlwZSI6ICJpbnRlZ2VyIiwKICAgICAgICAgICJtaW5pbXVtIjogMCwKICAgICAg", - "ICAgICJtYXhpbXVtIjogNjQsCiAgICAgICAgICAiZGVmYXVsdCI6IDE2LAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlNwYWNpbmcgYmV0d2VlbiBwYW5lcyBpbiB0aGUgY2FudmFzIGxheW91dCwgaW4gcG9pbnRzLiBTbmFwcGluZywgdGlkeSwgYW5kIG5ldy1wYW5lIHBsYWNlbWVudCBhbGwgdXNlIHRoaXMgb25lIGdhcC4iLAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5jYW52YXMucGFuZUdhcCIKICAgICAgICB9LAogICAgICAgICJzbmFwcGluZ0VuYWJsZWQiOiB7CiAgICAgICAg", - "ICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogdHJ1ZSwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTbmFwIHBhbmUgZHJhZ3MgYW5kIHJlc2l6ZXMgdG8gbmVpZ2hib3IgZWRnZXMgYW5kIHRoZSBwYW5lIGdhcC4gSG9sZCBDb21tYW5kIHRvIHN1c3BlbmQgc25hcHBpbmcgZm9yIG9uZSBnZXN0dXJlLiIsCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLmNhbnZhcy5zbmFwcGluZ0VuYWJsZWQiCiAgICAgICAgfQogICAgICB9CiAgICB9LAogICAgImZpbGVFZGl0", - "b3IiOiB7CiAgICAgICJ4LWNtdXgtc2NvcGVzIjogWyJnbG9iYWwiXSwKICAgICAgInRpdGxlIjogImZpbGVFZGl0b3IiLAogICAgICAiZGVzY3JpcHRpb24iOiAiQnVpbHQtaW4gZmlsZSBlZGl0b3Igc2V0dGluZ3MuIiwKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjogZmFsc2UsCiAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICJ3b3JkV3JhcCI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiBmYWxzZSwKICAgICAgICAgICJk", - "ZXNjcmlwdGlvbiI6ICJXcmFwIGxvbmcgbGluZXMgYXQgdGhlIGVkaXRvcidzIHJpZ2h0IGVkZ2UgaW5zdGVhZCBvZiBzY3JvbGxpbmcgaG9yaXpvbnRhbGx5LiIsCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLmZpbGVFZGl0b3Iud29yZFdyYXAiCiAgICAgICAgfSwKICAgICAgICAic3ludGF4SGlnaGxpZ2h0aW5nIjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiQ29sb3Igc291cmNl", - "IHRva2VucyBpbiB0aGUgYnVpbHQtaW4gZmlsZSBlZGl0b3IuIiwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMuZmlsZUVkaXRvci5zeW50YXhIaWdobGlnaHRpbmciCiAgICAgICAgfSwKICAgICAgICAibGluZU51bWJlcnMiOiB7CiAgICAgICAgICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogdHJ1ZSwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTaG93IGEgbGluZS1udW1iZXIgZ3V0dGVyIGluIHRoZSBidWlsdC1pbiBmaWxlIGVkaXRvci4iLAogICAgICAg", - "ICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5maWxlRWRpdG9yLmxpbmVOdW1iZXJzIgogICAgICAgIH0sCiAgICAgICAgImluZGVudEd1aWRlcyI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiB0cnVlLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkRyYXcgdmVydGljYWwgaW5kZW50IGd1aWRlcyBpbiB0aGUgYnVpbHQtaW4gZmlsZSBlZGl0b3IuIiwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMuZmlsZUVkaXRv", - "ci5pbmRlbnRHdWlkZXMiCiAgICAgICAgfSwKICAgICAgICAiY3VycmVudExpbmVIaWdobGlnaHQiOiB7CiAgICAgICAgICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogdHJ1ZSwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJIaWdobGlnaHQgdGhlIGNhcmV0J3MgbGluZSB3aGVuIHRoZSBzZWxlY3Rpb24gaXMgZW1wdHkuIiwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMuZmlsZUVkaXRvci5jdXJyZW50TGluZUhpZ2hsaWdodCIKICAgICAgICB9LAogICAgICAg", - "ICJ0YWJXaWR0aCI6IHsKICAgICAgICAgICJ0eXBlIjogImludGVnZXIiLAogICAgICAgICAgImRlZmF1bHQiOiA0LAogICAgICAgICAgIm1pbmltdW0iOiAxLAogICAgICAgICAgIm1heGltdW0iOiA4LAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkNvbHVtbnMgcGVyIHRhYiBzdG9wIGZvciBpbmRlbnQgZ3VpZGVzLiIsCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLmZpbGVFZGl0b3IudGFiV2lkdGgiCiAgICAgICAgfQogICAgICB9CiAgICB9LAogICAgImZpbGVFeHBsb3JlciI6IHsK", - "ICAgICAgIngtY211eC1zY29wZXMiOiBbImdsb2JhbCJdLAogICAgICAidGl0bGUiOiAiZmlsZUV4cGxvcmVyIiwKICAgICAgImRlc2NyaXB0aW9uIjogIlJpZ2h0LXNpZGViYXIgZmlsZSBleHBsb3JlciAoZmlsZSB0cmVlKSBzZXR0aW5ncy4iLAogICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLmZpbGVFeHBsb3Jlci5kZXNjcmlwdGlvbiIsCiAgICAgICJ0eXBlIjogIm9iamVjdCIsCiAgICAgICJhZGRpdGlvbmFsUHJvcGVydGllcyI6IGZhbHNlLAogICAgICAicHJvcGVydGllcyI6IHsKICAgICAg", - "ICAiZG91YmxlQ2xpY2tBY3Rpb24iOiB7CiAgICAgICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAgICAgImVudW0iOiBbInByZXZpZXciLCAiZGVmYXVsdEVkaXRvciIsICJwcmVmZXJyZWRFZGl0b3IiXSwKICAgICAgICAgICJkZWZhdWx0IjogInByZXZpZXciLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIldoYXQgZG91YmxlLWNsaWNraW5nIChvciBwcmVzc2luZyBSZXR1cm4gb24gYSBzZWFyY2ggcmVzdWx0IGZvcikgYSBmaWxlIGluIHRoZSBmaWxlIGV4cGxvcmVyIGRvZXMuIGBwcmV2aWV3YCBvcGVucyB0aGUgYnVp", - "bHQtaW4gY211eCBmaWxlIHByZXZpZXcgKHRoZSBkZWZhdWx0IGFuZCBoaXN0b3JpY2FsIGJlaGF2aW9yKS4gYGRlZmF1bHRFZGl0b3JgIG9wZW5zIHdpdGggdGhlIG1hY09TIGRlZmF1bHQgYXBwIGZvciB0aGUgZmlsZSB0eXBlLiBgcHJlZmVycmVkRWRpdG9yYCBvcGVucyB3aXRoIHRoZSBgYXBwLnByZWZlcnJlZEVkaXRvcmAgY29tbWFuZCwgZmFsbGluZyBiYWNrIHRvIHRoZSBkZWZhdWx0IGFwcCB3aGVuIG5vbmUgaXMgc2V0LiBPbmx5IGFwcGxpZXMgdG8gZmlsZXM7IGRpcmVjdG9yaWVzIGFsd2F5cyBleHBhbmQvY29sbGFw", - "c2UsIGFuZCBub24tbG9jYWwgKHJlbW90ZSkgZmlsZSBleHBsb3JlcnMgYWx3YXlzIG9wZW4gdGhlIGNtdXggcHJldmlldy4iLAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5maWxlRXhwbG9yZXIuZG91YmxlQ2xpY2tBY3Rpb24iCiAgICAgICAgfQogICAgICB9CiAgICB9LAogICAgImRpZmZWaWV3ZXIiOiB7CiAgICAgICJ4LWNtdXgtc2NvcGVzIjogWyJnbG9iYWwiXSwKICAgICAgInRpdGxlIjogImRpZmZWaWV3ZXIiLAogICAgICAiZGVzY3JpcHRpb24iOiAiQnVpbHQtaW4gZGlmZiB2", - "aWV3ZXIgc2V0dGluZ3MuIiwKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjogZmFsc2UsCiAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICJkZWZhdWx0TGF5b3V0IjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJlbnVtIjogWyJ1bmlmaWVkIiwgInNwbGl0Il0sCiAgICAgICAgICAiZGVmYXVsdCI6ICJ1bmlmaWVkIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJEZWZhdWx0IGxheW91dCBmb3IgbmV3bHkgb3BlbmVkIGRpZmYgdmlld2Vycy4g", - "VGhlIHRvb2xiYXIgbGF5b3V0IHRvZ2dsZSBwZXJzaXN0cyB0aGUgdXNlcidzIGxhc3QgY2hvaWNlOyBhbiBleHBsaWNpdCBgY211eCBkaWZmIC0tbGF5b3V0YCBvcHRpb24gb3ZlcnJpZGVzIGJvdGguIiwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMuZGlmZlZpZXdlci5kZWZhdWx0TGF5b3V0IgogICAgICAgIH0KICAgICAgfQogICAgfSwKICAgICJyaWdodFNpZGViYXIiOiB7CiAgICAgICJ0aXRsZSI6ICJyaWdodFNpZGViYXIiLAogICAgICAiZGVzY3JpcHRpb24iOiAiTGVnYWN5IGFu", - "ZCBleHRlbnNpb24tb3duZWQgcmlnaHQtc2lkZWJhciBjb25maWd1cmF0aW9uIHByZXNlcnZlZCBpbiBjbXV4Lmpzb24uIiwKICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5yaWdodFNpZGViYXIiLAogICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB0cnVlCiAgICB9LAogICAgInNob3J0Y3V0cyI6IHsKICAgICAgIngtY211eC1zY29wZXMiOiBbImdsb2JhbCJdLAogICAgICAidGl0bGUiOiAic2hvcnRjdXRzIiwKICAgICAgImRlc2NyaXB0aW9uIjog", - "IktleWJvYXJkIHNob3J0Y3V0IHNldHRpbmdzIGZyb20gU2V0dGluZ3MgPiBLZXlib2FyZCBTaG9ydGN1dHMuIiwKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjogZmFsc2UsCiAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICJzaG93TW9kaWZpZXJIb2xkSGludHMiOiB7CiAgICAgICAgICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogdHJ1ZSwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTaG93IHNob3J0Y3V0LWhpbnQgY2hpcHMgd2hpbGUgaG9s", - "ZGluZyBDbWQgb3IgQ29udHJvbC4iLAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5zaG9ydGN1dHMuc2hvd01vZGlmaWVySG9sZEhpbnRzIgogICAgICAgIH0sCiAgICAgICAgImJpbmRpbmdzIjogewogICAgICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgICAgICJkZWZhdWx0Ijoge30sCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiU2hvcnRjdXQgb3ZlcnJpZGVzIGtleWVkIGJ5IGNtdXggYWN0aW9uIGlkLiBVc2UgYSBzdHJpbmcgZm9yIGEgc2luZ2xlIHNob3J0Y3V0LCBhbiBh", - "cnJheSBmb3IgYSBjaG9yZCwgbnVsbCwgYW4gZW1wdHkgc3RyaW5nLCBub25lLCBjbGVhciwgdW5ib3VuZCwgb3IgZGlzYWJsZWQgdG8gdW5iaW5kLiIsCiAgICAgICAgICAicHJvcGVydHlOYW1lcyI6IHsKICAgICAgICAgICAgImVudW0iOiBbCiAgICAgICAgICAgICAgIm9wZW5TZXR0aW5ncyIsCiAgICAgICAgICAgICAgIm9wZW5UZWFtUGlja2VyIiwKICAgICAgICAgICAgICAicmVsb2FkQ29uZmlndXJhdGlvbiIsCiAgICAgICAgICAgICAgInNob3dIaWRlQWxsV2luZG93cyIsCiAgICAgICAgICAgICAgImdsb2JhbFNlYXJj", - "aCIsCiAgICAgICAgICAgICAgIm5ld1dpbmRvdyIsCiAgICAgICAgICAgICAgImNsb3NlV2luZG93IiwKICAgICAgICAgICAgICAidG9nZ2xlRnVsbFNjcmVlbiIsCiAgICAgICAgICAgICAgInF1aXQiLAogICAgICAgICAgICAgICJ0b2dnbGVTaWRlYmFyIiwKICAgICAgICAgICAgICAibmV3VGFiIiwKICAgICAgICAgICAgICAibmV3QnJvd3NlcldvcmtzcGFjZSIsCiAgICAgICAgICAgICAgIm5ld0Nsb3VkV29ya3NwYWNlIiwKICAgICAgICAgICAgICAibmV3Q2xvdWRNYWNoaW5lIiwKICAgICAgICAgICAgICAic2F2ZUxheW91", - "dFRlbXBsYXRlIiwKICAgICAgICAgICAgICAib3BlbkZvbGRlciIsCiAgICAgICAgICAgICAgInJlb3BlblByZXZpb3VzU2Vzc2lvbiIsCiAgICAgICAgICAgICAgImdvVG9Xb3Jrc3BhY2UiLAogICAgICAgICAgICAgICJjb21tYW5kUGFsZXR0ZSIsCiAgICAgICAgICAgICAgImNvbW1hbmRQYWxldHRlTmV4dCIsCiAgICAgICAgICAgICAgImNvbW1hbmRQYWxldHRlUHJldmlvdXMiLAogICAgICAgICAgICAgICJzZW5kRmVlZGJhY2siLAogICAgICAgICAgICAgICJzaG93Tm90aWZpY2F0aW9ucyIsCiAgICAgICAgICAgICAgImp1", - "bXBUb1VucmVhZCIsCiAgICAgICAgICAgICAgInRvZ2dsZVVucmVhZCIsCiAgICAgICAgICAgICAgIm1hcmtPbGRlc3RVbnJlYWRBbmRKdW1wTmV4dCIsCiAgICAgICAgICAgICAgIm1hcmtBbGxOb3RpZmljYXRpb25zUmVhZCIsCiAgICAgICAgICAgICAgImNsZWFyQWxsTm90aWZpY2F0aW9ucyIsCiAgICAgICAgICAgICAgImZvY3VzUmlnaHRTaWRlYmFyIiwKICAgICAgICAgICAgICAic3dpdGNoUmlnaHRTaWRlYmFyVG9GaWxlcyIsCiAgICAgICAgICAgICAgInN3aXRjaFJpZ2h0U2lkZWJhclRvRmluZCIsCiAgICAgICAgICAg", - "ICAgInN3aXRjaFJpZ2h0U2lkZWJhclRvU2Vzc2lvbnMiLAogICAgICAgICAgICAgICJzd2l0Y2hSaWdodFNpZGViYXJUb0ZlZWQiLAogICAgICAgICAgICAgICJzd2l0Y2hSaWdodFNpZGViYXJUb0RvY2siLAogICAgICAgICAgICAgICJzd2l0Y2hSaWdodFNpZGViYXJUb01hY2hpbmVzIiwKICAgICAgICAgICAgICAidHJpZ2dlckZsYXNoIiwKICAgICAgICAgICAgICAibmV4dFN1cmZhY2UiLAogICAgICAgICAgICAgICJwcmV2U3VyZmFjZSIsCiAgICAgICAgICAgICAgIm1vdmVTdXJmYWNlTGVmdCIsCiAgICAgICAgICAgICAg", - "Im1vdmVTdXJmYWNlUmlnaHQiLAogICAgICAgICAgICAgICJtb3ZlU3VyZmFjZVRvUHJldmlvdXNQYW5lIiwKICAgICAgICAgICAgICAibW92ZVN1cmZhY2VUb05leHRQYW5lIiwKICAgICAgICAgICAgICAibW92ZVN1cmZhY2VUb1BhbmVMZWZ0IiwKICAgICAgICAgICAgICAibW92ZVN1cmZhY2VUb1BhbmVSaWdodCIsCiAgICAgICAgICAgICAgIm1vdmVTdXJmYWNlVG9QYW5lVXAiLAogICAgICAgICAgICAgICJtb3ZlU3VyZmFjZVRvUGFuZURvd24iLAogICAgICAgICAgICAgICJzZWxlY3RTdXJmYWNlQnlOdW1iZXIiLAogICAg", - "ICAgICAgICAgICJuZXh0U2lkZWJhclRhYiIsCiAgICAgICAgICAgICAgInByZXZTaWRlYmFyVGFiIiwKICAgICAgICAgICAgICAibmV4dFNpZGViYXJUYWJJbkdyb3VwIiwKICAgICAgICAgICAgICAicHJldlNpZGViYXJUYWJJbkdyb3VwIiwKICAgICAgICAgICAgICAibW92ZVdvcmtzcGFjZVVwIiwKICAgICAgICAgICAgICAibW92ZVdvcmtzcGFjZURvd24iLAogICAgICAgICAgICAgICJmb2N1c0hpc3RvcnlCYWNrIiwKICAgICAgICAgICAgICAiZm9jdXNIaXN0b3J5Rm9yd2FyZCIsCiAgICAgICAgICAgICAgImZvY3VzSGlz", - "dG9yeUxhc3QiLAogICAgICAgICAgICAgICJzZWxlY3RXb3Jrc3BhY2VCeU51bWJlciIsCiAgICAgICAgICAgICAgInJlbmFtZVRhYiIsCiAgICAgICAgICAgICAgInJlbmFtZVdvcmtzcGFjZSIsCiAgICAgICAgICAgICAgImVkaXRXb3Jrc3BhY2VEZXNjcmlwdGlvbiIsCiAgICAgICAgICAgICAgIm1hcmtXb3Jrc3BhY2VEb25lIiwKICAgICAgICAgICAgICAiY3ljbGVXb3Jrc3BhY2VTdGF0dXMiLAogICAgICAgICAgICAgICJ0b2dnbGVDaGVja2xpc3RJdGVtQ29tcGxldGUiLAogICAgICAgICAgICAgICJjbG9zZVRhYiIsCiAg", - "ICAgICAgICAgICAgImNsb3NlT3RoZXJUYWJzSW5QYW5lIiwKICAgICAgICAgICAgICAiY2xvc2VXb3Jrc3BhY2UiLAogICAgICAgICAgICAgICJuZXdXb3Jrc3BhY2VHcm91cCIsCiAgICAgICAgICAgICAgImdyb3VwU2VsZWN0ZWRXb3Jrc3BhY2VzIiwKICAgICAgICAgICAgICAidG9nZ2xlRm9jdXNlZFdvcmtzcGFjZUdyb3VwQ29sbGFwc2VkIiwKICAgICAgICAgICAgICAicmVvcGVuQ2xvc2VkV29ya3NwYWNlIiwKICAgICAgICAgICAgICAicmVvcGVuQ2xvc2VkQnJvd3NlclBhbmVsIiwKICAgICAgICAgICAgICAibmV3U3Vy", - "ZmFjZSIsCiAgICAgICAgICAgICAgInRvZ2dsZVRlcm1pbmFsQ29weU1vZGUiLAogICAgICAgICAgICAgICJmb2N1c1RleHRCb3hJbnB1dCIsCiAgICAgICAgICAgICAgImN5Y2xlVGV4dEJveFN1Ym1pdEFjdGlvbiIsCiAgICAgICAgICAgICAgImF0dGFjaFRleHRCb3hGaWxlIiwKICAgICAgICAgICAgICAic2VuZEN0cmxGVG9UZXJtaW5hbCIsCiAgICAgICAgICAgICAgImNsZWFyU2NyZWVuS2VlcFNjcm9sbGJhY2siLAogICAgICAgICAgICAgICJzaW11bGF0b3JIb21lIiwKICAgICAgICAgICAgICAic2ltdWxhdG9yUm90YXRl", - "TGVmdCIsCiAgICAgICAgICAgICAgInNpbXVsYXRvclJvdGF0ZVJpZ2h0IiwKICAgICAgICAgICAgICAic2ltdWxhdG9yVG9nZ2xlQXBwZWFyYW5jZSIsCiAgICAgICAgICAgICAgInNpbXVsYXRvclRvZ2dsZVNvZnR3YXJlS2V5Ym9hcmQiLAogICAgICAgICAgICAgICJmb2N1c0xlZnQiLAogICAgICAgICAgICAgICJmb2N1c1JpZ2h0IiwKICAgICAgICAgICAgICAiZm9jdXNVcCIsCiAgICAgICAgICAgICAgImZvY3VzRG93biIsCiAgICAgICAgICAgICAgImZvY3VzUHJldmlvdXNQYW5lIiwKICAgICAgICAgICAgICAiZm9jdXNO", - "ZXh0UGFuZSIsCiAgICAgICAgICAgICAgInNwbGl0UmlnaHQiLAogICAgICAgICAgICAgICJzcGxpdERvd24iLAogICAgICAgICAgICAgICJ0b2dnbGVTcGxpdFpvb20iLAogICAgICAgICAgICAgICJpbmNyZWFzZVdvcmtzcGFjZVRlcm1pbmFsRm9udFNpemUiLAogICAgICAgICAgICAgICJkZWNyZWFzZVdvcmtzcGFjZVRlcm1pbmFsRm9udFNpemUiLAogICAgICAgICAgICAgICJyZXNldFdvcmtzcGFjZVRlcm1pbmFsRm9udFNpemUiLAogICAgICAgICAgICAgICJlcXVhbGl6ZVNwbGl0cyIsCiAgICAgICAgICAgICAgInJlc2l6", - "ZS1wYW5lLWxlZnQiLAogICAgICAgICAgICAgICJyZXNpemUtcGFuZS1yaWdodCIsCiAgICAgICAgICAgICAgInJlc2l6ZS1wYW5lLXVwIiwKICAgICAgICAgICAgICAicmVzaXplLXBhbmUtZG93biIsCiAgICAgICAgICAgICAgInNwbGl0QnJvd3NlclJpZ2h0IiwKICAgICAgICAgICAgICAic3BsaXRCcm93c2VyRG93biIsCiAgICAgICAgICAgICAgInRvZ2dsZUNhbnZhc0xheW91dCIsCiAgICAgICAgICAgICAgImNhbnZhc1JldmVhbEZvY3VzZWRQYW5lIiwKICAgICAgICAgICAgICAiY2FudmFzT3ZlcnZpZXciLAogICAgICAg", - "ICAgICAgICJjYW52YXNab29tSW4iLAogICAgICAgICAgICAgICJjYW52YXNab29tT3V0IiwKICAgICAgICAgICAgICAiY2FudmFzWm9vbVJlc2V0IiwKICAgICAgICAgICAgICAiY2FudmFzVGlkeSIsCiAgICAgICAgICAgICAgImNhbnZhc0FsaWduTGVmdCIsCiAgICAgICAgICAgICAgImNhbnZhc0FsaWduUmlnaHQiLAogICAgICAgICAgICAgICJjYW52YXNBbGlnblRvcCIsCiAgICAgICAgICAgICAgImNhbnZhc0FsaWduQm90dG9tIiwKICAgICAgICAgICAgICAiY2FudmFzRXF1YWxpemVXaWR0aHMiLAogICAgICAgICAgICAg", - "ICJjYW52YXNFcXVhbGl6ZUhlaWdodHMiLAogICAgICAgICAgICAgICJjYW52YXNEaXN0cmlidXRlSG9yaXpvbnRhbGx5IiwKICAgICAgICAgICAgICAiY2FudmFzRGlzdHJpYnV0ZVZlcnRpY2FsbHkiLAogICAgICAgICAgICAgICJ0b2dnbGVGaWxlRXhwbG9yZXIiLAogICAgICAgICAgICAgICJmaWxlRXhwbG9yZXJPcGVuU2VsZWN0aW9uIiwKICAgICAgICAgICAgICAiZmlsZUV4cGxvcmVyT3BlblNlbGVjdGlvbkZpbmRlckFsaWFzIiwKICAgICAgICAgICAgICAic2F2ZUZpbGVQcmV2aWV3IiwKICAgICAgICAgICAgICAidG9n", - "Z2xlRmlsZUVkaXRvcldvcmRXcmFwIiwKICAgICAgICAgICAgICAib3BlbkJyb3dzZXIiLAogICAgICAgICAgICAgICJmb2N1c0Jyb3dzZXJBZGRyZXNzQmFyIiwKICAgICAgICAgICAgICAiYnJvd3NlckJhY2siLAogICAgICAgICAgICAgICJicm93c2VyRm9yd2FyZCIsCiAgICAgICAgICAgICAgImJyb3dzZXJSZWxvYWQiLAogICAgICAgICAgICAgICJicm93c2VySGFyZFJlbG9hZCIsCiAgICAgICAgICAgICAgImJyb3dzZXJab29tSW4iLAogICAgICAgICAgICAgICJicm93c2VyWm9vbU91dCIsCiAgICAgICAgICAgICAgImJy", - "b3dzZXJab29tUmVzZXQiLAogICAgICAgICAgICAgICJtYXJrZG93blpvb21JbiIsCiAgICAgICAgICAgICAgIm1hcmtkb3duWm9vbU91dCIsCiAgICAgICAgICAgICAgIm1hcmtkb3duWm9vbVJlc2V0IiwKICAgICAgICAgICAgICAiZmluZCIsCiAgICAgICAgICAgICAgImZpbmRJbkRpcmVjdG9yeSIsCiAgICAgICAgICAgICAgImZpbmROZXh0IiwKICAgICAgICAgICAgICAiZmluZFByZXZpb3VzIiwKICAgICAgICAgICAgICAiaGlkZUZpbmQiLAogICAgICAgICAgICAgICJ1c2VTZWxlY3Rpb25Gb3JGaW5kIiwKICAgICAgICAg", - "ICAgICAidG9nZ2xlQnJvd3NlckRldmVsb3BlclRvb2xzIiwKICAgICAgICAgICAgICAic2hvd0Jyb3dzZXJKYXZhU2NyaXB0Q29uc29sZSIsCiAgICAgICAgICAgICAgInRvZ2dsZUJyb3dzZXJGb2N1c01vZGUiLAogICAgICAgICAgICAgICJ0b2dnbGVCcm93c2VyRGVzaWduTW9kZSIsCiAgICAgICAgICAgICAgInRvZ2dsZVJlYWN0R3JhYiIsCiAgICAgICAgICAgICAgIm9wZW5EaWZmVmlld2VyIiwKICAgICAgICAgICAgICAiZGlmZlZpZXdlclNjcm9sbERvd24iLAogICAgICAgICAgICAgICJkaWZmVmlld2VyU2Nyb2xsVXAi", - "LAogICAgICAgICAgICAgICJkaWZmVmlld2VyU2Nyb2xsSGFsZlBhZ2VEb3duIiwKICAgICAgICAgICAgICAiZGlmZlZpZXdlclNjcm9sbEhhbGZQYWdlVXAiLAogICAgICAgICAgICAgICJkaWZmVmlld2VyU2Nyb2xsRG93bkVtYWNzIiwKICAgICAgICAgICAgICAiZGlmZlZpZXdlclNjcm9sbFVwRW1hY3MiLAogICAgICAgICAgICAgICJkaWZmVmlld2VyU2Nyb2xsVG9Cb3R0b20iLAogICAgICAgICAgICAgICJkaWZmVmlld2VyU2Nyb2xsVG9Ub3AiLAogICAgICAgICAgICAgICJkaWZmVmlld2VyT3BlbkZpbGVTZWFyY2giLAog", - "ICAgICAgICAgICAgICJkaWZmVmlld2VyTmV4dEZpbGUiLAogICAgICAgICAgICAgICJkaWZmVmlld2VyUHJldmlvdXNGaWxlIgogICAgICAgICAgICBdCiAgICAgICAgICB9LAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJmaWxlRXhwbG9yZXJPcGVuU2VsZWN0aW9uIjogewogICAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvc2luZ2xlQmFyZUZpcnN0U3Ryb2tlU2hvcnRjdXRCaW5kaW5nTnVsbGFibGUiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJmaWxlRXhwbG9yZXJPcGVuU2VsZWN0aW9u", - "RmluZGVyQWxpYXMiOiB7CiAgICAgICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9zaW5nbGVCYXJlRmlyc3RTdHJva2VTaG9ydGN1dEJpbmRpbmdOdWxsYWJsZSIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgImRpZmZWaWV3ZXJTY3JvbGxEb3duIjogewogICAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvYmFyZUZpcnN0U3Ryb2tlU2hvcnRjdXRCaW5kaW5nTnVsbGFibGUiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJkaWZmVmlld2VyU2Nyb2xsVXAiOiB7CiAgICAgICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9i", - "YXJlRmlyc3RTdHJva2VTaG9ydGN1dEJpbmRpbmdOdWxsYWJsZSIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgImRpZmZWaWV3ZXJTY3JvbGxIYWxmUGFnZURvd24iOiB7CiAgICAgICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9iYXJlRmlyc3RTdHJva2VTaG9ydGN1dEJpbmRpbmdOdWxsYWJsZSIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgImRpZmZWaWV3ZXJTY3JvbGxIYWxmUGFnZVVwIjogewogICAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvYmFyZUZpcnN0U3Ryb2tlU2hvcnRjdXRCaW5kaW5nTnVsbGFibGUi", - "CiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJkaWZmVmlld2VyU2Nyb2xsRG93bkVtYWNzIjogewogICAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvYmFyZUZpcnN0U3Ryb2tlU2hvcnRjdXRCaW5kaW5nTnVsbGFibGUiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJkaWZmVmlld2VyU2Nyb2xsVXBFbWFjcyI6IHsKICAgICAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL2JhcmVGaXJzdFN0cm9rZVNob3J0Y3V0QmluZGluZ051bGxhYmxlIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiZGlmZlZpZXdlclNjcm9s", - "bFRvQm90dG9tIjogewogICAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvYmFyZUZpcnN0U3Ryb2tlU2hvcnRjdXRCaW5kaW5nTnVsbGFibGUiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJkaWZmVmlld2VyU2Nyb2xsVG9Ub3AiOiB7CiAgICAgICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9iYXJlRmlyc3RTdHJva2VTaG9ydGN1dEJpbmRpbmdOdWxsYWJsZSIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgImRpZmZWaWV3ZXJPcGVuRmlsZVNlYXJjaCI6IHsKICAgICAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL2Jh", - "cmVGaXJzdFN0cm9rZVNob3J0Y3V0QmluZGluZ051bGxhYmxlIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiZGlmZlZpZXdlck5leHRGaWxlIjogewogICAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvYmFyZUZpcnN0U3Ryb2tlU2hvcnRjdXRCaW5kaW5nTnVsbGFibGUiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJkaWZmVmlld2VyUHJldmlvdXNGaWxlIjogewogICAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvYmFyZUZpcnN0U3Ryb2tlU2hvcnRjdXRCaW5kaW5nTnVsbGFibGUiCiAgICAgICAgICAgIH0K", - "ICAgICAgICAgIH0sCiAgICAgICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvc2hvcnRjdXRCaW5kaW5nTnVsbGFibGUiCiAgICAgICAgICB9CiAgICAgICAgfSwKICAgICAgICAid2hlbiI6IHsKICAgICAgICAgICJ0eXBlIjogIm9iamVjdCIsCiAgICAgICAgICAiZGVmYXVsdCI6IHt9LAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIk9wdGlvbmFsIHBlci1hY3Rpb24gY29udGV4dCBwcmVkaWNhdGVzIChWUyBDb2RlLXN0eWxlIGB3aGVuYCBjbGF1c2VzKSwga2V5ZWQg", - "YnkgY211eCBhY3Rpb24gaWQuIEVhY2ggdmFsdWUgaXMgYSBib29sZWFuIGV4cHJlc3Npb24gb3ZlciBjb250ZXh0IGtleXMgY29tYmluZWQgd2l0aCAhLCAmJiwgfHwsIGFuZCBwYXJlbnRoZXNlcy4gQm9vbGVhbiBrZXlzOiBzaWRlYmFyRm9jdXMsIGJyb3dzZXJGb2N1cywgbWFya2Rvd25Gb2N1cywgZmlsZVByZXZpZXdUZXh0RWRpdG9yRm9jdXMsIHNpbXVsYXRvckZvY3VzLCB0ZXJtaW5hbEZvY3VzLCBjb21tYW5kUGFsZXR0ZVZpc2libGUsIHRlcm1pbmFsRmluZFZpc2libGUsIHdvcmtzcGFjZUNhbnZhc0xheW91dC4gVHlw", - "ZWQga2V5cyBzdXBwb3J0IGNvbXBhcmlzb25zOiB0aGUgc3RyaW5nIHNpZGViYXJNb2RlIChmaWxlcywgZmluZCwgc2Vzc2lvbnMsIGZlZWQsIG9yIGRvY2spIGFuZCB0aGUgaW50ZWdlcnMgcGFuZUNvdW50IGFuZCB3b3Jrc3BhY2VDb3VudC4gQ29tcGFyaXNvbiBvcGVyYXRvcnMgYXJlID09LCAhPSwgPX4gKHJlZ2V4KSwgPCwgPD0sID4sID49LCBhbmQgYGluIFthLCBiXWA7IGFuIHVua25vd24gb3IgYWJzZW50IGtleSByZWFkcyBhcyBmYWxzZS4gVGhlIGJvb2xlYW4gbGl0ZXJhbHMgdHJ1ZSBhbmQgZmFsc2UgYXJlIGFsc28g", - "YWNjZXB0ZWQ7IGBrZXkgPT0gZmFsc2VgIGlzIHRoZSBzYW1lIGFzIGAha2V5YC4gVGhlIGFjdGlvbidzIHNob3J0Y3V0IG9ubHkgZmlyZXMgKGFuZCBvbmx5IGNvbmZsaWN0cyB3aXRoIG90aGVyIHNob3J0Y3V0cykgd2hlbiB0aGUgY2xhdXNlIGhvbGRzLiBFeGFtcGxlczogeyBcInNlbGVjdFdvcmtzcGFjZUJ5TnVtYmVyXCI6IFwiIXNpZGViYXJGb2N1c1wiIH0gc2VsZWN0cyB3b3Jrc3BhY2VzIHdpdGggQ3RybCsx4oCTOSBldmVyeXdoZXJlIGV4Y2VwdCB3aGVuIHRoZSByaWdodCBzaWRlYmFyIGlzIGZvY3VzZWQ7IHsgXCJz", - "ZWxlY3RTdXJmYWNlQnlOdW1iZXJcIjogXCJzaWRlYmFyTW9kZSA9PSAnZmluZCcgJiYgcGFuZUNvdW50ID4gMVwiIH0gc2NvcGVzIGEgYmluZGluZyB0byB0aGUgRmluZCBzaWRlYmFyIHdoZW4gdGhlIHdvcmtzcGFjZSBoYXMgbXVsdGlwbGUgcGFuZXMuIiwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMuc2hvcnRjdXRzLndoZW4iLAogICAgICAgICAgImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjogewogICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICB9CiAgICAgICAgfQog", - "ICAgICB9CiAgICB9CiAgfSwKICAiJGRlZnMiOiB7CiAgICAiY29sb3JIZXgiOiB7CiAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICJwYXR0ZXJuIjogIl4jWzAtOUEtRmEtZl17Nn0kIiwKICAgICAgImRlc2NyaXB0aW9uIjogIlVwcGVyY2FzZSBvciBsb3dlcmNhc2UgNi1kaWdpdCBoZXggY29sb3IuIgogICAgfSwKICAgICJjb2xvckhleE9yTnVsbCI6IHsKICAgICAgIm9uZU9mIjogWwogICAgICAgIHsKICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvY29sb3JIZXgiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAg", - "ICAidHlwZSI6ICJudWxsIgogICAgICAgIH0KICAgICAgXQogICAgfSwKICAgICJub3RpZmljYXRpb25Tb3VuZE92ZXJyaWRlIjogewogICAgICAib25lT2YiOiBbCiAgICAgICAgewogICAgICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgICAgICJhZGRpdGlvbmFsUHJvcGVydGllcyI6IGZhbHNlLAogICAgICAgICAgInJlcXVpcmVkIjogWyJzb3VuZCJdLAogICAgICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICJzb3VuZCI6IHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAgICAgICAg", - "ICJlbnVtIjogWyJkZWZhdWx0IiwgIkJhc3NvIiwgIkJsb3ciLCAiQm90dGxlIiwgIkZyb2ciLCAiRnVuayIsICJHbGFzcyIsICJIZXJvIiwgIk1vcnNlIiwgIlBpbmciLCAiUG9wIiwgIlB1cnIiLCAiU29zdW1pIiwgIlN1Ym1hcmluZSIsICJUaW5rIiwgIm5vbmUiXQogICAgICAgICAgICB9CiAgICAgICAgICB9CiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjogZmFsc2UsCiAgICAgICAgICAicmVxdWlyZWQiOiBbInNvdW5kIiwg", - "ImN1c3RvbVNvdW5kRmlsZVBhdGgiXSwKICAgICAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICAgICAic291bmQiOiB7ICJjb25zdCI6ICJjdXN0b21fZmlsZSIgfSwKICAgICAgICAgICAgImN1c3RvbVNvdW5kRmlsZVBhdGgiOiB7CiAgICAgICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICAgICAibWluTGVuZ3RoIjogMSwKICAgICAgICAgICAgICAicGF0dGVybiI6ICIuKlxcUy4qIgogICAgICAgICAgICB9CiAgICAgICAgICB9CiAgICAgICAgfQogICAgICBdLAogICAgICAiZGVzY3JpcHRpb25LZXki", - "OiAic2NoZW1hRGVzY3JpcHRpb25zLm5vdGlmaWNhdGlvbnMuc291bmRPdmVycmlkZSIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJBIG5vdGlmaWNhdGlvbiBzb3VuZCBvdmVycmlkZS4gYGRlZmF1bHRgIHVzZXMgbm90aWZpY2F0aW9ucy5zb3VuZCwgYG5vbmVgIGRpc2FibGVzIHBsYXliYWNrLCBhbmQgdGhlIHJlbWFpbmluZyB2YWx1ZXMgc2VsZWN0IGEgYnVpbHQtaW4gb3IgY3VzdG9tIHNvdW5kLiIKICAgIH0sCiAgICAic2hvcnRjdXRCaW5kaW5nIjogewogICAgICAib25lT2YiOiBbCiAgICAgICAgewogICAgICAgICAgIiRy", - "ZWYiOiAiIy8kZGVmcy91bmJvdW5kU2hvcnRjdXRCaW5kaW5nIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJVbmJpbmQgdGhpcyBzaG9ydGN1dC4gQWNjZXB0ZWQgdmFsdWVzIGFyZSBhbiBlbXB0eSBzdHJpbmcsIG5vbmUsIGNsZWFyLCB1bmJvdW5kLCBvciBkaXNhYmxlZC4iCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL3Nob3J0Y3V0Rmlyc3RTdHJva2UiLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlNpbmdsZS1zdHJva2Ugc2hvcnRjdXQsIGZvciBleGFtcGxlIGNtZCtuIG9y", - "IGNtZCtzcGFjZS4iCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6ICJhcnJheSIsCiAgICAgICAgICAibWluSXRlbXMiOiAxLAogICAgICAgICAgIm1heEl0ZW1zIjogMiwKICAgICAgICAgICJwcmVmaXhJdGVtcyI6IFsKICAgICAgICAgICAgewogICAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvc2hvcnRjdXRGaXJzdFN0cm9rZSIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgewogICAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvc2hvcnRjdXRTdHJva2UiCiAgICAgICAgICAgIH0KICAgICAg", - "ICAgIF0sCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiQ2hvcmRlZCBzaG9ydGN1dC4gRXhhbXBsZTogW1wiY3RybCtiXCIsIFwiY1wiXS4iCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgInNob3J0Y3V0QmluZGluZ051bGxhYmxlIjogewogICAgICAib25lT2YiOiBbCiAgICAgICAgewogICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9zaG9ydGN1dEJpbmRpbmciCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6ICJudWxsIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJVbmJpbmQgdGhpcyBzaG9y", - "dGN1dC4iCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgInNpbmdsZUJhcmVGaXJzdFN0cm9rZVNob3J0Y3V0QmluZGluZyI6IHsKICAgICAgIm9uZU9mIjogWwogICAgICAgIHsKICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvdW5ib3VuZFNob3J0Y3V0QmluZGluZyIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiVW5iaW5kIHRoaXMgc2hvcnRjdXQuIEFjY2VwdGVkIHZhbHVlcyBhcmUgYW4gZW1wdHkgc3RyaW5nLCBub25lLCBjbGVhciwgdW5ib3VuZCwgb3IgZGlzYWJsZWQuIgogICAgICAgIH0sCiAgICAgICAgewog", - "ICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9iYXJlRmlyc3RTdHJva2VTaG9ydGN1dEZpcnN0U3Ryb2tlIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTaW5nbGUtc3Ryb2tlIHNob3J0Y3V0LiBCYXJlIGZpcnN0IHN0cm9rZXMgYXJlIGFjY2VwdGVkIGZvciB0aGlzIGFjdGlvbi4iCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgInNpbmdsZUJhcmVGaXJzdFN0cm9rZVNob3J0Y3V0QmluZGluZ051bGxhYmxlIjogewogICAgICAib25lT2YiOiBbCiAgICAgICAgewogICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9zaW5n", - "bGVCYXJlRmlyc3RTdHJva2VTaG9ydGN1dEJpbmRpbmciCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAidHlwZSI6ICJudWxsIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJVbmJpbmQgdGhpcyBzaG9ydGN1dC4iCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImJhcmVGaXJzdFN0cm9rZVNob3J0Y3V0QmluZGluZyI6IHsKICAgICAgIm9uZU9mIjogWwogICAgICAgIHsKICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvdW5ib3VuZFNob3J0Y3V0QmluZGluZyIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAi", - "VW5iaW5kIHRoaXMgc2hvcnRjdXQuIEFjY2VwdGVkIHZhbHVlcyBhcmUgYW4gZW1wdHkgc3RyaW5nLCBub25lLCBjbGVhciwgdW5ib3VuZCwgb3IgZGlzYWJsZWQuIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9iYXJlRmlyc3RTdHJva2VTaG9ydGN1dEZpcnN0U3Ryb2tlIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTaW5nbGUtc3Ryb2tlIHNob3J0Y3V0LiBCYXJlIGZpcnN0IHN0cm9rZXMgYXJlIGFjY2VwdGVkIGZvciB0aGlzIGFjdGlvbi4iCiAgICAgICAgfSwKICAgICAgICB7", - "CiAgICAgICAgICAidHlwZSI6ICJhcnJheSIsCiAgICAgICAgICAibWluSXRlbXMiOiAxLAogICAgICAgICAgIm1heEl0ZW1zIjogMiwKICAgICAgICAgICJwcmVmaXhJdGVtcyI6IFsKICAgICAgICAgICAgewogICAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvYmFyZUZpcnN0U3Ryb2tlU2hvcnRjdXRGaXJzdFN0cm9rZSIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgewogICAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvc2hvcnRjdXRTdHJva2UiCiAgICAgICAgICAgIH0KICAgICAgICAgIF0sCiAgICAgICAgICAi", - "ZGVzY3JpcHRpb24iOiAiQ2hvcmRlZCBzaG9ydGN1dC4gQmFyZSBmaXJzdCBzdHJva2VzIGFyZSBhY2NlcHRlZCBmb3IgdGhpcyBhY3Rpb24uIEV4YW1wbGU6IFtcImdcIiwgXCJnXCJdLiIKICAgICAgICB9CiAgICAgIF0KICAgIH0sCiAgICAiYmFyZUZpcnN0U3Ryb2tlU2hvcnRjdXRCaW5kaW5nTnVsbGFibGUiOiB7CiAgICAgICJvbmVPZiI6IFsKICAgICAgICB7CiAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL2JhcmVGaXJzdFN0cm9rZVNob3J0Y3V0QmluZGluZyIKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICJ0", - "eXBlIjogIm51bGwiLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlVuYmluZCB0aGlzIHNob3J0Y3V0LiIKICAgICAgICB9CiAgICAgIF0KICAgIH0sCiAgICAidW5ib3VuZFNob3J0Y3V0QmluZGluZyI6IHsKICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgImVudW0iOiBbIiIsICJub25lIiwgImNsZWFyIiwgInVuYm91bmQiLCAiZGlzYWJsZWQiXQogICAgfSwKICAgICJzaG9ydGN1dEZpcnN0U3Ryb2tlIjogewogICAgICAiYWxsT2YiOiBbCiAgICAgICAgewogICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9zaG9ydGN1", - "dFN0cm9rZSIKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICJhbnlPZiI6IFsKICAgICAgICAgICAgewogICAgICAgICAgICAgICJwYXR0ZXJuIjogIl4oPzooPzpbY0NdW21NXVtkRF18W2NDXVtvT11bbU1dW21NXVthQV1bbk5dW2REXXxbc1NdW2hIXVtpSV1bZkZdW3RUXXxbb09dW3BQXVt0VF18W29PXVtwUF1bdFRdW2lJXVtvT11bbk5dfFthQV1bbExdW3RUXXxbY0NdW3RUXVtyUl1bbExdfFtjQ11bb09dW25OXVt0VF1bclJdW29PXVtsTF18W2NDXVt0VF1bbExdfOKMmHzih6d84oylfOKMgylcXCspKyIKICAgICAg", - "ICAgICAgfSwKICAgICAgICAgICAgewogICAgICAgICAgICAgICJwYXR0ZXJuIjogIl4oPzogfFtzU11bcFBdW2FBXVtjQ11bZUVdfFtzU11bcFBdW2FBXVtjQ11bZUVdW2JCXVthQV1bclJdfDxbc1NdW3BQXVthQV1bY0NdW2VFXT4pJCIKICAgICAgICAgICAgfQogICAgICAgICAgXQogICAgICAgIH0KICAgICAgXSwKICAgICAgImRlc2NyaXB0aW9uIjogIkZpcnN0IG9yIG9ubHkgc2hvcnRjdXQgc3Ryb2tlLiBNdXN0IGluY2x1ZGUgYSBtb2RpZmllciB1bmxlc3MgdGhlIGtleSBpcyBTcGFjZS4iCiAgICB9LAogICAgImJhcmVG", - "aXJzdFN0cm9rZVNob3J0Y3V0Rmlyc3RTdHJva2UiOiB7CiAgICAgICJhbGxPZiI6IFsKICAgICAgICB7CiAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL3Nob3J0Y3V0U3Ryb2tlIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgImFueU9mIjogWwogICAgICAgICAgICB7CiAgICAgICAgICAgICAgInBhdHRlcm4iOiAiXig/Oig/OltjQ11bbU1dW2REXXxbY0NdW29PXVttTV1bbU1dW2FBXVtuTl1bZERdfFtzU11baEhdW2lJXVtmRl1bdFRdfFtvT11bcFBdW3RUXXxbb09dW3BQXVt0VF1baUldW29PXVtuTl18W2FBXVts", - "TF1bdFRdfFtjQ11bdFRdW3JSXVtsTF18W2NDXVtvT11bbk5dW3RUXVtyUl1bb09dW2xMXXxbY0NdW3RUXVtsTF184oyYfOKHp3zijKV84oyDKVxcKykrIgogICAgICAgICAgICB9LAogICAgICAgICAgICB7CiAgICAgICAgICAgICAgInBhdHRlcm4iOiAiXig/OiB8W3NTXVtwUF1bYUFdW2NDXVtlRV18W3NTXVtwUF1bYUFdW2NDXVtlRV1bYkJdW2FBXVtyUl18PFtzU11bcFBdW2FBXVtjQ11bZUVdPikkIgogICAgICAgICAgICB9LAogICAgICAgICAgICB7CiAgICAgICAgICAgICAgInBhdHRlcm4iOiAiXig/OltyUl1bZUVdW3RU", - "XVt1VV1bclJdW25OXXxbZUVdW25OXVt0VF1bZUVdW3JSXXzihqkpJCIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgewogICAgICAgICAgICAgICJwYXR0ZXJuIjogIl4oPzpbbExdW2VFXVtmRl1bdFRdfFthQV1bclJdW3JSXVtvT11bd1ddW2xMXVtlRV1bZkZdW3RUXXxbbExdW2VFXVtmRl1bdFRdW2FBXVtyUl1bclJdW29PXVt3V1184oaQfFtyUl1baUldW2dHXVtoSF1bdFRdfFthQV1bclJdW3JSXVtvT11bd1ddW3JSXVtpSV1bZ0ddW2hIXVt0VF18W3JSXVtpSV1bZ0ddW2hIXVt0VF1bYUFdW3JSXVtyUl1bb09dW3dXXXzi", - "hpJ8W3VVXVtwUF18W2FBXVtyUl1bclJdW29PXVt3V11bdVVdW3BQXXxbdVVdW3BQXVthQV1bclJdW3JSXVtvT11bd1ddfOKGkXxbZERdW29PXVt3V11bbk5dfFthQV1bclJdW3JSXVtvT11bd1ddW2REXVtvT11bd1ddW25OXXxbZERdW29PXVt3V11bbk5dW2FBXVtyUl1bclJdW29PXVt3V1184oaTKSQiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAicGF0dGVybiI6ICJeKD86W0EtWmEtejAtOV18WywuL1xcXFw7J2A9XFxbXFxdLV18W2NDXVtvT11bbU1dW21NXVthQV18W3BQXVtlRV1bclJdW2lJ", - "XVtvT11bZERdfFtkRF1bb09dW3RUXXxbc1NdW2xMXVthQV1bc1NdW2hIXXxbYkJdW2FBXVtjQ11ba0tdW3NTXVtsTF1bYUFdW3NTXVtoSF18W3NTXVtlRV1bbU1dW2lJXVtjQ11bb09dW2xMXVtvT11bbk5dfFtxUV1bdVVdW29PXVt0VF1bZUVdfFthQV1bcFBdW29PXVtzU11bdFRdW3JSXVtvT11bcFBdW2hIXVtlRV18W2JCXVthQV1bY0NdW2tLXVt0VF1baUldW2NDXVtrS118W2dHXVtyUl1bYUFdW3ZWXVtlRV18W21NXVtpSV1bbk5dW3VVXVtzU118W2hIXVt5WV1bcFBdW2hIXVtlRV1bbk5dfFtwUF1bbExdW3VVXVtzU118W2VF", - "XVtxUV1bdVVdW2FBXVtsTF1bc1NdfFtsTF1bZUVdW2ZGXVt0VF1bYkJdW3JSXVthQV1bY0NdW2tLXVtlRV1bdFRdfFtvT11bcFBdW2VFXVtuTl1bYkJdW3JSXVthQV1bY0NdW2tLXVtlRV1bdFRdfFtyUl1baUldW2dHXVtoSF1bdFRdW2JCXVtyUl1bYUFdW2NDXVtrS11bZUVdW3RUXXxbY0NdW2xMXVtvT11bc1NdW2VFXVtiQl1bclJdW2FBXVtjQ11ba0tdW2VFXVt0VF0pJCIKICAgICAgICAgICAgfQogICAgICAgICAgXQogICAgICAgIH0KICAgICAgXSwKICAgICAgImRlc2NyaXB0aW9uIjogIkZpcnN0IG9yIG9ubHkgc2hvcnRj", - "dXQgc3Ryb2tlLiBCYXJlIHByaW50YWJsZSBrZXlzIGFyZSBhY2NlcHRlZCBmb3IgYWN0aW9ucyB0aGF0IG9wdCBpbnRvIGJhcmUgZmlyc3Qgc3Ryb2tlcy4iCiAgICB9LAogICAgInNob3J0Y3V0U3Ryb2tlIjogewogICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAicGF0dGVybiI6ICJeKD86KD86W2NDXVttTV1bZERdfFtjQ11bb09dW21NXVttTV1bYUFdW25OXVtkRF18W3NTXVtoSF1baUldW2ZGXVt0VF18W29PXVtwUF1bdFRdfFtvT11bcFBdW3RUXVtpSV1bb09dW25OXXxbYUFdW2xMXVt0VF18W2NDXVt0VF1bclJdW2xM", - "XXxbY0NdW29PXVtuTl1bdFRdW3JSXVtvT11bbExdfFtjQ11bdFRdW2xMXXzijJh84oenfOKMpXzijIMpXFwrKSooPzogfFtBLVphLXowLTldfFtsTF1bZUVdW2ZGXVt0VF18W2FBXVtyUl1bclJdW29PXVt3V11bbExdW2VFXVtmRl1bdFRdfFtsTF1bZUVdW2ZGXVt0VF1bYUFdW3JSXVtyUl1bb09dW3dXXXzihpB8W3JSXVtpSV1bZ0ddW2hIXVt0VF18W2FBXVtyUl1bclJdW29PXVt3V11bclJdW2lJXVtnR11baEhdW3RUXXxbclJdW2lJXVtnR11baEhdW3RUXVthQV1bclJdW3JSXVtvT11bd1ddfOKGknxbdVVdW3BQXXxbYUFdW3JS", - "XVtyUl1bb09dW3dXXVt1VV1bcFBdfFt1VV1bcFBdW2FBXVtyUl1bclJdW29PXVt3V1184oaRfFtkRF1bb09dW3dXXVtuTl18W2FBXVtyUl1bclJdW29PXVt3V11bZERdW29PXVt3V11bbk5dfFtkRF1bb09dW3dXXVtuTl1bYUFdW3JSXVtyUl1bb09dW3dXXXzihpN8W3RUXVthQV1bYkJdfFtyUl1bZUVdW3RUXVt1VV1bclJdW25OXXxbZUVdW25OXVt0VF1bZUVdW3JSXXzihql8W3NTXVtwUF1bYUFdW2NDXVtlRV18W3NTXVtwUF1bYUFdW2NDXVtlRV1bYkJdW2FBXVtyUl18PFtzU11bcFBdW2FBXVtjQ11bZUVdPnxbLC4vXFxcXDsn", - "YD1cXFtcXF0tXXxbY0NdW29PXVttTV1bbU1dW2FBXXxbcFBdW2VFXVtyUl1baUldW29PXVtkRF18W2REXVtvT11bdFRdfFtzU11bbExdW2FBXVtzU11baEhdfFtiQl1bYUFdW2NDXVtrS11bc1NdW2xMXVthQV1bc1NdW2hIXXxbc1NdW2VFXVttTV1baUldW2NDXVtvT11bbExdW29PXVtuTl18W3FRXVt1VV1bb09dW3RUXVtlRV18W2FBXVtwUF1bb09dW3NTXVt0VF1bclJdW29PXVtwUF1baEhdW2VFXXxbYkJdW2FBXVtjQ11ba0tdW3RUXVtpSV1bY0NdW2tLXXxbZ0ddW3JSXVthQV1bdlZdW2VFXXxbbU1dW2lJXVtuTl1bdVVdW3NT", - "XXxbaEhdW3lZXVtwUF1baEhdW2VFXVtuTl18W3BQXVtsTF1bdVVdW3NTXXxbZUVdW3FRXVt1VV1bYUFdW2xMXVtzU118W2xMXVtlRV1bZkZdW3RUXVtiQl1bclJdW2FBXVtjQ11ba0tdW2VFXVt0VF18W29PXVtwUF1bZUVdW25OXVtiQl1bclJdW2FBXVtjQ11ba0tdW2VFXVt0VF18W3JSXVtpSV1bZ0ddW2hIXVt0VF1bYkJdW3JSXVthQV1bY0NdW2tLXVtlRV1bdFRdfFtjQ11bbExdW29PXVtzU11bZUVdW2JCXVtyUl1bYUFdW2NDXVtrS11bZUVdW3RUXXxbZkZdKD86WzEtOV18MVswLTldfDIwKXxbdlZdW29PXVtsTF1bdVVdW21N", - "XVtlRV1bdVVdW3BQXXxbbU1dW2VFXVtkRF1baUldW2FBXVt2Vl1bb09dW2xMXVt1VV1bbU1dW2VFXVt1VV1bcFBdfFttTV1bZUVdW2REXVtpSV1bYUFdXFwuW3ZWXVtvT11bbExdW3VVXVttTV1bZUVdW3VVXVtwUF18W3ZWXVtvT11bbExdW3VVXVttTV1bZUVdW2REXVtvT11bd1ddW25OXXxbbU1dW2VFXVtkRF1baUldW2FBXVt2Vl1bb09dW2xMXVt1VV1bbU1dW2VFXVtkRF1bb09dW3dXXVtuTl18W21NXVtlRV1bZERdW2lJXVthQV1cXC5bdlZdW29PXVtsTF1bdVVdW21NXVtlRV1bZERdW29PXVt3V11bbk5dfFtiQl1bclJdW2lJ", - "XVtnR11baEhdW3RUXVtuTl1bZUVdW3NTXVtzU11bdVVdW3BQXXxbbU1dW2VFXVtkRF1baUldW2FBXVtiQl1bclJdW2lJXVtnR11baEhdW3RUXVtuTl1bZUVdW3NTXVtzU11bdVVdW3BQXXxbbU1dW2VFXVtkRF1baUldW2FBXVxcLltiQl1bclJdW2lJXVtnR11baEhdW3RUXVtuTl1bZUVdW3NTXVtzU11bdVVdW3BQXXxbYkJdW3JSXVtpSV1bZ0ddW2hIXVt0VF1bbk5dW2VFXVtzU11bc1NdW2REXVtvT11bd1ddW25OXXxbbU1dW2VFXVtkRF1baUldW2FBXVtiQl1bclJdW2lJXVtnR11baEhdW3RUXVtuTl1bZUVdW3NTXVtzU11bZERd", - "W29PXVt3V11bbk5dfFttTV1bZUVdW2REXVtpSV1bYUFdXFwuW2JCXVtyUl1baUldW2dHXVtoSF1bdFRdW25OXVtlRV1bc1NdW3NTXVtkRF1bb09dW3dXXVtuTl18W21NXVt1VV1bdFRdW2VFXXxbbU1dW2VFXVtkRF1baUldW2FBXVttTV1bdVVdW3RUXVtlRV18W21NXVtlRV1bZERdW2lJXVthQV1cXC5bbU1dW3VVXVt0VF1bZUVdfFtwUF1bbExdW2FBXVt5WV1bcFBdW2FBXVt1VV1bc1NdW2VFXXxbbU1dW2VFXVtkRF1baUldW2FBXVtwUF1bbExdW2FBXVt5WV1bcFBdW2FBXVt1VV1bc1NdW2VFXXxbbU1dW2VFXVtkRF1baUldW2FB", - "XVxcLltwUF1bbExdW2FBXVt5WV1bcFBdW2FBXVt1VV1bc1NdW2VFXXxbbk5dW2VFXVt4WF1bdFRdW3RUXVtyUl1bYUFdW2NDXVtrS118W21NXVtlRV1bZERdW2lJXVthQV1bbk5dW2VFXVt4WF1bdFRdfFttTV1bZUVdW2REXVtpSV1bYUFdXFwuW25OXVtlRV1beFhdW3RUXXxbbU1dW2VFXVtkRF1baUldW2FBXVxcLltuTl1bZUVdW3hYXVt0VF1bdFRdW3JSXVthQV1bY0NdW2tLXXxbcFBdW3JSXVtlRV1bdlZdW2lJXVtvT11bdVVdW3NTXVt0VF1bclJdW2FBXVtjQ11ba0tdfFttTV1bZUVdW2REXVtpSV1bYUFdW3BQXVtyUl1bZUVd", - "W3ZWXVtpSV1bb09dW3VVXVtzU118W21NXVtlRV1bZERdW2lJXVthQV1cXC5bcFBdW3JSXVtlRV1bdlZdW2lJXVtvT11bdVVdW3NTXXxbbU1dW2VFXVtkRF1baUldW2FBXVxcLltwUF1bclJdW2VFXVt2Vl1baUldW29PXVt1VV1bc1NdW3RUXVtyUl1bYUFdW2NDXVtrS10pJCIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJPbmUga2V5Ym9hcmQgc2hvcnRjdXQgc3Ryb2tlIHVzaW5nIG1vZGlmaWVyK2tleSBzeW50YXguIFN1cHBvcnRlZCBrZXkgbmFtZXMgaW5jbHVkZSBzcGFjZSwgU3BhY2UsIDxzcGFjZT4sIGFuZCA8U3BhY2U+LiIK", - "ICAgIH0KICB9Cn0K", + "d0FnZW50VXNhZ2UiOiB7CiAgICAgICAgICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogZmFsc2UsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiQXBwZW5kIGNvZGluZy1hZ2VudCB1c2FnZSAobW9kZWwsIGNvbnRleHQgd2luZG93IHVzZWQsIGVzdGltYXRlZCBBUEkgY29zdCkgdG8gdGhlIENsYXVkZSBDb2RlIG9yIENvZGV4IHN0YXR1cyBlbnRyeS4gVGhlIGNvc3QgaXMgYW4gZXN0aW1hdGUgZnJvbSBwdWJsaXNoZWQgcGVyLXRva2VuIEFQSSBwcmljZXMsIG5vdCBhIGJpbGwuIgogICAgICAgIH0s", + "CiAgICAgICAgInNob3dBZ2VudEFjdGl2aXR5IjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiU2hvdyB0aGUgbG9hZGluZyBzcGlubmVyIG9uIHdvcmtzcGFjZXMgd2l0aCBydW5uaW5nIGNvZGluZyBhZ2VudHMgb3IgYWN0aXZlIGxvYWRlcnMuIgogICAgICAgIH0sCiAgICAgICAgImxvYWRpbmdTcGlubmVyUG9zaXRpb24iOiB7CiAgICAgICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAgICAgImVudW0iOiBbImxlYWRp", + "bmciLCAidHJhaWxpbmciXSwKICAgICAgICAgICJkZWZhdWx0IjogImxlYWRpbmciLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIldoaWNoIHNpZGUgb2YgdGhlIHdvcmtzcGFjZSByb3cgdGhlIGxvYWRpbmcgc3Bpbm5lciBhcHBlYXJzIG9uOiBsZWFkaW5nIChsZWZ0LCBzaGFyaW5nIHRoZSB1bnJlYWQtYmFkZ2Ugc2xvdCkgb3IgdHJhaWxpbmcgKHJpZ2h0KS4iCiAgICAgICAgfSwKICAgICAgICAibm90aWZpY2F0aW9uQmFkZ2VQb3NpdGlvbiI6IHsKICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAiZW51", + "bSI6IFsibGVhZGluZyIsICJ0cmFpbGluZyJdLAogICAgICAgICAgImRlZmF1bHQiOiAibGVhZGluZyIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiV2hpY2ggc2lkZSBvZiB0aGUgd29ya3NwYWNlIHJvdyB0aGUgdW5yZWFkIG5vdGlmaWNhdGlvbiBiYWRnZSBhcHBlYXJzIG9uOiBsZWFkaW5nIChsZWZ0KSBvciB0cmFpbGluZyAocmlnaHQpLiIKICAgICAgICB9LAogICAgICAgICJzaG93Q3VzdG9tTWV0YWRhdGEiOiB7CiAgICAgICAgICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogdHJ1ZSwKICAg", + "ICAgICAgICJkZXNjcmlwdGlvbiI6ICJTaG93IGN1c3RvbSBtZXRhZGF0YSBwaWxscy4iCiAgICAgICAgfSwKICAgICAgICAicmlnaHRNYXhXaWR0aCI6IHsKICAgICAgICAgICJ0eXBlIjogIm51bWJlciIsCiAgICAgICAgICAiZXhjbHVzaXZlTWluaW11bSI6IDAsCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLnNpZGViYXIucmlnaHRNYXhXaWR0aCIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiTWF4aW11bSB3aWR0aCBpbiBwb2ludHMgZm9yIHRoZSByaWdodCBzaWRlYmFyLiBXaGVu", + "IG9taXR0ZWQsIHRoZSBidWlsdC1pbiBkeW5hbWljIGNhcCBhcHBsaWVzLiIKICAgICAgICB9CiAgICAgIH0KICAgIH0sCiAgICAid29ya3NwYWNlQ29sb3JzIjogewogICAgICAieC1jbXV4LXNjb3BlcyI6IFsiZ2xvYmFsIl0sCiAgICAgICJ0aXRsZSI6ICJ3b3Jrc3BhY2VDb2xvcnMiLAogICAgICAiZGVzY3JpcHRpb24iOiAiV29ya3NwYWNlIHRhYiBhbmQgYmFkZ2UgY29sb3JzIGZyb20gU2V0dGluZ3MgPiBXb3Jrc3BhY2UgQ29sb3JzLiIsCiAgICAgICJ0eXBlIjogIm9iamVjdCIsCiAgICAgICJhZGRpdGlvbmFsUHJvcGVy", + "dGllcyI6IGZhbHNlLAogICAgICAicHJvcGVydGllcyI6IHsKICAgICAgICAiaW5kaWNhdG9yU3R5bGUiOiB7CiAgICAgICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAgICAgImVudW0iOiBbCiAgICAgICAgICAgICJsZWZ0UmFpbCIsCiAgICAgICAgICAgICJzb2xpZEZpbGwiLAogICAgICAgICAgICAicmFpbCIsCiAgICAgICAgICAgICJib3JkZXIiLAogICAgICAgICAgICAid2FzaCIsCiAgICAgICAgICAgICJsaWZ0IiwKICAgICAgICAgICAgInR5cG9ncmFwaHkiLAogICAgICAgICAgICAid2FzaFJhaWwiLAogICAgICAg", + "ICAgICAiYmx1ZVdhc2hDb2xvclJhaWwiCiAgICAgICAgICBdLAogICAgICAgICAgImRlZmF1bHQiOiAibGVmdFJhaWwiLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkFjdGl2ZSB3b3Jrc3BhY2UgaW5kaWNhdG9yIHN0eWxlLiBMZWdhY3kgYWxpYXNlcyBhcmUgYWNjZXB0ZWQgYW5kIG5vcm1hbGl6ZWQuIgogICAgICAgIH0sCiAgICAgICAgInNlbGVjdGlvbkNvbG9yIjogewogICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9jb2xvckhleE9yTnVsbCIsCiAgICAgICAgICAiZGVmYXVsdCI6IG51bGwsCiAgICAgICAgICAiZGVz", + "Y3JpcHRpb24iOiAiT3ZlcnJpZGUgdGhlIHNlbGVjdGVkIHdvcmtzcGFjZSBiYWNrZ3JvdW5kIGNvbG9yLiIKICAgICAgICB9LAogICAgICAgICJub3RpZmljYXRpb25CYWRnZUNvbG9yIjogewogICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9jb2xvckhleE9yTnVsbCIsCiAgICAgICAgICAiZGVmYXVsdCI6IG51bGwsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiT3ZlcnJpZGUgdGhlIHVucmVhZCBub3RpZmljYXRpb24gYmFkZ2UgY29sb3IuIgogICAgICAgIH0sCiAgICAgICAgImNvbG9ycyI6IHsKICAgICAgICAgICJ0eXBl", + "IjogIm9iamVjdCIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiRnVsbCBuYW1lZCB3b3Jrc3BhY2UgY29sb3IgcGFsZXR0ZS4gSW5jbHVkZSBidWlsdC1pbiBlbnRyaWVzIHlvdSB3YW50IHRvIGtlZXAsIHJlbW92ZSBrZXlzIHRvIHJlbW92ZSBjb2xvcnMsIGFuZCBhZGQgbW9yZSBuYW1lZCBlbnRyaWVzIHRvIGV4dGVuZCB0aGUgcGlja2VyLiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHsKICAgICAgICAgICAgIlJlZCI6ICIjQzAzOTJCIiwKICAgICAgICAgICAgIkNyaW1zb24iOiAiIzkyMkIyMSIsCiAgICAgICAgICAgICJP", + "cmFuZ2UiOiAiI0EwNDAwMCIsCiAgICAgICAgICAgICJBbWJlciI6ICIjN0Q2NjA4IiwKICAgICAgICAgICAgIk9saXZlIjogIiM0QTVDMTgiLAogICAgICAgICAgICAiR3JlZW4iOiAiIzE5NkYzRCIsCiAgICAgICAgICAgICJUZWFsIjogIiMwMDZCNkIiLAogICAgICAgICAgICAiQXF1YSI6ICIjMEU2QjhDIiwKICAgICAgICAgICAgIkJsdWUiOiAiIzE1NjVDMCIsCiAgICAgICAgICAgICJOYXZ5IjogIiMxQTUyNzYiLAogICAgICAgICAgICAiSW5kaWdvIjogIiMyODM1OTMiLAogICAgICAgICAgICAiUHVycGxlIjogIiM2QTFC", + "OUEiLAogICAgICAgICAgICAiTWFnZW50YSI6ICIjQUQxNDU3IiwKICAgICAgICAgICAgIlJvc2UiOiAiIzg4MEU0RiIsCiAgICAgICAgICAgICJCcm93biI6ICIjN0IzRjAwIiwKICAgICAgICAgICAgIkNoYXJjb2FsIjogIiMzRTRCNUUiCiAgICAgICAgICB9LAogICAgICAgICAgImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjogewogICAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL2NvbG9ySGV4IgogICAgICAgICAgfQogICAgICAgIH0sCiAgICAgICAgInBhbGV0dGVPdmVycmlkZXMiOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmpl", + "Y3QiLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkxlZ2FjeSB3b3Jrc3BhY2UgY29sb3Igb3ZlcnJpZGVzIGZvciBidWlsdC1pbiBwYWxldHRlIG5hbWVzLiBQcmVmZXIgd29ya3NwYWNlQ29sb3JzLmNvbG9ycyBmb3IgbmV3IGNvbmZpZ3MuIiwKICAgICAgICAgICJkZWZhdWx0Ijoge30sCiAgICAgICAgICAiZGVwcmVjYXRlZCI6IHRydWUsCiAgICAgICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiB7CiAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvY29sb3JIZXgiCiAgICAgICAgICB9CiAgICAgICAgfSwKICAgICAg", + "ICAiY3VzdG9tQ29sb3JzIjogewogICAgICAgICAgInR5cGUiOiAiYXJyYXkiLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkxlZ2FjeSBsaXN0IG9mIGN1c3RvbSB3b3Jrc3BhY2UgY29sb3JzLiBQcmVmZXIgd29ya3NwYWNlQ29sb3JzLmNvbG9ycyBmb3IgbmV3IGNvbmZpZ3MuIiwKICAgICAgICAgICJkZWZhdWx0IjogW10sCiAgICAgICAgICAiZGVwcmVjYXRlZCI6IHRydWUsCiAgICAgICAgICAiaXRlbXMiOiB7CiAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvY29sb3JIZXgiCiAgICAgICAgICB9CiAgICAgICAgfQog", + "ICAgICB9CiAgICB9LAogICAgInNpZGViYXJBcHBlYXJhbmNlIjogewogICAgICAieC1jbXV4LXNjb3BlcyI6IFsiZ2xvYmFsIl0sCiAgICAgICJ0aXRsZSI6ICJzaWRlYmFyQXBwZWFyYW5jZSIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJTaWRlYmFyIHRpbnQgc2V0dGluZ3MgZnJvbSBTZXR0aW5ncyA+IFNpZGViYXIgQXBwZWFyYW5jZS4iLAogICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiBmYWxzZSwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgIm1hdGNoVGVybWluYWxC", + "YWNrZ3JvdW5kIjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IGZhbHNlLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlVzZSB0aGUgdGVybWluYWwgYmFja2dyb3VuZCBpbnN0ZWFkIG9mIHRoZSBzaWRlYmFyIHRpbnQuIgogICAgICAgIH0sCiAgICAgICAgInRpbnRDb2xvciI6IHsKICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvY29sb3JIZXgiLAogICAgICAgICAgImRlZmF1bHQiOiAiIzAwMDAwMCIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiQmFzZSBzaWRlYmFyIHRp", + "bnQgY29sb3IgdXNlZCB3aGVuIGxpZ2h0L2Rhcmsgb3ZlcnJpZGVzIGFyZSBub3Qgc2V0LiIKICAgICAgICB9LAogICAgICAgICJsaWdodE1vZGVUaW50Q29sb3IiOiB7CiAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL2NvbG9ySGV4T3JOdWxsIiwKICAgICAgICAgICJkZWZhdWx0IjogbnVsbCwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTaWRlYmFyIHRpbnQgb3ZlcnJpZGUgZm9yIGxpZ2h0IGFwcGVhcmFuY2UuIgogICAgICAgIH0sCiAgICAgICAgImRhcmtNb2RlVGludENvbG9yIjogewogICAgICAgICAgIiRyZWYiOiAi", + "Iy8kZGVmcy9jb2xvckhleE9yTnVsbCIsCiAgICAgICAgICAiZGVmYXVsdCI6IG51bGwsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiU2lkZWJhciB0aW50IG92ZXJyaWRlIGZvciBkYXJrIGFwcGVhcmFuY2UuIgogICAgICAgIH0sCiAgICAgICAgInRpbnRPcGFjaXR5IjogewogICAgICAgICAgInR5cGUiOiAibnVtYmVyIiwKICAgICAgICAgICJtaW5pbXVtIjogMCwKICAgICAgICAgICJtYXhpbXVtIjogMSwKICAgICAgICAgICJkZWZhdWx0IjogMC4xOCwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTaWRlYmFyIHRpbnQg", + "b3BhY2l0eSBmcm9tIDAgdG8gMS4gTm90ZTogdGhpcyBvbmx5IGNvbnRyb2xzIHRoZSBzaWRlYmFyIHRpbnQsIG5vdCB0ZXJtaW5hbC93aW5kb3cgdHJhbnNwYXJlbmN5LiBGb3IgdGVybWluYWwgYmFja2dyb3VuZCB0cmFuc3BhcmVuY3kgb3IgYmx1ciwgc2V0IGBiYWNrZ3JvdW5kLW9wYWNpdHlgIGFuZCBgYmFja2dyb3VuZC1ibHVyYCBpbiBgfi8uY29uZmlnL2dob3N0dHkvY29uZmlnYCBhbmQgcnVuIGBjbXV4IHJlbG9hZC1jb25maWdgLiIKICAgICAgICB9CiAgICAgIH0KICAgIH0sCiAgICAiYXV0b21hdGlvbiI6IHsKICAg", + "ICAgIngtY211eC1zY29wZXMiOiBbImdsb2JhbCJdLAogICAgICAidGl0bGUiOiAiYXV0b21hdGlvbiIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJTb2NrZXQgY29udHJvbCBhbmQgYXV0b21hdGlvbiBzZXR0aW5ncyBmcm9tIFNldHRpbmdzID4gQXV0b21hdGlvbi4iLAogICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiBmYWxzZSwKICAgICAgInByb3BlcnRpZXMiOiB7CiAgICAgICAgInNvY2tldENvbnRyb2xNb2RlIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAg", + "ICAgICJlbnVtIjogWwogICAgICAgICAgICAib2ZmIiwKICAgICAgICAgICAgImNtdXhPbmx5IiwKICAgICAgICAgICAgImF1dG9tYXRpb24iLAogICAgICAgICAgICAicGFzc3dvcmQiLAogICAgICAgICAgICAiYWxsb3dBbGwiLAogICAgICAgICAgICAib3BlbkFjY2VzcyIsCiAgICAgICAgICAgICJmdWxsT3BlbkFjY2VzcyIsCiAgICAgICAgICAgICJub3RpZmljYXRpb25zIiwKICAgICAgICAgICAgImZ1bGwiCiAgICAgICAgICBdLAogICAgICAgICAgImRlZmF1bHQiOiAiY211eE9ubHkiLAogICAgICAgICAgImRlc2NyaXB0", + "aW9uIjogIlNvY2tldCBjb250cm9sIG1vZGUuIExlZ2FjeSBhbGlhc2VzIGFyZSBhY2NlcHRlZCBhbmQgbm9ybWFsaXplZC4iCiAgICAgICAgfSwKICAgICAgICAic29ja2V0UGFzc3dvcmQiOiB7CiAgICAgICAgICAib25lT2YiOiBbCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAidHlwZSI6ICJudWxsIgogICAgICAgICAgICB9CiAgICAgICAgICBdLAogICAgICAgICAgImRlZmF1bHQiOiAiIiwKICAgICAgICAgICJk", + "ZXNjcmlwdGlvbiI6ICJQYXNzd29yZCBmb3IgcGFzc3dvcmQtbW9kZSBzb2NrZXQgYWNjZXNzLiBVc2UgbnVsbCBvciBhbiBlbXB0eSBzdHJpbmcgdG8gY2xlYXIgaXQuIgogICAgICAgIH0sCiAgICAgICAgImNsYXVkZUNvZGVJbnRlZ3JhdGlvbiI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiB0cnVlLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkVuYWJsZSBjbXV4IGludGVncmF0aW9uIGhvb2tzIGZvciBDbGF1ZGUgQ29kZS4iCiAgICAgICAgfSwKICAgICAgICAiY29kZXhJ", + "bnRlZ3JhdGlvbiI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiB0cnVlLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkVuYWJsZSBjbXV4IGludGVncmF0aW9uIGhvb2tzIGZvciBDb2RleC4gV2hlbiBkaXNhYmxlZCwgY211eCBubyBsb25nZXIgd3JhcHMgdGhlIGNvZGV4IGNvbW1hbmQgYnV0IHN0aWxsIHRyYWNrcyBsaXZlIENvZGV4IHNlc3Npb25zIGl0IGNhbiBvYnNlcnZlLiIKICAgICAgICB9LAogICAgICAgICJjbGF1ZGVCaW5hcnlQYXRoIjogewogICAgICAgICAgInR5", + "cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJkZWZhdWx0IjogIiIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiQ3VzdG9tIHBhdGggdG8gdGhlIGNsYXVkZSBiaW5hcnkuIgogICAgICAgIH0sCiAgICAgICAgIndvcmtzcGFjZUF1dG9OYW1pbmciOiB7CiAgICAgICAgICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogZmFsc2UsCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLmF1dG9tYXRpb24ud29ya3NwYWNlQXV0b05hbWluZyIsCiAgICAgICAgICAiZGVzY3Jp", + "cHRpb24iOiAiT3B0LWluIEFJIGF1dG8tbmFtaW5nIG9mIHdvcmtzcGFjZXMgYW5kIHRhYnMgZnJvbSBhZ2VudCBjb252ZXJzYXRpb24gY29udGVudC4gV2hlbiBlbmFibGVkLCBjbXV4IHN1bW1hcml6ZXMgc3VwcG9ydGVkIGFnZW50IHNlc3Npb25zIGludG8gc2hvcnQgdGl0bGVzIHVzaW5nIGVhY2ggYWdlbnQncyBvd24gYmluYXJ5OyBtYW51YWwgcmVuYW1lcyBhbHdheXMgd2luLiIKICAgICAgICB9LAogICAgICAgICJhdXRvTmFtaW5nQWdlbnQiOiB7CiAgICAgICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAgICAgImRl", + "ZmF1bHQiOiAiYXV0byIsCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLmF1dG9tYXRpb24uYXV0b05hbWluZ0FnZW50IiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJXaGljaCBhZ2VudCBnZW5lcmF0ZXMgYXV0by1uYW1lcyBmb3IgZXZlcnkgc2Vzc2lvbi4gXCJhdXRvXCIgKGRlZmF1bHQpIG5hbWVzIGVhY2ggc2Vzc2lvbiB3aXRoIGl0cyBvd24gYWdlbnQ7IGFueSBhZ2VudCBzbHVnIChjbGF1ZGUsIGNvZGV4LCBncm9rLCBvcGVuY29kZSwgcGksIG9tcCwg4oCmKSBvdmVycmlkZXMg", + "bmFtaW5nIGZvciBhbGwgc2Vzc2lvbnMsIGV2ZW4gb3RoZXIgYWdlbnRzJyBzZXNzaW9ucy4gVW5kcml2ZWFibGUgb3IgdW5pbnN0YWxsZWQgYWdlbnRzIGZhbGwgYmFjayB0byB0aGUgc2Vzc2lvbidzIG93biBhZ2VudCwgc28gbmFtaW5nIG5ldmVyIGJyZWFrcy4iCiAgICAgICAgfSwKICAgICAgICAicmlwZ3JlcEJpbmFyeVBhdGgiOiB7CiAgICAgICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAgICAgImRlZmF1bHQiOiAiIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJDdXN0b20gcGF0aCB0byB0aGUgcmlwZ3JlcCAo", + "cmcpIGJpbmFyeSB1c2VkIGJ5IHByb2plY3Qgc2VhcmNoLiIKICAgICAgICB9LAogICAgICAgICJzdXBwcmVzc1N1YmFnZW50Tm90aWZpY2F0aW9ucyI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiB0cnVlLAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5hdXRvbWF0aW9uLnN1cHByZXNzU3ViYWdlbnROb3RpZmljYXRpb25zIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTdXBwcmVzcyB2aXNpYmxlIGNvbXBsZXRpb24gbm90aWZpY2F0", + "aW9ucyBhbmQgc3RhdHVzIG11dGF0aW9ucyBmcm9tIG5lc3RlZCBDb2RleCBvciBDbGF1ZGUgY2hpbGQgYWdlbnRzIHdoaWxlIGtlZXBpbmcgdGhlaXIgZXZlbnRzIGluIEZlZWQgdGVsZW1ldHJ5LiIKICAgICAgICB9LAogICAgICAgICJhbXBJbnRlZ3JhdGlvbiI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiB0cnVlLAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5hdXRvbWF0aW9uLmFtcEludGVncmF0aW9uIiwKICAgICAgICAgICJkZXNj", + "cmlwdGlvbiI6ICJFbmFibGUgY211eCBpbnRlZ3JhdGlvbiBob29rcyBmb3IgQW1wLiBXaGVuIGRpc2FibGVkLCB0aGUgYnVuZGxlZCBwbHVnaW4gc3RheXMgaW5hY3RpdmUgd2l0aG91dCBuZWVkaW5nIHRvIGJlIHJlbW92ZWQuIgogICAgICAgIH0sCiAgICAgICAgImN1cnNvckludGVncmF0aW9uIjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiRW5hYmxlIGNtdXggaW50ZWdyYXRpb24gaG9va3MgZm9yIEN1cnNvci4iCiAg", + "ICAgICAgfSwKICAgICAgICAiZ2VtaW5pSW50ZWdyYXRpb24iOiB7CiAgICAgICAgICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogdHJ1ZSwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJFbmFibGUgY211eCBpbnRlZ3JhdGlvbiBob29rcyBmb3IgR2VtaW5pLiIKICAgICAgICB9LAogICAgICAgICJraXJvSW50ZWdyYXRpb24iOiB7CiAgICAgICAgICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogdHJ1ZSwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJFbmFibGUgY211eCBp", + "bnRlZ3JhdGlvbiBob29rcyBmb3IgS2lybyBDTEkuIgogICAgICAgIH0sCiAgICAgICAgImtpcm9Ob3RpZmljYXRpb25MZXZlbCI6IHsKICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAiZW51bSI6IFsKICAgICAgICAgICAgIm1pbmltYWwiLAogICAgICAgICAgICAic3RhbmRhcmQiLAogICAgICAgICAgICAidmVyYm9zZSIKICAgICAgICAgIF0sCiAgICAgICAgICAiZGVmYXVsdCI6ICJzdGFuZGFyZCIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiQ29udHJvbHMgaG93IG1hbnkgS2lybyB0b29sIGV2ZW50", + "cyBhcHBlYXIgaW4gRmVlZC4iCiAgICAgICAgfSwKICAgICAgICAicG9ydEJhc2UiOiB7CiAgICAgICAgICAidHlwZSI6ICJpbnRlZ2VyIiwKICAgICAgICAgICJtaW5pbXVtIjogMSwKICAgICAgICAgICJkZWZhdWx0IjogOTEwMCwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTdGFydGluZyB2YWx1ZSBmb3Igd29ya3NwYWNlIENNVVhfUE9SVCBhc3NpZ25tZW50cy4iCiAgICAgICAgfSwKICAgICAgICAicG9ydFJhbmdlIjogewogICAgICAgICAgInR5cGUiOiAiaW50ZWdlciIsCiAgICAgICAgICAibWluaW11bSI6IDEsCiAg", + "ICAgICAgICAiZGVmYXVsdCI6IDEwLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIk51bWJlciBvZiBwb3J0cyByZXNlcnZlZCBwZXIgd29ya3NwYWNlLiIKICAgICAgICB9CiAgICAgIH0KICAgIH0sCiAgICAiYnJvd3NlciI6IHsKICAgICAgIngtY211eC1zY29wZXMiOiBbImdsb2JhbCJdLAogICAgICAidGl0bGUiOiAiYnJvd3NlciIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJFbWJlZGRlZCBicm93c2VyIHNldHRpbmdzIGZyb20gU2V0dGluZ3MgPiBCcm93c2VyLiIsCiAgICAgICJ0eXBlIjogIm9iamVjdCIsCiAgICAgICJh", + "ZGRpdGlvbmFsUHJvcGVydGllcyI6IGZhbHNlLAogICAgICAicHJvcGVydGllcyI6IHsKICAgICAgICAiZGVmYXVsdFNlYXJjaEVuZ2luZSI6IHsKICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAiZW51bSI6IFsiZ29vZ2xlIiwgImR1Y2tkdWNrZ28iLCAiYmluZyIsICJrYWdpIiwgInN0YXJ0cGFnZSIsICJicmF2ZSIsICJwZXJwbGV4aXR5IiwgImV4YSIsICJ5YWhvbyIsICJlY29zaWEiLCAicXdhbnQiLCAibW9qZWVrIiwgIndpa2lwZWRpYSIsICJnaXRodWIiLCAiYmFpZHUiLCAieWFuZGV4IiwgImN1c3Rv", + "bSJdLAogICAgICAgICAgImRlZmF1bHQiOiAiZ29vZ2xlIiwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMuYnJvd3Nlci5kZWZhdWx0U2VhcmNoRW5naW5lIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJEZWZhdWx0IHNlYXJjaCBlbmdpbmUgZm9yIG5vbi1VUkwgYnJvd3NlciBhZGRyZXNzIGJhciBxdWVyaWVzLiBVc2UgY3VzdG9tIHdpdGggY3VzdG9tU2VhcmNoRW5naW5lVVJMVGVtcGxhdGUgZm9yIGFyYml0cmFyeSBwcm92aWRlcnMuIgogICAgICAgIH0sCiAgICAgICAgImN1c3Rv", + "bVNlYXJjaEVuZ2luZU5hbWUiOiB7CiAgICAgICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAgICAgImRlZmF1bHQiOiAiIiwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMuYnJvd3Nlci5jdXN0b21TZWFyY2hFbmdpbmVOYW1lIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJEaXNwbGF5IG5hbWUgdXNlZCB3aGVuIGRlZmF1bHRTZWFyY2hFbmdpbmUgaXMgY3VzdG9tLiIKICAgICAgICB9LAogICAgICAgICJjdXN0b21TZWFyY2hFbmdpbmVVUkxUZW1wbGF0ZSI6IHsKICAgICAgICAg", + "ICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAiZGVmYXVsdCI6ICJodHRwczovL3d3dy5nb29nbGUuY29tL3NlYXJjaD9xPXtxdWVyeX0iLAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5icm93c2VyLmN1c3RvbVNlYXJjaEVuZ2luZVVSTFRlbXBsYXRlIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTZWFyY2ggVVJMIHVzZWQgd2hlbiBkZWZhdWx0U2VhcmNoRW5naW5lIGlzIGN1c3RvbS4gSW5jbHVkZSB7cXVlcnl9IG9yICVzIGZvciB0aGUgZW5jb2RlZCBxdWVyeS4gSWYgb21p", + "dHRlZCwgY211eCBhcHBlbmRzIHE9IHRvIHRoZSBVUkwuIgogICAgICAgIH0sCiAgICAgICAgInNob3dTZWFyY2hTdWdnZXN0aW9ucyI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiB0cnVlLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlNob3cgb21uaWJhciBzZWFyY2ggc3VnZ2VzdGlvbnMuIgogICAgICAgIH0sCiAgICAgICAgInRoZW1lIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJlbnVtIjogWyJzeXN0ZW0iLCAibGlnaHQiLCAiZGFyayJd", + "LAogICAgICAgICAgImRlZmF1bHQiOiAic3lzdGVtIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJFbWJlZGRlZCBicm93c2VyIHRoZW1lLiIKICAgICAgICB9LAogICAgICAgICJkZWZhdWx0Wm9vbUxldmVsIjogewogICAgICAgICAgInR5cGUiOiAibnVtYmVyIiwKICAgICAgICAgICJtaW5pbXVtIjogMC4yNSwKICAgICAgICAgICJtYXhpbXVtIjogNSwKICAgICAgICAgICJkZWZhdWx0IjogMSwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMuYnJvd3Nlci5kZWZhdWx0Wm9vbUxldmVs", + "IiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJEZWZhdWx0IHBhZ2Ugem9vbSBmYWN0b3IgYXBwbGllZCB0byBuZXdseSBvcGVuZWQgYnJvd3NlciBwYWdlcyAoMSA9IDEwMCUpLiBab29tIEluLCBab29tIE91dCwgYW5kIEFjdHVhbCBTaXplIHN0aWxsIGFkanVzdCBlYWNoIHBhZ2UuIgogICAgICAgIH0sCiAgICAgICAgImRpc2NhcmRIaWRkZW5XZWJWaWV3cyI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiB0cnVlLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkFsbG93IGhp", + "ZGRlbiBicm93c2VyIHRhYnMgdG8gcmVsZWFzZSBwYWdlIG1lbW9yeSBhbmQgcmVzdG9yZSB3aGVuIHNob3duIGFnYWluLiIKICAgICAgICB9LAogICAgICAgICJoaWRkZW5XZWJWaWV3RGlzY2FyZERlbGF5U2Vjb25kcyI6IHsKICAgICAgICAgICJ0eXBlIjogIm51bWJlciIsCiAgICAgICAgICAibWluaW11bSI6IDAsCiAgICAgICAgICAibWF4aW11bSI6IDM2MDAsCiAgICAgICAgICAiZGVmYXVsdCI6IDMwMCwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTZWNvbmRzIGEgYnJvd3NlciB0YWIgbXVzdCBzdGF5IGhpZGRlbiBi", + "ZWZvcmUgY211eCBmcmVlcyBpdHMgcGFnZSBtZW1vcnkuIgogICAgICAgIH0sCiAgICAgICAgImFza1doZXJlVG9TYXZlRG93bmxvYWRzIjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IGZhbHNlLAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5icm93c2VyLmFza1doZXJlVG9TYXZlRG93bmxvYWRzIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTaG93IGEgc2F2ZSBwYW5lbCBmb3IgYnJvd3NlciBkb3dubG9hZHMgaW5zdGVhZCBvZiBz", + "YXZpbmcgZGlyZWN0bHkgdG8gRG93bmxvYWRzLiIKICAgICAgICB9LAogICAgICAgICJvcGVuVGVybWluYWxMaW5rc0luQ211eEJyb3dzZXIiOiB7CiAgICAgICAgICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogdHJ1ZSwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJPcGVuIGNsaWNrZWQgdGVybWluYWwgbGlua3MgaW4gdGhlIGVtYmVkZGVkIGJyb3dzZXIuIgogICAgICAgIH0sCiAgICAgICAgImludGVyY2VwdFRlcm1pbmFsT3BlbkNvbW1hbmRJbkNtdXhCcm93c2VyIjogewogICAgICAgICAgInR5", + "cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiSW50ZXJjZXB0IHRlcm1pbmFsIG9wZW4gaHR0cChzKSBjb21tYW5kcyBhbmQgcm91dGUgdGhlbSB0aHJvdWdoIHRoZSBlbWJlZGRlZCBicm93c2VyLiIKICAgICAgICB9LAogICAgICAgICJob3N0c1RvT3BlbkluRW1iZWRkZWRCcm93c2VyIjogewogICAgICAgICAgInR5cGUiOiAiYXJyYXkiLAogICAgICAgICAgIml0ZW1zIjogewogICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICB9LAog", + "ICAgICAgICAgImRlZmF1bHQiOiBbXSwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJBbGxvd2xpc3Qgb2YgaG9zdHMgdGhhdCBzaG91bGQgc3RheSBpbnNpZGUgdGhlIGVtYmVkZGVkIGJyb3dzZXIuIgogICAgICAgIH0sCiAgICAgICAgInVybHNUb0Fsd2F5c09wZW5FeHRlcm5hbGx5IjogewogICAgICAgICAgInR5cGUiOiAiYXJyYXkiLAogICAgICAgICAgIml0ZW1zIjogewogICAgICAgICAgICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICB9LAogICAgICAgICAgImRlZmF1bHQiOiBbXSwKICAgICAgICAgICJkZXNjcmlw", + "dGlvbiI6ICJSdWxlcyB0aGF0IGFsd2F5cyBvcGVuIG1hdGNoaW5nIFVSTHMgaW4gdGhlIHN5c3RlbSBicm93c2VyLiIKICAgICAgICB9LAogICAgICAgICJpbnNlY3VyZUh0dHBIb3N0c0FsbG93ZWRJbkVtYmVkZGVkQnJvd3NlciI6IHsKICAgICAgICAgICJ0eXBlIjogImFycmF5IiwKICAgICAgICAgICJpdGVtcyI6IHsKICAgICAgICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgICAgICAgfSwKICAgICAgICAgICJkZWZhdWx0IjogWwogICAgICAgICAgICAibG9jYWxob3N0IiwKICAgICAgICAgICAgIioubG9jYWxob3N0IiwK", + "ICAgICAgICAgICAgIjEyNy4wLjAuMSIsCiAgICAgICAgICAgICI6OjEiLAogICAgICAgICAgICAiMC4wLjAuMCIsCiAgICAgICAgICAgICIqLmxvY2FsdGVzdC5tZSIKICAgICAgICAgIF0sCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiSFRUUCBob3N0cyBhbGxvd2VkIGluIHRoZSBlbWJlZGRlZCBicm93c2VyIHdpdGhvdXQgYSB3YXJuaW5nIHByb21wdC4iCiAgICAgICAgfSwKICAgICAgICAidXJsQWxsb3dsaXN0IjogewogICAgICAgICAgInR5cGUiOiAiYXJyYXkiLAogICAgICAgICAgIml0ZW1zIjogewogICAgICAgICAg", + "ICAidHlwZSI6ICJzdHJpbmciCiAgICAgICAgICB9LAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5icm93c2VyLnVybEFsbG93bGlzdCIsCiAgICAgICAgICAiZGVmYXVsdCI6IFsKICAgICAgICAgICAgImxvY2FsaG9zdCIsCiAgICAgICAgICAgICIqLmxvY2FsaG9zdCIsCiAgICAgICAgICAgICIxMjcuMC4wLjEiLAogICAgICAgICAgICAiOjoxIiwKICAgICAgICAgICAgIjAuMC4wLjAiLAogICAgICAgICAgICAiKi5sb2NhbHRlc3QubWUiCiAgICAgICAgICBdLAogICAgICAgICAgImRl", + "c2NyaXB0aW9uIjogIkhvc3Qgb3IgVVJMIHBhdHRlcm5zIHRoYXQgcmVzdHJpY3QgZW1iZWRkZWQtYnJvd3NlciBuYXZpZ2F0aW9uLiBUaGUgU2V0dGluZ3MgVUkgc3VnZ2VzdHMgbG9jYWwgZGV2ZWxvcG1lbnQgb3JpZ2luczsgc2F2aW5nIGEgbGlzdCBvcHRzIGludG8gdGhlIG9wdGlvbmFsIHJlc3RyaWN0aW9uLiBSZW1vdmUgZW50cmllcyB0byBibG9jayB0aGVtLCBvciBsZWF2ZSB0aGUgdXNlciB2YWx1ZSBlbXB0eSB0byBkaXNhYmxlIGl0IHdoZW4gbm8gbWFuYWdlZCBwb2xpY3kgYXBwbGllcy4iCiAgICAgICAgfSwKICAg", + "ICAgICAic2hvd0ltcG9ydEhpbnRPbkJsYW5rVGFicyI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiB0cnVlLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlNob3cgdGhlIGJyb3dzZXIgaW1wb3J0IGhpbnQgb24gYmxhbmsgdGFicy4iCiAgICAgICAgfSwKICAgICAgICAicmVhY3RHcmFiVmVyc2lvbiI6IHsKICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAiZGVmYXVsdCI6ICIwLjEuMjkiLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlBpbm5lZCByZWFj", + "dC1ncmFiIHZlcnNpb24gZm9yIHRoZSBicm93c2VyIHRvb2xiYXIgaGVscGVyLiIKICAgICAgICB9CiAgICAgIH0KICAgIH0sCiAgICAibW9iaWxlIjogewogICAgICAieC1jbXV4LXNjb3BlcyI6IFsiZ2xvYmFsIl0sCiAgICAgICJ0aXRsZSI6ICJtb2JpbGUiLAogICAgICAiZGVzY3JpcHRpb24iOiAiTWFjLXNpZGUgc2V0dGluZ3MgZm9yIGNtdXggb24gaU9TLiIsCiAgICAgICJ0eXBlIjogIm9iamVjdCIsCiAgICAgICJhZGRpdGlvbmFsUHJvcGVydGllcyI6IGZhbHNlLAogICAgICAicHJvcGVydGllcyI6IHsKICAgICAgICAi", + "YXJ0aWZhY3RGb2xkZXJBY2Nlc3MiOiB7CiAgICAgICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAgICAgImVudW0iOiBbInN1YnRyZWUiLCAib25lTGV2ZWwiXSwKICAgICAgICAgICJkZWZhdWx0IjogInN1YnRyZWUiLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkNvbnRyb2xzIHdoZXRoZXIgYSByZWZlcmVuY2VkIG9yIHRlcm1pbmFsLXZpc2libGUgZGlyZWN0b3J5IGF1dGhvcml6ZXMgaXRzIGZ1bGwgY2Fub25pY2FsIHN1YnRyZWUgb3Igb25seSBpbW1lZGlhdGUgY2hpbGRyZW4uIgogICAgICAgIH0KICAgICAgfQog", + "ICAgfSwKICAgICJtYXJrZG93biI6IHsKICAgICAgIngtY211eC1zY29wZXMiOiBbImdsb2JhbCJdLAogICAgICAidGl0bGUiOiAibWFya2Rvd24iLAogICAgICAiZGVzY3JpcHRpb24iOiAiQnVpbHQtaW4gbWFya2Rvd24gdmlld2VyIHNldHRpbmdzLiIsCiAgICAgICJ0eXBlIjogIm9iamVjdCIsCiAgICAgICJhZGRpdGlvbmFsUHJvcGVydGllcyI6IGZhbHNlLAogICAgICAicHJvcGVydGllcyI6IHsKICAgICAgICAiZm9udFNpemUiOiB7CiAgICAgICAgICAidHlwZSI6ICJpbnRlZ2VyIiwKICAgICAgICAgICJtaW5pbXVtIjog", + "OCwKICAgICAgICAgICJtYXhpbXVtIjogOTYsCiAgICAgICAgICAiZGVmYXVsdCI6IDE1LAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkRlZmF1bHQgYm9keSBmb250IHNpemUsIGluIHBvaW50cywgZm9yIG5ld2x5IG9wZW5lZCBtYXJrZG93biB2aWV3ZXJzLiBab29tIGEgdmlld2VyIGxpdmUgd2l0aCBDbWQtKyAvIENtZC0tIC8gQ21kLTAuIiwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMubWFya2Rvd24uZm9udFNpemUiCiAgICAgICAgfSwKICAgICAgICAiZm9udEZhbWlseSI6IHsK", + "ICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAiZGVmYXVsdCI6ICIiLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkRlZmF1bHQgYm9keSBmb250IGZhbWlseSBmb3IgbmV3bHkgb3BlbmVkIG1hcmtkb3duIHZpZXdlcnMuIExlYXZlIGVtcHR5IGZvciB0aGUgc3lzdGVtIG1hcmtkb3duIGZvbnQgc3RhY2suIiwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMubWFya2Rvd24uZm9udEZhbWlseSIKICAgICAgICB9LAogICAgICAgICJtYXhXaWR0aCI6IHsKICAgICAgICAg", + "ICJ0eXBlIjogImludGVnZXIiLAogICAgICAgICAgIm1pbmltdW0iOiAzMjAsCiAgICAgICAgICAibWF4aW11bSI6IDI0MDAsCiAgICAgICAgICAiZGVmYXVsdCI6IDk4MCwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJEZWZhdWx0IG1heGltdW0gcmVhZGluZyBjb2x1bW4gd2lkdGgsIGluIENTUyBwaXhlbHMsIGZvciBuZXdseSBvcGVuZWQgbWFya2Rvd24gdmlld2Vycy4iLAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5tYXJrZG93bi5tYXhXaWR0aCIKICAgICAgICB9CiAgICAgIH0K", + "ICAgIH0sCiAgICAiY2FudmFzIjogewogICAgICAieC1jbXV4LXNjb3BlcyI6IFsiZ2xvYmFsIl0sCiAgICAgICJ0aXRsZSI6ICJjYW52YXMiLAogICAgICAiZGVzY3JpcHRpb24iOiAiRnJlZWZvcm0gY2FudmFzIHdvcmtzcGFjZSBsYXlvdXQgc2V0dGluZ3MuIiwKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjogZmFsc2UsCiAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICJwYW5lR2FwIjogewogICAgICAgICAgInR5cGUiOiAiaW50ZWdlciIsCiAgICAgICAgICAibWluaW11", + "bSI6IDAsCiAgICAgICAgICAibWF4aW11bSI6IDY0LAogICAgICAgICAgImRlZmF1bHQiOiAxNiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTcGFjaW5nIGJldHdlZW4gcGFuZXMgaW4gdGhlIGNhbnZhcyBsYXlvdXQsIGluIHBvaW50cy4gU25hcHBpbmcsIHRpZHksIGFuZCBuZXctcGFuZSBwbGFjZW1lbnQgYWxsIHVzZSB0aGlzIG9uZSBnYXAuIiwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMuY2FudmFzLnBhbmVHYXAiCiAgICAgICAgfSwKICAgICAgICAic25hcHBpbmdFbmFibGVk", + "IjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiU25hcCBwYW5lIGRyYWdzIGFuZCByZXNpemVzIHRvIG5laWdoYm9yIGVkZ2VzIGFuZCB0aGUgcGFuZSBnYXAuIEhvbGQgQ29tbWFuZCB0byBzdXNwZW5kIHNuYXBwaW5nIGZvciBvbmUgZ2VzdHVyZS4iLAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5jYW52YXMuc25hcHBpbmdFbmFibGVkIgogICAgICAgIH0KICAgICAgfQogICAgfSwK", + "ICAgICJmaWxlRWRpdG9yIjogewogICAgICAieC1jbXV4LXNjb3BlcyI6IFsiZ2xvYmFsIl0sCiAgICAgICJ0aXRsZSI6ICJmaWxlRWRpdG9yIiwKICAgICAgImRlc2NyaXB0aW9uIjogIkJ1aWx0LWluIGZpbGUgZWRpdG9yIHNldHRpbmdzLiIsCiAgICAgICJ0eXBlIjogIm9iamVjdCIsCiAgICAgICJhZGRpdGlvbmFsUHJvcGVydGllcyI6IGZhbHNlLAogICAgICAicHJvcGVydGllcyI6IHsKICAgICAgICAid29yZFdyYXAiOiB7CiAgICAgICAgICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogZmFsc2Us", + "CiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiV3JhcCBsb25nIGxpbmVzIGF0IHRoZSBlZGl0b3IncyByaWdodCBlZGdlIGluc3RlYWQgb2Ygc2Nyb2xsaW5nIGhvcml6b250YWxseS4iLAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5maWxlRWRpdG9yLndvcmRXcmFwIgogICAgICAgIH0sCiAgICAgICAgInN5bnRheEhpZ2hsaWdodGluZyI6IHsKICAgICAgICAgICJ0eXBlIjogImJvb2xlYW4iLAogICAgICAgICAgImRlZmF1bHQiOiB0cnVlLAogICAgICAgICAgImRlc2NyaXB0aW9uIjog", + "IkNvbG9yIHNvdXJjZSB0b2tlbnMgaW4gdGhlIGJ1aWx0LWluIGZpbGUgZWRpdG9yLiIsCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLmZpbGVFZGl0b3Iuc3ludGF4SGlnaGxpZ2h0aW5nIgogICAgICAgIH0sCiAgICAgICAgImxpbmVOdW1iZXJzIjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiU2hvdyBhIGxpbmUtbnVtYmVyIGd1dHRlciBpbiB0aGUgYnVpbHQtaW4gZmlsZSBlZGl0", + "b3IuIiwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMuZmlsZUVkaXRvci5saW5lTnVtYmVycyIKICAgICAgICB9LAogICAgICAgICJpbmRlbnRHdWlkZXMiOiB7CiAgICAgICAgICAidHlwZSI6ICJib29sZWFuIiwKICAgICAgICAgICJkZWZhdWx0IjogdHJ1ZSwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJEcmF3IHZlcnRpY2FsIGluZGVudCBndWlkZXMgaW4gdGhlIGJ1aWx0LWluIGZpbGUgZWRpdG9yLiIsCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRp", + "b25zLmZpbGVFZGl0b3IuaW5kZW50R3VpZGVzIgogICAgICAgIH0sCiAgICAgICAgImN1cnJlbnRMaW5lSGlnaGxpZ2h0IjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiSGlnaGxpZ2h0IHRoZSBjYXJldCdzIGxpbmUgd2hlbiB0aGUgc2VsZWN0aW9uIGlzIGVtcHR5LiIsCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLmZpbGVFZGl0b3IuY3VycmVudExpbmVIaWdobGlnaHQiCiAgICAg", + "ICAgfSwKICAgICAgICAidGFiV2lkdGgiOiB7CiAgICAgICAgICAidHlwZSI6ICJpbnRlZ2VyIiwKICAgICAgICAgICJkZWZhdWx0IjogNCwKICAgICAgICAgICJtaW5pbXVtIjogMSwKICAgICAgICAgICJtYXhpbXVtIjogOCwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJDb2x1bW5zIHBlciB0YWIgc3RvcCBmb3IgaW5kZW50IGd1aWRlcy4iLAogICAgICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5maWxlRWRpdG9yLnRhYldpZHRoIgogICAgICAgIH0KICAgICAgfQogICAgfSwKICAgICJmaWxl", + "RXhwbG9yZXIiOiB7CiAgICAgICJ4LWNtdXgtc2NvcGVzIjogWyJnbG9iYWwiXSwKICAgICAgInRpdGxlIjogImZpbGVFeHBsb3JlciIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJSaWdodC1zaWRlYmFyIGZpbGUgZXhwbG9yZXIgKGZpbGUgdHJlZSkgc2V0dGluZ3MuIiwKICAgICAgImRlc2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5maWxlRXhwbG9yZXIuZGVzY3JpcHRpb24iLAogICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiBmYWxzZSwKICAgICAgInByb3BlcnRp", + "ZXMiOiB7CiAgICAgICAgImRvdWJsZUNsaWNrQWN0aW9uIjogewogICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgICAgICJlbnVtIjogWyJwcmV2aWV3IiwgImRlZmF1bHRFZGl0b3IiLCAicHJlZmVycmVkRWRpdG9yIl0sCiAgICAgICAgICAiZGVmYXVsdCI6ICJwcmV2aWV3IiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJXaGF0IGRvdWJsZS1jbGlja2luZyAob3IgcHJlc3NpbmcgUmV0dXJuIG9uIGEgc2VhcmNoIHJlc3VsdCBmb3IpIGEgZmlsZSBpbiB0aGUgZmlsZSBleHBsb3JlciBkb2VzLiBgcHJldmlld2Ag", + "b3BlbnMgdGhlIGJ1aWx0LWluIGNtdXggZmlsZSBwcmV2aWV3ICh0aGUgZGVmYXVsdCBhbmQgaGlzdG9yaWNhbCBiZWhhdmlvcikuIGBkZWZhdWx0RWRpdG9yYCBvcGVucyB3aXRoIHRoZSBtYWNPUyBkZWZhdWx0IGFwcCBmb3IgdGhlIGZpbGUgdHlwZS4gYHByZWZlcnJlZEVkaXRvcmAgb3BlbnMgd2l0aCB0aGUgYGFwcC5wcmVmZXJyZWRFZGl0b3JgIGNvbW1hbmQsIGZhbGxpbmcgYmFjayB0byB0aGUgZGVmYXVsdCBhcHAgd2hlbiBub25lIGlzIHNldC4gT25seSBhcHBsaWVzIHRvIGZpbGVzOyBkaXJlY3RvcmllcyBhbHdheXMg", + "ZXhwYW5kL2NvbGxhcHNlLCBhbmQgbm9uLWxvY2FsIChyZW1vdGUpIGZpbGUgZXhwbG9yZXJzIGFsd2F5cyBvcGVuIHRoZSBjbXV4IHByZXZpZXcuIiwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMuZmlsZUV4cGxvcmVyLmRvdWJsZUNsaWNrQWN0aW9uIgogICAgICAgIH0KICAgICAgfQogICAgfSwKICAgICJkaWZmVmlld2VyIjogewogICAgICAieC1jbXV4LXNjb3BlcyI6IFsiZ2xvYmFsIl0sCiAgICAgICJ0aXRsZSI6ICJkaWZmVmlld2VyIiwKICAgICAgImRlc2NyaXB0aW9uIjogIkJ1", + "aWx0LWluIGRpZmYgdmlld2VyIHNldHRpbmdzLiIsCiAgICAgICJ0eXBlIjogIm9iamVjdCIsCiAgICAgICJhZGRpdGlvbmFsUHJvcGVydGllcyI6IGZhbHNlLAogICAgICAicHJvcGVydGllcyI6IHsKICAgICAgICAiZGVmYXVsdExheW91dCI6IHsKICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAiZW51bSI6IFsidW5pZmllZCIsICJzcGxpdCJdLAogICAgICAgICAgImRlZmF1bHQiOiAidW5pZmllZCIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiRGVmYXVsdCBsYXlvdXQgZm9yIG5ld2x5IG9wZW5lZCBk", + "aWZmIHZpZXdlcnMuIFRoZSB0b29sYmFyIGxheW91dCB0b2dnbGUgcGVyc2lzdHMgdGhlIHVzZXIncyBsYXN0IGNob2ljZTsgYW4gZXhwbGljaXQgYGNtdXggZGlmZiAtLWxheW91dGAgb3B0aW9uIG92ZXJyaWRlcyBib3RoLiIsCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLmRpZmZWaWV3ZXIuZGVmYXVsdExheW91dCIKICAgICAgICB9CiAgICAgIH0KICAgIH0sCiAgICAicmlnaHRTaWRlYmFyIjogewogICAgICAidGl0bGUiOiAicmlnaHRTaWRlYmFyIiwKICAgICAgImRlc2NyaXB0aW9u", + "IjogIkxlZ2FjeSBhbmQgZXh0ZW5zaW9uLW93bmVkIHJpZ2h0LXNpZGViYXIgY29uZmlndXJhdGlvbiBwcmVzZXJ2ZWQgaW4gY211eC5qc29uLiIsCiAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMucmlnaHRTaWRlYmFyIiwKICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjogdHJ1ZQogICAgfSwKICAgICJzaG9ydGN1dHMiOiB7CiAgICAgICJ4LWNtdXgtc2NvcGVzIjogWyJnbG9iYWwiXSwKICAgICAgInRpdGxlIjogInNob3J0Y3V0cyIsCiAgICAgICJk", + "ZXNjcmlwdGlvbiI6ICJLZXlib2FyZCBzaG9ydGN1dCBzZXR0aW5ncyBmcm9tIFNldHRpbmdzID4gS2V5Ym9hcmQgU2hvcnRjdXRzLiIsCiAgICAgICJ0eXBlIjogIm9iamVjdCIsCiAgICAgICJhZGRpdGlvbmFsUHJvcGVydGllcyI6IGZhbHNlLAogICAgICAicHJvcGVydGllcyI6IHsKICAgICAgICAic2hvd01vZGlmaWVySG9sZEhpbnRzIjogewogICAgICAgICAgInR5cGUiOiAiYm9vbGVhbiIsCiAgICAgICAgICAiZGVmYXVsdCI6IHRydWUsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiU2hvdyBzaG9ydGN1dC1oaW50IGNo", + "aXBzIHdoaWxlIGhvbGRpbmcgQ21kIG9yIENvbnRyb2wuIiwKICAgICAgICAgICJkZXNjcmlwdGlvbktleSI6ICJzY2hlbWFEZXNjcmlwdGlvbnMuc2hvcnRjdXRzLnNob3dNb2RpZmllckhvbGRIaW50cyIKICAgICAgICB9LAogICAgICAgICJiaW5kaW5ncyI6IHsKICAgICAgICAgICJ0eXBlIjogIm9iamVjdCIsCiAgICAgICAgICAiZGVmYXVsdCI6IHt9LAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlNob3J0Y3V0IG92ZXJyaWRlcyBrZXllZCBieSBjbXV4IGFjdGlvbiBpZC4gVXNlIGEgc3RyaW5nIGZvciBhIHNpbmdsZSBz", + "aG9ydGN1dCwgYW4gYXJyYXkgZm9yIGEgY2hvcmQsIG51bGwsIGFuIGVtcHR5IHN0cmluZywgbm9uZSwgY2xlYXIsIHVuYm91bmQsIG9yIGRpc2FibGVkIHRvIHVuYmluZC4iLAogICAgICAgICAgInByb3BlcnR5TmFtZXMiOiB7CiAgICAgICAgICAgICJlbnVtIjogWwogICAgICAgICAgICAgICJvcGVuU2V0dGluZ3MiLAogICAgICAgICAgICAgICJvcGVuVGVhbVBpY2tlciIsCiAgICAgICAgICAgICAgInJlbG9hZENvbmZpZ3VyYXRpb24iLAogICAgICAgICAgICAgICJzaG93SGlkZUFsbFdpbmRvd3MiLAogICAgICAgICAgICAg", + "ICJnbG9iYWxTZWFyY2giLAogICAgICAgICAgICAgICJuZXdXaW5kb3ciLAogICAgICAgICAgICAgICJjbG9zZVdpbmRvdyIsCiAgICAgICAgICAgICAgInRvZ2dsZUZ1bGxTY3JlZW4iLAogICAgICAgICAgICAgICJxdWl0IiwKICAgICAgICAgICAgICAidG9nZ2xlU2lkZWJhciIsCiAgICAgICAgICAgICAgIm5ld1RhYiIsCiAgICAgICAgICAgICAgIm5ld0Jyb3dzZXJXb3Jrc3BhY2UiLAogICAgICAgICAgICAgICJuZXdDbG91ZFdvcmtzcGFjZSIsCiAgICAgICAgICAgICAgIm5ld0Nsb3VkTWFjaGluZSIsCiAgICAgICAgICAg", + "ICAgInNhdmVMYXlvdXRUZW1wbGF0ZSIsCiAgICAgICAgICAgICAgIm9wZW5Gb2xkZXIiLAogICAgICAgICAgICAgICJyZW9wZW5QcmV2aW91c1Nlc3Npb24iLAogICAgICAgICAgICAgICJnb1RvV29ya3NwYWNlIiwKICAgICAgICAgICAgICAiY29tbWFuZFBhbGV0dGUiLAogICAgICAgICAgICAgICJjb21tYW5kUGFsZXR0ZU5leHQiLAogICAgICAgICAgICAgICJjb21tYW5kUGFsZXR0ZVByZXZpb3VzIiwKICAgICAgICAgICAgICAic2VuZEZlZWRiYWNrIiwKICAgICAgICAgICAgICAic2hvd05vdGlmaWNhdGlvbnMiLAogICAg", + "ICAgICAgICAgICJqdW1wVG9VbnJlYWQiLAogICAgICAgICAgICAgICJ0b2dnbGVVbnJlYWQiLAogICAgICAgICAgICAgICJtYXJrT2xkZXN0VW5yZWFkQW5kSnVtcE5leHQiLAogICAgICAgICAgICAgICJtYXJrQWxsTm90aWZpY2F0aW9uc1JlYWQiLAogICAgICAgICAgICAgICJjbGVhckFsbE5vdGlmaWNhdGlvbnMiLAogICAgICAgICAgICAgICJmb2N1c1JpZ2h0U2lkZWJhciIsCiAgICAgICAgICAgICAgInN3aXRjaFJpZ2h0U2lkZWJhclRvRmlsZXMiLAogICAgICAgICAgICAgICJzd2l0Y2hSaWdodFNpZGViYXJUb0ZpbmQi", + "LAogICAgICAgICAgICAgICJzd2l0Y2hSaWdodFNpZGViYXJUb1Nlc3Npb25zIiwKICAgICAgICAgICAgICAic3dpdGNoUmlnaHRTaWRlYmFyVG9GZWVkIiwKICAgICAgICAgICAgICAic3dpdGNoUmlnaHRTaWRlYmFyVG9Eb2NrIiwKICAgICAgICAgICAgICAic3dpdGNoUmlnaHRTaWRlYmFyVG9NYWNoaW5lcyIsCiAgICAgICAgICAgICAgInRyaWdnZXJGbGFzaCIsCiAgICAgICAgICAgICAgIm5leHRTdXJmYWNlIiwKICAgICAgICAgICAgICAicHJldlN1cmZhY2UiLAogICAgICAgICAgICAgICJtb3ZlU3VyZmFjZUxlZnQiLAog", + "ICAgICAgICAgICAgICJtb3ZlU3VyZmFjZVJpZ2h0IiwKICAgICAgICAgICAgICAibW92ZVN1cmZhY2VUb1ByZXZpb3VzUGFuZSIsCiAgICAgICAgICAgICAgIm1vdmVTdXJmYWNlVG9OZXh0UGFuZSIsCiAgICAgICAgICAgICAgIm1vdmVTdXJmYWNlVG9QYW5lTGVmdCIsCiAgICAgICAgICAgICAgIm1vdmVTdXJmYWNlVG9QYW5lUmlnaHQiLAogICAgICAgICAgICAgICJtb3ZlU3VyZmFjZVRvUGFuZVVwIiwKICAgICAgICAgICAgICAibW92ZVN1cmZhY2VUb1BhbmVEb3duIiwKICAgICAgICAgICAgICAic2VsZWN0U3VyZmFjZUJ5", + "TnVtYmVyIiwKICAgICAgICAgICAgICAibmV4dFNpZGViYXJUYWIiLAogICAgICAgICAgICAgICJwcmV2U2lkZWJhclRhYiIsCiAgICAgICAgICAgICAgIm5leHRTaWRlYmFyVGFiSW5Hcm91cCIsCiAgICAgICAgICAgICAgInByZXZTaWRlYmFyVGFiSW5Hcm91cCIsCiAgICAgICAgICAgICAgIm1vdmVXb3Jrc3BhY2VVcCIsCiAgICAgICAgICAgICAgIm1vdmVXb3Jrc3BhY2VEb3duIiwKICAgICAgICAgICAgICAiZm9jdXNIaXN0b3J5QmFjayIsCiAgICAgICAgICAgICAgImZvY3VzSGlzdG9yeUZvcndhcmQiLAogICAgICAgICAg", + "ICAgICJmb2N1c0hpc3RvcnlMYXN0IiwKICAgICAgICAgICAgICAic2VsZWN0V29ya3NwYWNlQnlOdW1iZXIiLAogICAgICAgICAgICAgICJyZW5hbWVUYWIiLAogICAgICAgICAgICAgICJyZW5hbWVXb3Jrc3BhY2UiLAogICAgICAgICAgICAgICJlZGl0V29ya3NwYWNlRGVzY3JpcHRpb24iLAogICAgICAgICAgICAgICJtYXJrV29ya3NwYWNlRG9uZSIsCiAgICAgICAgICAgICAgImN5Y2xlV29ya3NwYWNlU3RhdHVzIiwKICAgICAgICAgICAgICAidG9nZ2xlQ2hlY2tsaXN0SXRlbUNvbXBsZXRlIiwKICAgICAgICAgICAgICAi", + "Y2xvc2VUYWIiLAogICAgICAgICAgICAgICJjbG9zZU90aGVyVGFic0luUGFuZSIsCiAgICAgICAgICAgICAgImNsb3NlV29ya3NwYWNlIiwKICAgICAgICAgICAgICAibmV3V29ya3NwYWNlR3JvdXAiLAogICAgICAgICAgICAgICJncm91cFNlbGVjdGVkV29ya3NwYWNlcyIsCiAgICAgICAgICAgICAgInRvZ2dsZUZvY3VzZWRXb3Jrc3BhY2VHcm91cENvbGxhcHNlZCIsCiAgICAgICAgICAgICAgInJlb3BlbkNsb3NlZFdvcmtzcGFjZSIsCiAgICAgICAgICAgICAgInJlb3BlbkNsb3NlZEJyb3dzZXJQYW5lbCIsCiAgICAgICAg", + "ICAgICAgIm5ld1N1cmZhY2UiLAogICAgICAgICAgICAgICJ0b2dnbGVUZXJtaW5hbENvcHlNb2RlIiwKICAgICAgICAgICAgICAiZm9jdXNUZXh0Qm94SW5wdXQiLAogICAgICAgICAgICAgICJjeWNsZVRleHRCb3hTdWJtaXRBY3Rpb24iLAogICAgICAgICAgICAgICJhdHRhY2hUZXh0Qm94RmlsZSIsCiAgICAgICAgICAgICAgInNlbmRDdHJsRlRvVGVybWluYWwiLAogICAgICAgICAgICAgICJjbGVhclNjcmVlbktlZXBTY3JvbGxiYWNrIiwKICAgICAgICAgICAgICAic2ltdWxhdG9ySG9tZSIsCiAgICAgICAgICAgICAgInNp", + "bXVsYXRvclJvdGF0ZUxlZnQiLAogICAgICAgICAgICAgICJzaW11bGF0b3JSb3RhdGVSaWdodCIsCiAgICAgICAgICAgICAgInNpbXVsYXRvclRvZ2dsZUFwcGVhcmFuY2UiLAogICAgICAgICAgICAgICJzaW11bGF0b3JUb2dnbGVTb2Z0d2FyZUtleWJvYXJkIiwKICAgICAgICAgICAgICAiZm9jdXNMZWZ0IiwKICAgICAgICAgICAgICAiZm9jdXNSaWdodCIsCiAgICAgICAgICAgICAgImZvY3VzVXAiLAogICAgICAgICAgICAgICJmb2N1c0Rvd24iLAogICAgICAgICAgICAgICJmb2N1c1ByZXZpb3VzUGFuZSIsCiAgICAgICAg", + "ICAgICAgImZvY3VzTmV4dFBhbmUiLAogICAgICAgICAgICAgICJzcGxpdFJpZ2h0IiwKICAgICAgICAgICAgICAic3BsaXREb3duIiwKICAgICAgICAgICAgICAidG9nZ2xlU3BsaXRab29tIiwKICAgICAgICAgICAgICAiaW5jcmVhc2VXb3Jrc3BhY2VUZXJtaW5hbEZvbnRTaXplIiwKICAgICAgICAgICAgICAiZGVjcmVhc2VXb3Jrc3BhY2VUZXJtaW5hbEZvbnRTaXplIiwKICAgICAgICAgICAgICAicmVzZXRXb3Jrc3BhY2VUZXJtaW5hbEZvbnRTaXplIiwKICAgICAgICAgICAgICAiZXF1YWxpemVTcGxpdHMiLAogICAgICAg", + "ICAgICAgICJyZXNpemUtcGFuZS1sZWZ0IiwKICAgICAgICAgICAgICAicmVzaXplLXBhbmUtcmlnaHQiLAogICAgICAgICAgICAgICJyZXNpemUtcGFuZS11cCIsCiAgICAgICAgICAgICAgInJlc2l6ZS1wYW5lLWRvd24iLAogICAgICAgICAgICAgICJzcGxpdEJyb3dzZXJSaWdodCIsCiAgICAgICAgICAgICAgInNwbGl0QnJvd3NlckRvd24iLAogICAgICAgICAgICAgICJ0b2dnbGVDYW52YXNMYXlvdXQiLAogICAgICAgICAgICAgICJjYW52YXNSZXZlYWxGb2N1c2VkUGFuZSIsCiAgICAgICAgICAgICAgImNhbnZhc092ZXJ2", + "aWV3IiwKICAgICAgICAgICAgICAiY2FudmFzWm9vbUluIiwKICAgICAgICAgICAgICAiY2FudmFzWm9vbU91dCIsCiAgICAgICAgICAgICAgImNhbnZhc1pvb21SZXNldCIsCiAgICAgICAgICAgICAgImNhbnZhc1RpZHkiLAogICAgICAgICAgICAgICJjYW52YXNBbGlnbkxlZnQiLAogICAgICAgICAgICAgICJjYW52YXNBbGlnblJpZ2h0IiwKICAgICAgICAgICAgICAiY2FudmFzQWxpZ25Ub3AiLAogICAgICAgICAgICAgICJjYW52YXNBbGlnbkJvdHRvbSIsCiAgICAgICAgICAgICAgImNhbnZhc0VxdWFsaXplV2lkdGhzIiwK", + "ICAgICAgICAgICAgICAiY2FudmFzRXF1YWxpemVIZWlnaHRzIiwKICAgICAgICAgICAgICAiY2FudmFzRGlzdHJpYnV0ZUhvcml6b250YWxseSIsCiAgICAgICAgICAgICAgImNhbnZhc0Rpc3RyaWJ1dGVWZXJ0aWNhbGx5IiwKICAgICAgICAgICAgICAidG9nZ2xlRmlsZUV4cGxvcmVyIiwKICAgICAgICAgICAgICAiZmlsZUV4cGxvcmVyT3BlblNlbGVjdGlvbiIsCiAgICAgICAgICAgICAgImZpbGVFeHBsb3Jlck9wZW5TZWxlY3Rpb25GaW5kZXJBbGlhcyIsCiAgICAgICAgICAgICAgInNhdmVGaWxlUHJldmlldyIsCiAgICAg", + "ICAgICAgICAgInRvZ2dsZUZpbGVFZGl0b3JXb3JkV3JhcCIsCiAgICAgICAgICAgICAgIm9wZW5Ccm93c2VyIiwKICAgICAgICAgICAgICAiZm9jdXNCcm93c2VyQWRkcmVzc0JhciIsCiAgICAgICAgICAgICAgImJyb3dzZXJCYWNrIiwKICAgICAgICAgICAgICAiYnJvd3NlckZvcndhcmQiLAogICAgICAgICAgICAgICJicm93c2VyUmVsb2FkIiwKICAgICAgICAgICAgICAiYnJvd3NlckhhcmRSZWxvYWQiLAogICAgICAgICAgICAgICJicm93c2VyWm9vbUluIiwKICAgICAgICAgICAgICAiYnJvd3Nlclpvb21PdXQiLAogICAg", + "ICAgICAgICAgICJicm93c2VyWm9vbVJlc2V0IiwKICAgICAgICAgICAgICAibWFya2Rvd25ab29tSW4iLAogICAgICAgICAgICAgICJtYXJrZG93blpvb21PdXQiLAogICAgICAgICAgICAgICJtYXJrZG93blpvb21SZXNldCIsCiAgICAgICAgICAgICAgImZpbmQiLAogICAgICAgICAgICAgICJmaW5kSW5EaXJlY3RvcnkiLAogICAgICAgICAgICAgICJmaW5kTmV4dCIsCiAgICAgICAgICAgICAgImZpbmRQcmV2aW91cyIsCiAgICAgICAgICAgICAgImhpZGVGaW5kIiwKICAgICAgICAgICAgICAidXNlU2VsZWN0aW9uRm9yRmlu", + "ZCIsCiAgICAgICAgICAgICAgInRvZ2dsZUJyb3dzZXJEZXZlbG9wZXJUb29scyIsCiAgICAgICAgICAgICAgInNob3dCcm93c2VySmF2YVNjcmlwdENvbnNvbGUiLAogICAgICAgICAgICAgICJ0b2dnbGVCcm93c2VyRm9jdXNNb2RlIiwKICAgICAgICAgICAgICAidG9nZ2xlQnJvd3NlckRlc2lnbk1vZGUiLAogICAgICAgICAgICAgICJ0b2dnbGVSZWFjdEdyYWIiLAogICAgICAgICAgICAgICJvcGVuRGlmZlZpZXdlciIsCiAgICAgICAgICAgICAgImRpZmZWaWV3ZXJTY3JvbGxEb3duIiwKICAgICAgICAgICAgICAiZGlmZlZp", + "ZXdlclNjcm9sbFVwIiwKICAgICAgICAgICAgICAiZGlmZlZpZXdlclNjcm9sbEhhbGZQYWdlRG93biIsCiAgICAgICAgICAgICAgImRpZmZWaWV3ZXJTY3JvbGxIYWxmUGFnZVVwIiwKICAgICAgICAgICAgICAiZGlmZlZpZXdlclNjcm9sbERvd25FbWFjcyIsCiAgICAgICAgICAgICAgImRpZmZWaWV3ZXJTY3JvbGxVcEVtYWNzIiwKICAgICAgICAgICAgICAiZGlmZlZpZXdlclNjcm9sbFRvQm90dG9tIiwKICAgICAgICAgICAgICAiZGlmZlZpZXdlclNjcm9sbFRvVG9wIiwKICAgICAgICAgICAgICAiZGlmZlZpZXdlck9wZW5G", + "aWxlU2VhcmNoIiwKICAgICAgICAgICAgICAiZGlmZlZpZXdlck5leHRGaWxlIiwKICAgICAgICAgICAgICAiZGlmZlZpZXdlclByZXZpb3VzRmlsZSIKICAgICAgICAgICAgXQogICAgICAgICAgfSwKICAgICAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICAgICAiZmlsZUV4cGxvcmVyT3BlblNlbGVjdGlvbiI6IHsKICAgICAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL3NpbmdsZUJhcmVGaXJzdFN0cm9rZVNob3J0Y3V0QmluZGluZ051bGxhYmxlIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiZmlsZUV4cGxvcmVy", + "T3BlblNlbGVjdGlvbkZpbmRlckFsaWFzIjogewogICAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvc2luZ2xlQmFyZUZpcnN0U3Ryb2tlU2hvcnRjdXRCaW5kaW5nTnVsbGFibGUiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJkaWZmVmlld2VyU2Nyb2xsRG93biI6IHsKICAgICAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL2JhcmVGaXJzdFN0cm9rZVNob3J0Y3V0QmluZGluZ051bGxhYmxlIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiZGlmZlZpZXdlclNjcm9sbFVwIjogewogICAgICAgICAgICAgICIkcmVm", + "IjogIiMvJGRlZnMvYmFyZUZpcnN0U3Ryb2tlU2hvcnRjdXRCaW5kaW5nTnVsbGFibGUiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJkaWZmVmlld2VyU2Nyb2xsSGFsZlBhZ2VEb3duIjogewogICAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvYmFyZUZpcnN0U3Ryb2tlU2hvcnRjdXRCaW5kaW5nTnVsbGFibGUiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJkaWZmVmlld2VyU2Nyb2xsSGFsZlBhZ2VVcCI6IHsKICAgICAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL2JhcmVGaXJzdFN0cm9rZVNob3J0Y3V0Qmlu", + "ZGluZ051bGxhYmxlIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiZGlmZlZpZXdlclNjcm9sbERvd25FbWFjcyI6IHsKICAgICAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL2JhcmVGaXJzdFN0cm9rZVNob3J0Y3V0QmluZGluZ051bGxhYmxlIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiZGlmZlZpZXdlclNjcm9sbFVwRW1hY3MiOiB7CiAgICAgICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9iYXJlRmlyc3RTdHJva2VTaG9ydGN1dEJpbmRpbmdOdWxsYWJsZSIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgImRp", + "ZmZWaWV3ZXJTY3JvbGxUb0JvdHRvbSI6IHsKICAgICAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL2JhcmVGaXJzdFN0cm9rZVNob3J0Y3V0QmluZGluZ051bGxhYmxlIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiZGlmZlZpZXdlclNjcm9sbFRvVG9wIjogewogICAgICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvYmFyZUZpcnN0U3Ryb2tlU2hvcnRjdXRCaW5kaW5nTnVsbGFibGUiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgICJkaWZmVmlld2VyT3BlbkZpbGVTZWFyY2giOiB7CiAgICAgICAgICAgICAgIiRyZWYi", + "OiAiIy8kZGVmcy9iYXJlRmlyc3RTdHJva2VTaG9ydGN1dEJpbmRpbmdOdWxsYWJsZSIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgImRpZmZWaWV3ZXJOZXh0RmlsZSI6IHsKICAgICAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL2JhcmVGaXJzdFN0cm9rZVNob3J0Y3V0QmluZGluZ051bGxhYmxlIgogICAgICAgICAgICB9LAogICAgICAgICAgICAiZGlmZlZpZXdlclByZXZpb3VzRmlsZSI6IHsKICAgICAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL2JhcmVGaXJzdFN0cm9rZVNob3J0Y3V0QmluZGluZ051bGxhYmxlIgog", + "ICAgICAgICAgICB9CiAgICAgICAgICB9LAogICAgICAgICAgImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjogewogICAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL3Nob3J0Y3V0QmluZGluZ051bGxhYmxlIgogICAgICAgICAgfQogICAgICAgIH0sCiAgICAgICAgIndoZW4iOiB7CiAgICAgICAgICAidHlwZSI6ICJvYmplY3QiLAogICAgICAgICAgImRlZmF1bHQiOiB7fSwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJPcHRpb25hbCBwZXItYWN0aW9uIGNvbnRleHQgcHJlZGljYXRlcyAoVlMgQ29kZS1zdHlsZSBgd2hlbmAgY2xh", + "dXNlcyksIGtleWVkIGJ5IGNtdXggYWN0aW9uIGlkLiBFYWNoIHZhbHVlIGlzIGEgYm9vbGVhbiBleHByZXNzaW9uIG92ZXIgY29udGV4dCBrZXlzIGNvbWJpbmVkIHdpdGggISwgJiYsIHx8LCBhbmQgcGFyZW50aGVzZXMuIEJvb2xlYW4ga2V5czogc2lkZWJhckZvY3VzLCBicm93c2VyRm9jdXMsIG1hcmtkb3duRm9jdXMsIGZpbGVQcmV2aWV3VGV4dEVkaXRvckZvY3VzLCBzaW11bGF0b3JGb2N1cywgdGVybWluYWxGb2N1cywgY29tbWFuZFBhbGV0dGVWaXNpYmxlLCB0ZXJtaW5hbEZpbmRWaXNpYmxlLCB3b3Jrc3BhY2VDYW52", + "YXNMYXlvdXQuIFR5cGVkIGtleXMgc3VwcG9ydCBjb21wYXJpc29uczogdGhlIHN0cmluZyBzaWRlYmFyTW9kZSAoZmlsZXMsIGZpbmQsIHNlc3Npb25zLCBmZWVkLCBvciBkb2NrKSBhbmQgdGhlIGludGVnZXJzIHBhbmVDb3VudCBhbmQgd29ya3NwYWNlQ291bnQuIENvbXBhcmlzb24gb3BlcmF0b3JzIGFyZSA9PSwgIT0sID1+IChyZWdleCksIDwsIDw9LCA+LCA+PSwgYW5kIGBpbiBbYSwgYl1gOyBhbiB1bmtub3duIG9yIGFic2VudCBrZXkgcmVhZHMgYXMgZmFsc2UuIFRoZSBib29sZWFuIGxpdGVyYWxzIHRydWUgYW5kIGZh", + "bHNlIGFyZSBhbHNvIGFjY2VwdGVkOyBga2V5ID09IGZhbHNlYCBpcyB0aGUgc2FtZSBhcyBgIWtleWAuIFRoZSBhY3Rpb24ncyBzaG9ydGN1dCBvbmx5IGZpcmVzIChhbmQgb25seSBjb25mbGljdHMgd2l0aCBvdGhlciBzaG9ydGN1dHMpIHdoZW4gdGhlIGNsYXVzZSBob2xkcy4gRXhhbXBsZXM6IHsgXCJzZWxlY3RXb3Jrc3BhY2VCeU51bWJlclwiOiBcIiFzaWRlYmFyRm9jdXNcIiB9IHNlbGVjdHMgd29ya3NwYWNlcyB3aXRoIEN0cmwrMeKAkzkgZXZlcnl3aGVyZSBleGNlcHQgd2hlbiB0aGUgcmlnaHQgc2lkZWJhciBpcyBm", + "b2N1c2VkOyB7IFwic2VsZWN0U3VyZmFjZUJ5TnVtYmVyXCI6IFwic2lkZWJhck1vZGUgPT0gJ2ZpbmQnICYmIHBhbmVDb3VudCA+IDFcIiB9IHNjb3BlcyBhIGJpbmRpbmcgdG8gdGhlIEZpbmQgc2lkZWJhciB3aGVuIHRoZSB3b3Jrc3BhY2UgaGFzIG11bHRpcGxlIHBhbmVzLiIsCiAgICAgICAgICAiZGVzY3JpcHRpb25LZXkiOiAic2NoZW1hRGVzY3JpcHRpb25zLnNob3J0Y3V0cy53aGVuIiwKICAgICAgICAgICJhZGRpdGlvbmFsUHJvcGVydGllcyI6IHsKICAgICAgICAgICAgInR5cGUiOiAic3RyaW5nIgogICAgICAgICAg", + "fQogICAgICAgIH0KICAgICAgfQogICAgfQogIH0sCiAgIiRkZWZzIjogewogICAgImNvbG9ySGV4IjogewogICAgICAidHlwZSI6ICJzdHJpbmciLAogICAgICAicGF0dGVybiI6ICJeI1swLTlBLUZhLWZdezZ9JCIsCiAgICAgICJkZXNjcmlwdGlvbiI6ICJVcHBlcmNhc2Ugb3IgbG93ZXJjYXNlIDYtZGlnaXQgaGV4IGNvbG9yLiIKICAgIH0sCiAgICAiY29sb3JIZXhPck51bGwiOiB7CiAgICAgICJvbmVPZiI6IFsKICAgICAgICB7CiAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL2NvbG9ySGV4IgogICAgICAgIH0sCiAgICAg", + "ICAgewogICAgICAgICAgInR5cGUiOiAibnVsbCIKICAgICAgICB9CiAgICAgIF0KICAgIH0sCiAgICAibm90aWZpY2F0aW9uU291bmRPdmVycmlkZSI6IHsKICAgICAgIm9uZU9mIjogWwogICAgICAgIHsKICAgICAgICAgICJ0eXBlIjogIm9iamVjdCIsCiAgICAgICAgICAiYWRkaXRpb25hbFByb3BlcnRpZXMiOiBmYWxzZSwKICAgICAgICAgICJyZXF1aXJlZCI6IFsic291bmQiXSwKICAgICAgICAgICJwcm9wZXJ0aWVzIjogewogICAgICAgICAgICAic291bmQiOiB7CiAgICAgICAgICAgICAgInR5cGUiOiAic3RyaW5nIiwK", + "ICAgICAgICAgICAgICAiZW51bSI6IFsiZGVmYXVsdCIsICJCYXNzbyIsICJCbG93IiwgIkJvdHRsZSIsICJGcm9nIiwgIkZ1bmsiLCAiR2xhc3MiLCAiSGVybyIsICJNb3JzZSIsICJQaW5nIiwgIlBvcCIsICJQdXJyIiwgIlNvc3VtaSIsICJTdWJtYXJpbmUiLCAiVGluayIsICJub25lIl0KICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgInR5cGUiOiAib2JqZWN0IiwKICAgICAgICAgICJhZGRpdGlvbmFsUHJvcGVydGllcyI6IGZhbHNlLAogICAgICAgICAgInJlcXVpcmVk", + "IjogWyJzb3VuZCIsICJjdXN0b21Tb3VuZEZpbGVQYXRoIl0sCiAgICAgICAgICAicHJvcGVydGllcyI6IHsKICAgICAgICAgICAgInNvdW5kIjogeyAiY29uc3QiOiAiY3VzdG9tX2ZpbGUiIH0sCiAgICAgICAgICAgICJjdXN0b21Tb3VuZEZpbGVQYXRoIjogewogICAgICAgICAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICAgICAgICAgIm1pbkxlbmd0aCI6IDEsCiAgICAgICAgICAgICAgInBhdHRlcm4iOiAiLipcXFMuKiIKICAgICAgICAgICAgfQogICAgICAgICAgfQogICAgICAgIH0KICAgICAgXSwKICAgICAgImRl", + "c2NyaXB0aW9uS2V5IjogInNjaGVtYURlc2NyaXB0aW9ucy5ub3RpZmljYXRpb25zLnNvdW5kT3ZlcnJpZGUiLAogICAgICAiZGVzY3JpcHRpb24iOiAiQSBub3RpZmljYXRpb24gc291bmQgb3ZlcnJpZGUuIGBkZWZhdWx0YCB1c2VzIG5vdGlmaWNhdGlvbnMuc291bmQsIGBub25lYCBkaXNhYmxlcyBwbGF5YmFjaywgYW5kIHRoZSByZW1haW5pbmcgdmFsdWVzIHNlbGVjdCBhIGJ1aWx0LWluIG9yIGN1c3RvbSBzb3VuZC4iCiAgICB9LAogICAgInNob3J0Y3V0QmluZGluZyI6IHsKICAgICAgIm9uZU9mIjogWwogICAgICAgIHsK", + "ICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvdW5ib3VuZFNob3J0Y3V0QmluZGluZyIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiVW5iaW5kIHRoaXMgc2hvcnRjdXQuIEFjY2VwdGVkIHZhbHVlcyBhcmUgYW4gZW1wdHkgc3RyaW5nLCBub25lLCBjbGVhciwgdW5ib3VuZCwgb3IgZGlzYWJsZWQuIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9zaG9ydGN1dEZpcnN0U3Ryb2tlIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJTaW5nbGUtc3Ryb2tlIHNob3J0Y3V0LCBmb3IgZXhh", + "bXBsZSBjbWQrbiBvciBjbWQrc3BhY2UuIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgInR5cGUiOiAiYXJyYXkiLAogICAgICAgICAgIm1pbkl0ZW1zIjogMSwKICAgICAgICAgICJtYXhJdGVtcyI6IDIsCiAgICAgICAgICAicHJlZml4SXRlbXMiOiBbCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL3Nob3J0Y3V0Rmlyc3RTdHJva2UiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL3Nob3J0Y3V0U3Ryb2tlIgogICAgICAg", + "ICAgICB9CiAgICAgICAgICBdLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkNob3JkZWQgc2hvcnRjdXQuIEV4YW1wbGU6IFtcImN0cmwrYlwiLCBcImNcIl0uIgogICAgICAgIH0KICAgICAgXQogICAgfSwKICAgICJzaG9ydGN1dEJpbmRpbmdOdWxsYWJsZSI6IHsKICAgICAgIm9uZU9mIjogWwogICAgICAgIHsKICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvc2hvcnRjdXRCaW5kaW5nIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgInR5cGUiOiAibnVsbCIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiVW5i", + "aW5kIHRoaXMgc2hvcnRjdXQuIgogICAgICAgIH0KICAgICAgXQogICAgfSwKICAgICJzaW5nbGVCYXJlRmlyc3RTdHJva2VTaG9ydGN1dEJpbmRpbmciOiB7CiAgICAgICJvbmVPZiI6IFsKICAgICAgICB7CiAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL3VuYm91bmRTaG9ydGN1dEJpbmRpbmciLAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIlVuYmluZCB0aGlzIHNob3J0Y3V0LiBBY2NlcHRlZCB2YWx1ZXMgYXJlIGFuIGVtcHR5IHN0cmluZywgbm9uZSwgY2xlYXIsIHVuYm91bmQsIG9yIGRpc2FibGVkLiIKICAgICAgICB9", + "LAogICAgICAgIHsKICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvYmFyZUZpcnN0U3Ryb2tlU2hvcnRjdXRGaXJzdFN0cm9rZSIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiU2luZ2xlLXN0cm9rZSBzaG9ydGN1dC4gQmFyZSBmaXJzdCBzdHJva2VzIGFyZSBhY2NlcHRlZCBmb3IgdGhpcyBhY3Rpb24uIgogICAgICAgIH0KICAgICAgXQogICAgfSwKICAgICJzaW5nbGVCYXJlRmlyc3RTdHJva2VTaG9ydGN1dEJpbmRpbmdOdWxsYWJsZSI6IHsKICAgICAgIm9uZU9mIjogWwogICAgICAgIHsKICAgICAgICAgICIkcmVmIjog", + "IiMvJGRlZnMvc2luZ2xlQmFyZUZpcnN0U3Ryb2tlU2hvcnRjdXRCaW5kaW5nIgogICAgICAgIH0sCiAgICAgICAgewogICAgICAgICAgInR5cGUiOiAibnVsbCIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiVW5iaW5kIHRoaXMgc2hvcnRjdXQuIgogICAgICAgIH0KICAgICAgXQogICAgfSwKICAgICJiYXJlRmlyc3RTdHJva2VTaG9ydGN1dEJpbmRpbmciOiB7CiAgICAgICJvbmVPZiI6IFsKICAgICAgICB7CiAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL3VuYm91bmRTaG9ydGN1dEJpbmRpbmciLAogICAgICAgICAgImRl", + "c2NyaXB0aW9uIjogIlVuYmluZCB0aGlzIHNob3J0Y3V0LiBBY2NlcHRlZCB2YWx1ZXMgYXJlIGFuIGVtcHR5IHN0cmluZywgbm9uZSwgY2xlYXIsIHVuYm91bmQsIG9yIGRpc2FibGVkLiIKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICIkcmVmIjogIiMvJGRlZnMvYmFyZUZpcnN0U3Ryb2tlU2hvcnRjdXRGaXJzdFN0cm9rZSIsCiAgICAgICAgICAiZGVzY3JpcHRpb24iOiAiU2luZ2xlLXN0cm9rZSBzaG9ydGN1dC4gQmFyZSBmaXJzdCBzdHJva2VzIGFyZSBhY2NlcHRlZCBmb3IgdGhpcyBhY3Rpb24uIgogICAgICAg", + "IH0sCiAgICAgICAgewogICAgICAgICAgInR5cGUiOiAiYXJyYXkiLAogICAgICAgICAgIm1pbkl0ZW1zIjogMSwKICAgICAgICAgICJtYXhJdGVtcyI6IDIsCiAgICAgICAgICAicHJlZml4SXRlbXMiOiBbCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL2JhcmVGaXJzdFN0cm9rZVNob3J0Y3V0Rmlyc3RTdHJva2UiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAiJHJlZiI6ICIjLyRkZWZzL3Nob3J0Y3V0U3Ryb2tlIgogICAgICAgICAgICB9CiAgICAgICAgICBd", + "LAogICAgICAgICAgImRlc2NyaXB0aW9uIjogIkNob3JkZWQgc2hvcnRjdXQuIEJhcmUgZmlyc3Qgc3Ryb2tlcyBhcmUgYWNjZXB0ZWQgZm9yIHRoaXMgYWN0aW9uLiBFeGFtcGxlOiBbXCJnXCIsIFwiZ1wiXS4iCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgImJhcmVGaXJzdFN0cm9rZVNob3J0Y3V0QmluZGluZ051bGxhYmxlIjogewogICAgICAib25lT2YiOiBbCiAgICAgICAgewogICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9iYXJlRmlyc3RTdHJva2VTaG9ydGN1dEJpbmRpbmciCiAgICAgICAgfSwKICAgICAgICB7", + "CiAgICAgICAgICAidHlwZSI6ICJudWxsIiwKICAgICAgICAgICJkZXNjcmlwdGlvbiI6ICJVbmJpbmQgdGhpcyBzaG9ydGN1dC4iCiAgICAgICAgfQogICAgICBdCiAgICB9LAogICAgInVuYm91bmRTaG9ydGN1dEJpbmRpbmciOiB7CiAgICAgICJ0eXBlIjogInN0cmluZyIsCiAgICAgICJlbnVtIjogWyIiLCAibm9uZSIsICJjbGVhciIsICJ1bmJvdW5kIiwgImRpc2FibGVkIl0KICAgIH0sCiAgICAic2hvcnRjdXRGaXJzdFN0cm9rZSI6IHsKICAgICAgImFsbE9mIjogWwogICAgICAgIHsKICAgICAgICAgICIkcmVmIjogIiMv", + "JGRlZnMvc2hvcnRjdXRTdHJva2UiCiAgICAgICAgfSwKICAgICAgICB7CiAgICAgICAgICAiYW55T2YiOiBbCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAicGF0dGVybiI6ICJeKD86KD86W2NDXVttTV1bZERdfFtjQ11bb09dW21NXVttTV1bYUFdW25OXVtkRF18W3NTXVtoSF1baUldW2ZGXVt0VF18W29PXVtwUF1bdFRdfFtvT11bcFBdW3RUXVtpSV1bb09dW25OXXxbYUFdW2xMXVt0VF18W2NDXVt0VF1bclJdW2xMXXxbY0NdW29PXVtuTl1bdFRdW3JSXVtvT11bbExdfFtjQ11bdFRdW2xMXXzijJh84oenfOKMpXzijIMp", + "XFwrKSsiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAicGF0dGVybiI6ICJeKD86IHxbc1NdW3BQXVthQV1bY0NdW2VFXXxbc1NdW3BQXVthQV1bY0NdW2VFXVtiQl1bYUFdW3JSXXw8W3NTXVtwUF1bYUFdW2NDXVtlRV0+KSQiCiAgICAgICAgICAgIH0KICAgICAgICAgIF0KICAgICAgICB9CiAgICAgIF0sCiAgICAgICJkZXNjcmlwdGlvbiI6ICJGaXJzdCBvciBvbmx5IHNob3J0Y3V0IHN0cm9rZS4gTXVzdCBpbmNsdWRlIGEgbW9kaWZpZXIgdW5sZXNzIHRoZSBrZXkgaXMgU3BhY2UuIgogICAg", + "fSwKICAgICJiYXJlRmlyc3RTdHJva2VTaG9ydGN1dEZpcnN0U3Ryb2tlIjogewogICAgICAiYWxsT2YiOiBbCiAgICAgICAgewogICAgICAgICAgIiRyZWYiOiAiIy8kZGVmcy9zaG9ydGN1dFN0cm9rZSIKICAgICAgICB9LAogICAgICAgIHsKICAgICAgICAgICJhbnlPZiI6IFsKICAgICAgICAgICAgewogICAgICAgICAgICAgICJwYXR0ZXJuIjogIl4oPzooPzpbY0NdW21NXVtkRF18W2NDXVtvT11bbU1dW21NXVthQV1bbk5dW2REXXxbc1NdW2hIXVtpSV1bZkZdW3RUXXxbb09dW3BQXVt0VF18W29PXVtwUF1bdFRdW2lJXVtv", + "T11bbk5dfFthQV1bbExdW3RUXXxbY0NdW3RUXVtyUl1bbExdfFtjQ11bb09dW25OXVt0VF1bclJdW29PXVtsTF18W2NDXVt0VF1bbExdfOKMmHzih6d84oylfOKMgylcXCspKyIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgewogICAgICAgICAgICAgICJwYXR0ZXJuIjogIl4oPzogfFtzU11bcFBdW2FBXVtjQ11bZUVdfFtzU11bcFBdW2FBXVtjQ11bZUVdW2JCXVthQV1bclJdfDxbc1NdW3BQXVthQV1bY0NdW2VFXT4pJCIKICAgICAgICAgICAgfSwKICAgICAgICAgICAgewogICAgICAgICAgICAgICJwYXR0ZXJuIjogIl4o", + "PzpbclJdW2VFXVt0VF1bdVVdW3JSXVtuTl18W2VFXVtuTl1bdFRdW2VFXVtyUl184oapKSQiCiAgICAgICAgICAgIH0sCiAgICAgICAgICAgIHsKICAgICAgICAgICAgICAicGF0dGVybiI6ICJeKD86W2xMXVtlRV1bZkZdW3RUXXxbYUFdW3JSXVtyUl1bb09dW3dXXVtsTF1bZUVdW2ZGXVt0VF18W2xMXVtlRV1bZkZdW3RUXVthQV1bclJdW3JSXVtvT11bd1ddfOKGkHxbclJdW2lJXVtnR11baEhdW3RUXXxbYUFdW3JSXVtyUl1bb09dW3dXXVtyUl1baUldW2dHXVtoSF1bdFRdfFtyUl1baUldW2dHXVtoSF1bdFRdW2FBXVtyUl1b", + "clJdW29PXVt3V1184oaSfFt1VV1bcFBdfFthQV1bclJdW3JSXVtvT11bd1ddW3VVXVtwUF18W3VVXVtwUF1bYUFdW3JSXVtyUl1bb09dW3dXXXzihpF8W2REXVtvT11bd1ddW25OXXxbYUFdW3JSXVtyUl1bb09dW3dXXVtkRF1bb09dW3dXXVtuTl18W2REXVtvT11bd1ddW25OXVthQV1bclJdW3JSXVtvT11bd1ddfOKGkykkIgogICAgICAgICAgICB9LAogICAgICAgICAgICB7CiAgICAgICAgICAgICAgInBhdHRlcm4iOiAiXig/OltBLVphLXowLTldfFssLi9cXFxcOydgPVxcW1xcXS1dfFtjQ11bb09dW21NXVttTV1bYUFdfFtw", + "UF1bZUVdW3JSXVtpSV1bb09dW2REXXxbZERdW29PXVt0VF18W3NTXVtsTF1bYUFdW3NTXVtoSF18W2JCXVthQV1bY0NdW2tLXVtzU11bbExdW2FBXVtzU11baEhdfFtzU11bZUVdW21NXVtpSV1bY0NdW29PXVtsTF1bb09dW25OXXxbcVFdW3VVXVtvT11bdFRdW2VFXXxbYUFdW3BQXVtvT11bc1NdW3RUXVtyUl1bb09dW3BQXVtoSF1bZUVdfFtiQl1bYUFdW2NDXVtrS11bdFRdW2lJXVtjQ11ba0tdfFtnR11bclJdW2FBXVt2Vl1bZUVdfFttTV1baUldW25OXVt1VV1bc1NdfFtoSF1beVldW3BQXVtoSF1bZUVdW25OXXxbcFBdW2xM", + "XVt1VV1bc1NdfFtlRV1bcVFdW3VVXVthQV1bbExdW3NTXXxbbExdW2VFXVtmRl1bdFRdW2JCXVtyUl1bYUFdW2NDXVtrS11bZUVdW3RUXXxbb09dW3BQXVtlRV1bbk5dW2JCXVtyUl1bYUFdW2NDXVtrS11bZUVdW3RUXXxbclJdW2lJXVtnR11baEhdW3RUXVtiQl1bclJdW2FBXVtjQ11ba0tdW2VFXVt0VF18W2NDXVtsTF1bb09dW3NTXVtlRV1bYkJdW3JSXVthQV1bY0NdW2tLXVtlRV1bdFRdKSQiCiAgICAgICAgICAgIH0KICAgICAgICAgIF0KICAgICAgICB9CiAgICAgIF0sCiAgICAgICJkZXNjcmlwdGlvbiI6ICJGaXJzdCBv", + "ciBvbmx5IHNob3J0Y3V0IHN0cm9rZS4gQmFyZSBwcmludGFibGUga2V5cyBhcmUgYWNjZXB0ZWQgZm9yIGFjdGlvbnMgdGhhdCBvcHQgaW50byBiYXJlIGZpcnN0IHN0cm9rZXMuIgogICAgfSwKICAgICJzaG9ydGN1dFN0cm9rZSI6IHsKICAgICAgInR5cGUiOiAic3RyaW5nIiwKICAgICAgInBhdHRlcm4iOiAiXig/Oig/OltjQ11bbU1dW2REXXxbY0NdW29PXVttTV1bbU1dW2FBXVtuTl1bZERdfFtzU11baEhdW2lJXVtmRl1bdFRdfFtvT11bcFBdW3RUXXxbb09dW3BQXVt0VF1baUldW29PXVtuTl18W2FBXVtsTF1bdFRdfFtj", + "Q11bdFRdW3JSXVtsTF18W2NDXVtvT11bbk5dW3RUXVtyUl1bb09dW2xMXXxbY0NdW3RUXVtsTF184oyYfOKHp3zijKV84oyDKVxcKykqKD86IHxbQS1aYS16MC05XXxbbExdW2VFXVtmRl1bdFRdfFthQV1bclJdW3JSXVtvT11bd1ddW2xMXVtlRV1bZkZdW3RUXXxbbExdW2VFXVtmRl1bdFRdW2FBXVtyUl1bclJdW29PXVt3V1184oaQfFtyUl1baUldW2dHXVtoSF1bdFRdfFthQV1bclJdW3JSXVtvT11bd1ddW3JSXVtpSV1bZ0ddW2hIXVt0VF18W3JSXVtpSV1bZ0ddW2hIXVt0VF1bYUFdW3JSXVtyUl1bb09dW3dXXXzihpJ8W3VV", + "XVtwUF18W2FBXVtyUl1bclJdW29PXVt3V11bdVVdW3BQXXxbdVVdW3BQXVthQV1bclJdW3JSXVtvT11bd1ddfOKGkXxbZERdW29PXVt3V11bbk5dfFthQV1bclJdW3JSXVtvT11bd1ddW2REXVtvT11bd1ddW25OXXxbZERdW29PXVt3V11bbk5dW2FBXVtyUl1bclJdW29PXVt3V1184oaTfFt0VF1bYUFdW2JCXXxbclJdW2VFXVt0VF1bdVVdW3JSXVtuTl18W2VFXVtuTl1bdFRdW2VFXVtyUl184oapfFtzU11bcFBdW2FBXVtjQ11bZUVdfFtzU11bcFBdW2FBXVtjQ11bZUVdW2JCXVthQV1bclJdfDxbc1NdW3BQXVthQV1bY0NdW2VF", + "XT58WywuL1xcXFw7J2A9XFxbXFxdLV18W2NDXVtvT11bbU1dW21NXVthQV18W3BQXVtlRV1bclJdW2lJXVtvT11bZERdfFtkRF1bb09dW3RUXXxbc1NdW2xMXVthQV1bc1NdW2hIXXxbYkJdW2FBXVtjQ11ba0tdW3NTXVtsTF1bYUFdW3NTXVtoSF18W3NTXVtlRV1bbU1dW2lJXVtjQ11bb09dW2xMXVtvT11bbk5dfFtxUV1bdVVdW29PXVt0VF1bZUVdfFthQV1bcFBdW29PXVtzU11bdFRdW3JSXVtvT11bcFBdW2hIXVtlRV18W2JCXVthQV1bY0NdW2tLXVt0VF1baUldW2NDXVtrS118W2dHXVtyUl1bYUFdW3ZWXVtlRV18W21NXVtp", + "SV1bbk5dW3VVXVtzU118W2hIXVt5WV1bcFBdW2hIXVtlRV1bbk5dfFtwUF1bbExdW3VVXVtzU118W2VFXVtxUV1bdVVdW2FBXVtsTF1bc1NdfFtsTF1bZUVdW2ZGXVt0VF1bYkJdW3JSXVthQV1bY0NdW2tLXVtlRV1bdFRdfFtvT11bcFBdW2VFXVtuTl1bYkJdW3JSXVthQV1bY0NdW2tLXVtlRV1bdFRdfFtyUl1baUldW2dHXVtoSF1bdFRdW2JCXVtyUl1bYUFdW2NDXVtrS11bZUVdW3RUXXxbY0NdW2xMXVtvT11bc1NdW2VFXVtiQl1bclJdW2FBXVtjQ11ba0tdW2VFXVt0VF18W2ZGXSg/OlsxLTldfDFbMC05XXwyMCl8W3ZWXVtv", + "T11bbExdW3VVXVttTV1bZUVdW3VVXVtwUF18W21NXVtlRV1bZERdW2lJXVthQV1bdlZdW29PXVtsTF1bdVVdW21NXVtlRV1bdVVdW3BQXXxbbU1dW2VFXVtkRF1baUldW2FBXVxcLlt2Vl1bb09dW2xMXVt1VV1bbU1dW2VFXVt1VV1bcFBdfFt2Vl1bb09dW2xMXVt1VV1bbU1dW2VFXVtkRF1bb09dW3dXXVtuTl18W21NXVtlRV1bZERdW2lJXVthQV1bdlZdW29PXVtsTF1bdVVdW21NXVtlRV1bZERdW29PXVt3V11bbk5dfFttTV1bZUVdW2REXVtpSV1bYUFdXFwuW3ZWXVtvT11bbExdW3VVXVttTV1bZUVdW2REXVtvT11bd1ddW25O", + "XXxbYkJdW3JSXVtpSV1bZ0ddW2hIXVt0VF1bbk5dW2VFXVtzU11bc1NdW3VVXVtwUF18W21NXVtlRV1bZERdW2lJXVthQV1bYkJdW3JSXVtpSV1bZ0ddW2hIXVt0VF1bbk5dW2VFXVtzU11bc1NdW3VVXVtwUF18W21NXVtlRV1bZERdW2lJXVthQV1cXC5bYkJdW3JSXVtpSV1bZ0ddW2hIXVt0VF1bbk5dW2VFXVtzU11bc1NdW3VVXVtwUF18W2JCXVtyUl1baUldW2dHXVtoSF1bdFRdW25OXVtlRV1bc1NdW3NTXVtkRF1bb09dW3dXXVtuTl18W21NXVtlRV1bZERdW2lJXVthQV1bYkJdW3JSXVtpSV1bZ0ddW2hIXVt0VF1bbk5dW2VF", + "XVtzU11bc1NdW2REXVtvT11bd1ddW25OXXxbbU1dW2VFXVtkRF1baUldW2FBXVxcLltiQl1bclJdW2lJXVtnR11baEhdW3RUXVtuTl1bZUVdW3NTXVtzU11bZERdW29PXVt3V11bbk5dfFttTV1bdVVdW3RUXVtlRV18W21NXVtlRV1bZERdW2lJXVthQV1bbU1dW3VVXVt0VF1bZUVdfFttTV1bZUVdW2REXVtpSV1bYUFdXFwuW21NXVt1VV1bdFRdW2VFXXxbcFBdW2xMXVthQV1beVldW3BQXVthQV1bdVVdW3NTXVtlRV18W21NXVtlRV1bZERdW2lJXVthQV1bcFBdW2xMXVthQV1beVldW3BQXVthQV1bdVVdW3NTXVtlRV18W21NXVtl", + "RV1bZERdW2lJXVthQV1cXC5bcFBdW2xMXVthQV1beVldW3BQXVthQV1bdVVdW3NTXVtlRV18W25OXVtlRV1beFhdW3RUXVt0VF1bclJdW2FBXVtjQ11ba0tdfFttTV1bZUVdW2REXVtpSV1bYUFdW25OXVtlRV1beFhdW3RUXXxbbU1dW2VFXVtkRF1baUldW2FBXVxcLltuTl1bZUVdW3hYXVt0VF18W21NXVtlRV1bZERdW2lJXVthQV1cXC5bbk5dW2VFXVt4WF1bdFRdW3RUXVtyUl1bYUFdW2NDXVtrS118W3BQXVtyUl1bZUVdW3ZWXVtpSV1bb09dW3VVXVtzU11bdFRdW3JSXVthQV1bY0NdW2tLXXxbbU1dW2VFXVtkRF1baUldW2FB", + "XVtwUF1bclJdW2VFXVt2Vl1baUldW29PXVt1VV1bc1NdfFttTV1bZUVdW2REXVtpSV1bYUFdXFwuW3BQXVtyUl1bZUVdW3ZWXVtpSV1bb09dW3VVXVtzU118W21NXVtlRV1bZERdW2lJXVthQV1cXC5bcFBdW3JSXVtlRV1bdlZdW2lJXVtvT11bdVVdW3NTXVt0VF1bclJdW2FBXVtjQ11ba0tdKSQiLAogICAgICAiZGVzY3JpcHRpb24iOiAiT25lIGtleWJvYXJkIHNob3J0Y3V0IHN0cm9rZSB1c2luZyBtb2RpZmllcitrZXkgc3ludGF4LiBTdXBwb3J0ZWQga2V5IG5hbWVzIGluY2x1ZGUgc3BhY2UsIFNwYWNlLCA8c3BhY2U+LCBh", + "bmQgPFNwYWNlPi4iCiAgICB9CiAgfQp9Cg==", ].joined() guard let data = Data(base64Encoded: base64) else { preconditionFailure("embedded cmux.json schema is corrupt") diff --git a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/SidebarCatalogSection.swift b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/SidebarCatalogSection.swift index 27b19eb20692..fd74e616ce38 100644 --- a/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/SidebarCatalogSection.swift +++ b/Packages/macOS/CmuxSettings/Sources/CmuxSettings/Keys/SidebarCatalogSection.swift @@ -138,6 +138,16 @@ public struct SidebarCatalogSection: SettingCatalogSection { userDefaultsKey: "sidebarShowProgress" ) + /// Whether sidebar workspace rows append coding-agent usage (model, + /// context-window percentage, estimated API cost) to the agent status + /// entry (`sidebar.showAgentUsage`). Defaults to off: sampling reads the + /// agent transcript off the main actor after hook events. + public let showAgentUsage = DefaultsKey( + id: "sidebar.showAgentUsage", + defaultValue: false, + userDefaultsKey: "sidebarShowAgentUsage" + ) + /// Whether sidebar workspace rows show the loading spinner for running /// coding agents and manual `cmux workspace loading` loaders /// (`sidebar.showAgentActivity`). Defaults to on. diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+AgentUsage.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+AgentUsage.swift new file mode 100644 index 000000000000..71724b3e54af --- /dev/null +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+AgentUsage.swift @@ -0,0 +1,19 @@ +import CmuxSettings +import Foundation + +extension Array where Element == CuratedSettingEntry { + /// Search entries for the opt-in sidebar agent-usage row + /// (`sidebar.showAgentUsage`), appended to ``cmuxDefault(catalog:)``. + static var sidebarAgentUsageEntries: [CuratedSettingEntry] { + [ + .init( + section: .sidebarAppearance, + id: "show-agent-usage", + title: String(localized: "settings.app.showAgentUsage", defaultValue: "Show Agent Usage in Sidebar"), + detailText: String(localized: "settings.app.showAgentUsage.subtitle", defaultValue: "Append the model, context window used, and estimated API cost to Claude Code and Codex status entries. Cost is an estimate from published API prices, not a bill."), + paths: ["sidebar.showAgentUsage"], + synonyms: "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex" + ), + ] + } +} diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift index 55f6f5e465cb..84d81ff496a0 100644 --- a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Navigation/CuratedSettingEntry+Default.swift @@ -519,7 +519,7 @@ extension Array where Element == CuratedSettingEntry { // Reset .init(section: .reset, id: "reset-all", title: String(localized: "settings.reset.resetAll", defaultValue: "Reset All Settings"), synonyms: "Reset All Settings factory reset restore defaults clear preferences"), - ] + ] + sidebarAgentUsageEntries } private static var keyboardShortcutActionSynonyms: String { diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection+AgentActivity.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection+AgentActivity.swift index 92a9acc84f73..f25a55d35b0b 100644 --- a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection+AgentActivity.swift +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection+AgentActivity.swift @@ -4,6 +4,8 @@ import SwiftUI extension SidebarSection { @ViewBuilder var agentActivityRows: some View { + agentUsageRow + SettingsCardRow( configurationReview: .json("sidebar.showAgentActivity"), String(localized: "settings.app.showAgentActivity", defaultValue: "Show Loading Spinner"), diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection+AgentUsage.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection+AgentUsage.swift new file mode 100644 index 000000000000..aa87b1de247b --- /dev/null +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection+AgentUsage.swift @@ -0,0 +1,21 @@ +import CmuxSettings +import SwiftUI + +extension SidebarSection { + /// The opt-in `sidebar.showAgentUsage` toggle row. Like the other detail + /// rows it is disabled while "Hide All Details" is on. + @ViewBuilder + var agentUsageRow: some View { + SettingsCardRow( + configurationReview: .json("sidebar.showAgentUsage"), + String(localized: "settings.app.showAgentUsage", defaultValue: "Show Agent Usage in Sidebar"), + subtitle: String(localized: "settings.app.showAgentUsage.subtitle", defaultValue: "Append the model, context window used, and estimated API cost to Claude Code and Codex status entries. Cost is an estimate from published API prices, not a bill.") + ) { + Toggle("", isOn: Binding(get: { showAgentUsage.current }, set: { showAgentUsage.set($0) })) + .labelsHidden() + .controlSize(.small) + } + .disabled(hideAll.current) + SettingsCardDivider() + } +} diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swift index e67a832776d9..25b7ac73ef35 100644 --- a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swift +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swift @@ -31,6 +31,7 @@ public struct SidebarSection: View { @State private var showLog: DefaultsValueModel @State private var showProgress: DefaultsValueModel @State var showAgentActivity: DefaultsValueModel + @State var showAgentUsage: DefaultsValueModel @State var loadingSpinnerPosition: DefaultsValueModel @State var notificationBadgePosition: DefaultsValueModel @State private var showMetadata: DefaultsValueModel @@ -62,6 +63,7 @@ public struct SidebarSection: View { _showLog = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.sidebar.showLog)) _showProgress = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.sidebar.showProgress)) _showAgentActivity = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.sidebar.showAgentActivity)) + _showAgentUsage = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.sidebar.showAgentUsage)) _loadingSpinnerPosition = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.sidebar.loadingSpinnerPosition)) _notificationBadgePosition = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.sidebar.notificationBadgePosition)) _showMetadata = State(initialValue: DefaultsValueModel(store: defaultsStore, key: catalog.sidebar.showCustomMetadata)) @@ -98,11 +100,8 @@ public struct SidebarSection: View { prClickable, prLinks, portLinks, - showSSH, - showPorts, - showLog, - showProgress, - showAgentActivity, + showSSH, showPorts, showLog, + showProgress, showAgentActivity, showAgentUsage, loadingSpinnerPosition, notificationBadgePosition, showMetadata, diff --git a/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift b/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift index 95b4c485324f..19dbefc5133e 100644 --- a/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift +++ b/Packages/macOS/CmuxSettingsUI/Tests/CmuxSettingsUITests/SettingsRowAnchorResolutionTests.swift @@ -116,6 +116,7 @@ struct SettingsRowAnchorResolutionTests { "sidebar.showNotificationMessage", "sidebar.notificationMessageLineLimit", "sidebar.showAgentActivity", + "sidebar.showAgentUsage", "sidebar.loadingSpinnerPosition", "sidebar.notificationBadgePosition", "sidebar.showPorts", diff --git a/Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Detail/SidebarWorkspaceAuxiliaryDetailVisibility.swift b/Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Detail/SidebarWorkspaceAuxiliaryDetailVisibility.swift index 10ea73b5466d..9d850ab1631d 100644 --- a/Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Detail/SidebarWorkspaceAuxiliaryDetailVisibility.swift +++ b/Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Detail/SidebarWorkspaceAuxiliaryDetailVisibility.swift @@ -2,10 +2,10 @@ public import Foundation /// Resolved visibility of the auxiliary detail rows under a sidebar /// workspace row (metadata pills, log line, progress, branch/directory, -/// pull requests, ports). +/// pull requests, ports, coding-agent usage). /// /// The master "hide all details" switch wins over every individual toggle; -/// ``resolved(showMetadata:showLog:showProgress:showBranchDirectory:showPullRequests:showPorts:hideAllDetails:)`` +/// ``resolved(showMetadata:showLog:showProgress:showAgentUsage:showBranchDirectory:showPullRequests:showPorts:hideAllDetails:)`` /// applies exactly that legacy precedence. public struct SidebarWorkspaceAuxiliaryDetailVisibility: Equatable, Sendable { /// Whether custom metadata pills are shown. @@ -14,6 +14,9 @@ public struct SidebarWorkspaceAuxiliaryDetailVisibility: Equatable, Sendable { public let showsLog: Bool /// Whether agent progress is shown. public let showsProgress: Bool + /// Whether coding-agent usage (model, context %, estimated cost) is + /// appended to the agent status entry. + public let showsAgentUsage: Bool /// Whether the branch/directory line is shown. public let showsBranchDirectory: Bool /// Whether pull-request badges are shown. @@ -34,17 +37,22 @@ public struct SidebarWorkspaceAuxiliaryDetailVisibility: Equatable, Sendable { } /// Creates a visibility value with each row's flag as given. + /// + /// `showsAgentUsage` defaults to `false` because agent usage is an + /// opt-in detail (`sidebar.showAgentUsage`). public init( showsMetadata: Bool, showsLog: Bool, showsProgress: Bool, showsBranchDirectory: Bool, showsPullRequests: Bool, - showsPorts: Bool + showsPorts: Bool, + showsAgentUsage: Bool = false ) { self.showsMetadata = showsMetadata self.showsLog = showsLog self.showsProgress = showsProgress + self.showsAgentUsage = showsAgentUsage self.showsBranchDirectory = showsBranchDirectory self.showsPullRequests = showsPullRequests self.showsPorts = showsPorts @@ -67,6 +75,7 @@ public struct SidebarWorkspaceAuxiliaryDetailVisibility: Equatable, Sendable { showMetadata: Bool, showLog: Bool, showProgress: Bool, + showAgentUsage: Bool = false, showBranchDirectory: Bool, showPullRequests: Bool, showPorts: Bool, @@ -79,7 +88,8 @@ public struct SidebarWorkspaceAuxiliaryDetailVisibility: Equatable, Sendable { showsProgress: showProgress, showsBranchDirectory: showBranchDirectory, showsPullRequests: showPullRequests, - showsPorts: showPorts + showsPorts: showPorts, + showsAgentUsage: showAgentUsage ) } } diff --git a/Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Status/SidebarAgentUsage.swift b/Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Status/SidebarAgentUsage.swift new file mode 100644 index 000000000000..16e9506ce2b9 --- /dev/null +++ b/Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/Status/SidebarAgentUsage.swift @@ -0,0 +1,25 @@ +public import Foundation + +/// Coding-agent usage shown next to an agent's sidebar status entry: the +/// model, how full its context window is, and an estimated API cost. +/// +/// The app target samples this from the agent transcript off the main actor +/// and stores it per status key in ``WorkspaceSidebarMetadataModel``; the +/// row renders it only when `sidebar.showAgentUsage` is on. +public struct SidebarAgentUsage: Equatable, Sendable { + /// Short model name (`Opus 4.8`, `gpt-5-codex`). + public let modelName: String + /// Fraction of the context window in use (`0...1`), or `nil` when the + /// window is unknown. + public let contextFraction: Double? + /// Estimated pay-as-you-go cost in US dollars, or `nil` when the model + /// has no known list price. + public let estimatedCostUSD: Double? + + /// Creates a usage value. + public init(modelName: String, contextFraction: Double?, estimatedCostUSD: Double?) { + self.modelName = modelName + self.contextFraction = contextFraction + self.estimatedCostUSD = estimatedCostUSD + } +} diff --git a/Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/WorkspaceModel/WorkspaceSidebarMetadataModel.swift b/Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/WorkspaceModel/WorkspaceSidebarMetadataModel.swift index 7590ddea0e68..246a5e1929f7 100644 --- a/Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/WorkspaceModel/WorkspaceSidebarMetadataModel.swift +++ b/Packages/macOS/CmuxSidebar/Sources/CmuxSidebar/WorkspaceModel/WorkspaceSidebarMetadataModel.swift @@ -81,6 +81,13 @@ public final class WorkspaceSidebarMetadataModel { didSet { panelDirectoryDisplayLabelsSubject.send(panelDirectoryDisplayLabels) } } + /// Coding-agent usage keyed by the agent's sidebar status key + /// (`claude_code`, `codex`). Rendered next to the matching status entry + /// only while `sidebar.showAgentUsage` is on. + public var agentUsageByStatusKey: [String: SidebarAgentUsage] = [:] { + didSet { agentUsageSubject.send(agentUsageByStatusKey) } + } + @ObservationIgnored private let limitProvider: any SidebarLogEntryLimitProviding @@ -101,6 +108,8 @@ public final class WorkspaceSidebarMetadataModel { @ObservationIgnored private lazy var panelPullRequestsSubject = CurrentValueSubject<[UUID: SidebarPullRequestState], Never>(panelPullRequests) @ObservationIgnored + private lazy var agentUsageSubject = CurrentValueSubject<[String: SidebarAgentUsage], Never>(agentUsageByStatusKey) + @ObservationIgnored private lazy var panelDirectoryDisplayLabelsSubject = CurrentValueSubject<[UUID: String], Never>(panelDirectoryDisplayLabels) /// Creates an empty sidebar-metadata model. @@ -167,6 +176,20 @@ public final class WorkspaceSidebarMetadataModel { panelDirectoryDisplayLabelsSubject.eraseToAnyPublisher() } + /// Emits the current agent usage on subscription, then on every change. + public var agentUsagePublisher: AnyPublisher<[String: SidebarAgentUsage], Never> { + agentUsageSubject.eraseToAnyPublisher() + } + + /// Sets or clears the usage shown next to one agent status entry. + /// - Parameters: + /// - usage: The new usage, or `nil` to clear it. + /// - statusKey: The agent's sidebar status key. + public func updateAgentUsage(_ usage: SidebarAgentUsage?, forStatusKey statusKey: String) { + guard agentUsageByStatusKey[statusKey] != usage else { return } + agentUsageByStatusKey[statusKey] = usage + } + /// Emits a sidebar observation pulse without mutating metadata. /// /// The app target uses this when the rendered workspace-row snapshot changes diff --git a/Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/SidebarDetailVisibilityTests.swift b/Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/SidebarDetailVisibilityTests.swift index bfad89b5116a..155f63c78daa 100644 --- a/Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/SidebarDetailVisibilityTests.swift +++ b/Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/SidebarDetailVisibilityTests.swift @@ -110,5 +110,21 @@ struct SidebarWorkspaceAuxiliaryDetailVisibilityTests { #expect(!visibility.showsBranchDirectory) #expect(visibility.showsPullRequests) #expect(!visibility.showsPorts) + #expect(!visibility.showsAgentUsage) + } + + @Test(arguments: [false, true]) + func agentUsageIsOptInAndHiddenByHideAllDetails(hideAllDetails: Bool) { + let visibility = SidebarWorkspaceAuxiliaryDetailVisibility.resolved( + showMetadata: true, + showLog: true, + showProgress: true, + showAgentUsage: true, + showBranchDirectory: true, + showPullRequests: true, + showPorts: true, + hideAllDetails: hideAllDetails + ) + #expect(visibility.showsAgentUsage == !hideAllDetails) } } diff --git a/Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/WorkspaceSidebarMetadataModelTests.swift b/Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/WorkspaceSidebarMetadataModelTests.swift index fbda7204b8e1..3ec076af82e2 100644 --- a/Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/WorkspaceSidebarMetadataModelTests.swift +++ b/Packages/macOS/CmuxSidebar/Tests/CmuxSidebarTests/WorkspaceSidebarMetadataModelTests.swift @@ -152,4 +152,19 @@ private struct FixedLogLimitProvider: SidebarLogEntryLimitProviding { #expect(emitted.count == 3) #expect(emitted.last == [:]) } + + @Test func agentUsageUpdatesPublishAndIgnoreNoOps() { + let model = makeModel() + var published: [[String: SidebarAgentUsage]] = [] + let cancellable = model.agentUsagePublisher.sink { published.append($0) } + defer { cancellable.cancel() } + let usage = SidebarAgentUsage(modelName: "Opus 4.8", contextFraction: 0.42, estimatedCostUSD: 1.2) + + model.updateAgentUsage(usage, forStatusKey: "claude_code") + model.updateAgentUsage(usage, forStatusKey: "claude_code") + model.updateAgentUsage(nil, forStatusKey: "claude_code") + + #expect(published == [[:], ["claude_code": usage], [:]]) + #expect(model.agentUsageByStatusKey.isEmpty) + } } diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 1caad6287a06..5762b88cb447 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -303955,6 +303955,242 @@ } } }, + "settings.app.showAgentUsage": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "عرض استخدام الوكيل في الشريط الجانبي" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Agent-Nutzung in der Seitenleiste anzeigen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Show Agent Usage in Sidebar" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar el uso del agente en la barra lateral" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher l’utilisation des agents dans la barre latérale" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "サイドバーにエージェントの使用状況を表示" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "사이드바에 에이전트 사용량 표시" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "在侧边栏显示代理用量" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "在側邊欄顯示代理用量" + } + } + } + }, + "settings.app.showAgentUsage.subtitle": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "يُلحق النموذج ونسبة نافذة السياق المستخدمة والتكلفة التقديرية لواجهة API بإدخالات حالة Claude Code وCodex. التكلفة تقدير مبني على أسعار API المنشورة، وليست فاتورة." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hängt Modell, belegtes Kontextfenster und geschätzte API-Kosten an die Statuseinträge von Claude Code und Codex an. Die Kosten sind eine Schätzung auf Basis veröffentlichter API-Preise, keine Rechnung." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Append the model, context window used, and estimated API cost to Claude Code and Codex status entries. Cost is an estimate from published API prices, not a bill." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añade el modelo, la ventana de contexto usada y el coste estimado de la API a las entradas de estado de Claude Code y Codex. El coste es una estimación según los precios publicados de la API, no una factura." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajoute le modèle, la part de la fenêtre de contexte utilisée et le coût API estimé aux entrées d’état de Claude Code et Codex. Le coût est une estimation d’après les tarifs API publiés, pas une facture." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Claude Code と Codex のステータス項目に、モデル、使用中のコンテキストウィンドウ、推定 API コストを追加します。コストは公開 API 価格に基づく推定値で、請求額ではありません。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "Claude Code 및 Codex 상태 항목에 모델, 사용 중인 컨텍스트 창, 예상 API 비용을 덧붙입니다. 비용은 공개된 API 가격 기준의 추정치이며 청구 금액이 아닙니다." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "在 Claude Code 和 Codex 的状态条目后附加模型、已用的上下文窗口和估算的 API 费用。费用是根据公开的 API 价格估算的,并非账单。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "在 Claude Code 與 Codex 的狀態項目後附加模型、已用的上下文視窗與估算的 API 費用。費用是依公開 API 價格估算,並非帳單。" + } + } + } + }, + "settings.search.alias.setting.app.show-agent-usage": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex وكيل استخدام نموذج نافذة السياق رموز نسبة تكلفة سعر تقدير الشريط الجانبي" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex Agent Nutzung Modell Kontextfenster Token Prozent Kosten Preis Schätzung Seitenleiste" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex agente uso modelo ventana de contexto tokens porcentaje coste precio estimación barra lateral" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex agent utilisation modèle fenêtre de contexte jetons pourcentage coût prix estimation barre latérale" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex エージェント 使用状況 モデル コンテキストウィンドウ トークン パーセント コスト 料金 推定 サイドバー" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex 에이전트 사용량 모델 컨텍스트 창 토큰 퍼센트 비용 가격 추정 사이드바" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex 代理 用量 模型 上下文窗口 令牌 百分比 费用 价格 估算 侧边栏" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex 代理 用量 模型 上下文視窗 權杖 百分比 費用 價格 估算 側邊欄" + } + } + } + }, + "sidebar.agentUsage.estimatedCost": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "حوالي %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "ca. %@" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "~%@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "aprox. %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "env. %@" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "約%@" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "약 %@" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "约 %@" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "約 %@" + } + } + } + }, "settings.app.showProgress.subtitle": { "extractionState": "manual", "localizations": { diff --git a/Sources/AppDelegate.swift b/Sources/AppDelegate.swift index 8eb0cf74b28c..4e9f0d6a09bd 100644 --- a/Sources/AppDelegate.swift +++ b/Sources/AppDelegate.swift @@ -2593,6 +2593,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent ensureMobileWorkspaceListObserver(for: tabManager) MobileTerminalRenderObserver.shared.start() agentChatTranscriptService.start() + SidebarAgentUsageCoordinator { [weak self] id in self?.tabManagerFor(tabId: id)?.workspacesById[id]?.sidebarMetadata }.start() installMobileHostSettingsObserver() installManagedPolicyEnforcement() scheduleGhosttyCrashBreadcrumbIfNeeded(notificationStore: notificationStore) @@ -3550,7 +3551,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCent uiTestDiagnosticsWriter.write(stage: "feedSidebarUITest.portalStats.setup") } - #endif private func captureSessionLaunchStateIfNeeded( diff --git a/Sources/CmuxSettingsFileStore+SupportedPaths.swift b/Sources/CmuxSettingsFileStore+SupportedPaths.swift index 9969c49e2218..b7165e5a40f7 100644 --- a/Sources/CmuxSettingsFileStore+SupportedPaths.swift +++ b/Sources/CmuxSettingsFileStore+SupportedPaths.swift @@ -96,6 +96,7 @@ extension CmuxSettingsFileStore { "sidebar.showPorts", "sidebar.showLog", "sidebar.showProgress", + "sidebar.showAgentUsage", "sidebar.showAgentActivity", "sidebar.loadingSpinnerPosition", "sidebar.notificationBadgePosition", diff --git a/Sources/CmuxSettingsJSONPathSupport.swift b/Sources/CmuxSettingsJSONPathSupport.swift index a014d3751c2e..72e1bc8cc5fb 100644 --- a/Sources/CmuxSettingsJSONPathSupport.swift +++ b/Sources/CmuxSettingsJSONPathSupport.swift @@ -37,6 +37,7 @@ enum SidebarWorkspaceDetailDefaults { static let showPortsKey = sidebar.showPorts.userDefaultsKey static let showLogKey = sidebar.showLog.userDefaultsKey static let showProgressKey = sidebar.showProgress.userDefaultsKey + static let showAgentUsageKey = sidebar.showAgentUsage.userDefaultsKey static let showAgentActivityKey = sidebar.showAgentActivity.userDefaultsKey static let showCustomMetadataKey = sidebar.showCustomMetadata.userDefaultsKey @@ -47,6 +48,7 @@ enum SidebarWorkspaceDetailDefaults { static let showPorts = sidebar.showPorts.defaultValue static let showLog = sidebar.showLog.defaultValue static let showProgress = sidebar.showProgress.defaultValue + static let showAgentUsage = sidebar.showAgentUsage.defaultValue static let showAgentActivity = sidebar.showAgentActivity.defaultValue static let showCustomMetadata = sidebar.showCustomMetadata.defaultValue } @@ -89,6 +91,7 @@ extension SidebarWorkspaceDetailDefaults { showMetadata: details.showCustomMetadata, showLog: details.showLog, showProgress: details.showProgress, + showAgentUsage: details.showAgentUsage, showBranchDirectory: details.showBranchDirectory, showPullRequests: details.showPullRequests, showPorts: details.showPorts, @@ -340,6 +343,10 @@ enum SidebarSettingsFileMapping { jsonKey: "showProgress", defaultsKey: SidebarWorkspaceDetailDefaults.showProgressKey ), + .init( + jsonKey: "showAgentUsage", + defaultsKey: SidebarWorkspaceDetailDefaults.showAgentUsageKey + ), .init( jsonKey: "showAgentActivity", defaultsKey: SidebarWorkspaceDetailDefaults.showAgentActivityKey diff --git a/Sources/CommandPalette/CommandPaletteSettingsToggle.swift b/Sources/CommandPalette/CommandPaletteSettingsToggle.swift index 193417f4e9f7..2a9b88203eaa 100644 --- a/Sources/CommandPalette/CommandPaletteSettingsToggle.swift +++ b/Sources/CommandPalette/CommandPaletteSettingsToggle.swift @@ -973,7 +973,7 @@ enum CommandPaletteSettingsToggleCommands { SystemWideHotkeySettings.setEnabled(newValue, defaults: defaults) } ), - ] + ] + [sidebarAgentUsageDescriptor(sectionTitle: sidebar, isAvailable: sidebarDetailsAvailable)] }() } diff --git a/Sources/CommandPalette/CommandPaletteSettingsToggleCommands+AgentUsage.swift b/Sources/CommandPalette/CommandPaletteSettingsToggleCommands+AgentUsage.swift new file mode 100644 index 000000000000..fa98f72924df --- /dev/null +++ b/Sources/CommandPalette/CommandPaletteSettingsToggleCommands+AgentUsage.swift @@ -0,0 +1,24 @@ +import Foundation + +extension CommandPaletteSettingsToggleCommands { + /// The palette toggle for `sidebar.showAgentUsage`, kept out of the + /// length-budgeted descriptor table. It shares the sidebar-details + /// availability rule, so it is hidden while "Hide All Details" is on. + static func sidebarAgentUsageDescriptor( + sectionTitle: @escaping @Sendable () -> String, + isAvailable: @escaping @Sendable (UserDefaults) -> Bool + ) -> CommandPaletteSettingToggleDescriptor { + CommandPaletteSettingToggleDescriptor( + commandId: commandIdPrefix + "showAgentUsageInSidebar", + settingsKey: "sidebar.showAgentUsage", + title: { + String(localized: "settings.app.showAgentUsage", defaultValue: "Show Agent Usage in Sidebar") + }, + sectionTitle: sectionTitle, + keywords: ["sidebar.showAgentUsage", "sidebar", "agent", "usage", "model", "context", "tokens", "cost"], + defaultValue: SidebarWorkspaceDetailDefaults.showAgentUsage, + defaultsKey: SidebarWorkspaceDetailDefaults.showAgentUsageKey, + isAvailable: isAvailable + ) + } +} diff --git a/Sources/KeyboardShortcutSettingsFileStore+Template.swift b/Sources/KeyboardShortcutSettingsFileStore+Template.swift index ed08e8499123..5889d9537a00 100644 --- a/Sources/KeyboardShortcutSettingsFileStore+Template.swift +++ b/Sources/KeyboardShortcutSettingsFileStore+Template.swift @@ -164,6 +164,7 @@ extension CmuxSettingsFileStore { "showPorts": SidebarWorkspaceDetailDefaults.showPorts, "showLog": SidebarWorkspaceDetailDefaults.showLog, "showProgress": SidebarWorkspaceDetailDefaults.showProgress, + "showAgentUsage": SidebarWorkspaceDetailDefaults.showAgentUsage, "showAgentActivity": SidebarWorkspaceDetailDefaults.showAgentActivity, "showCustomMetadata": SidebarWorkspaceDetailDefaults.showCustomMetadata, ], diff --git a/Sources/SettingsSearchAliases.swift b/Sources/SettingsSearchAliases.swift index ddf5ac446469..6c16a2289c24 100644 --- a/Sources/SettingsSearchAliases.swift +++ b/Sources/SettingsSearchAliases.swift @@ -144,6 +144,7 @@ enum SettingsSearchAliasIndex { "sidebarAppearance:show-ports": localized("settings.search.alias.setting.app.show-ports", defaultValue: "sidebar.showPorts localhost port listener dev server url"), "sidebarAppearance:show-log": localized("settings.search.alias.setting.app.show-log", defaultValue: "sidebar.showLog log status latest message imperative"), "sidebarAppearance:show-progress": localized("settings.search.alias.setting.app.show-progress", defaultValue: "sidebar.showProgress progress bar percent status set_progress"), + "sidebarAppearance:show-agent-usage": localized("settings.search.alias.setting.app.show-agent-usage", defaultValue: "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex"), "sidebarAppearance:show-agent-activity": localized("settings.search.alias.setting.app.show-agent-activity", defaultValue: "sidebar.showAgentActivity loading spinner active coding agent agents running activity"), "sidebarAppearance:loading-spinner-position": localized("settings.search.alias.setting.app.loading-spinner-position", defaultValue: "sidebar.loadingSpinnerPosition loading spinner position left right leading trailing side workspace"), "sidebarAppearance:notification-badge-position": localized("settings.search.alias.setting.app.notification-badge-position", defaultValue: "sidebar.notificationBadgePosition notification unread badge position left right leading trailing side workspace"), diff --git a/Sources/SettingsSearchIndex.swift b/Sources/SettingsSearchIndex.swift index df4addc88572..54c0c7fac298 100644 --- a/Sources/SettingsSearchIndex.swift +++ b/Sources/SettingsSearchIndex.swift @@ -125,6 +125,7 @@ enum SettingsSearchIndex { setting(.sidebarAppearance, "show-ports", String(localized: "settings.app.showPorts", defaultValue: "Show Listening Ports in Sidebar"), "localhost port"), setting(.sidebarAppearance, "show-log", String(localized: "settings.app.showLog", defaultValue: "Show Latest Log in Sidebar"), "status message"), setting(.sidebarAppearance, "show-progress", String(localized: "settings.app.showProgress", defaultValue: "Show Progress in Sidebar"), "progress bar"), + setting(.sidebarAppearance, "show-agent-usage", String(localized: "settings.app.showAgentUsage", defaultValue: "Show Agent Usage in Sidebar"), "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex"), setting(.sidebarAppearance, "show-agent-activity", String(localized: "settings.app.showAgentActivity", defaultValue: "Show Loading Spinner"), "sidebar.showAgentActivity loading spinner active coding agent agents running activity"), setting(.sidebarAppearance, "loading-spinner-position", String(localized: "settings.app.loadingSpinnerPosition", defaultValue: "Loading Spinner Position"), "sidebar.loadingSpinnerPosition loading spinner position left right leading trailing side"), setting(.sidebarAppearance, "notification-badge-position", String(localized: "settings.app.notificationBadgePosition", defaultValue: "Notification Badge Position"), "sidebar.notificationBadgePosition notification unread badge position left right leading trailing side"), @@ -273,6 +274,7 @@ enum SettingsSearchIndex { "sidebar.showPorts": settingID(for: .sidebarAppearance, idSuffix: "show-ports"), "sidebar.showLog": settingID(for: .sidebarAppearance, idSuffix: "show-log"), "sidebar.showProgress": settingID(for: .sidebarAppearance, idSuffix: "show-progress"), + "sidebar.showAgentUsage": settingID(for: .sidebarAppearance, idSuffix: "show-agent-usage"), "sidebar.showAgentActivity": settingID(for: .sidebarAppearance, idSuffix: "show-agent-activity"), "sidebar.loadingSpinnerPosition": settingID(for: .sidebarAppearance, idSuffix: "loading-spinner-position"), "sidebar.notificationBadgePosition": settingID(for: .sidebarAppearance, idSuffix: "notification-badge-position"), diff --git a/Sources/Sidebar/SidebarAgentUsageCoordinator.swift b/Sources/Sidebar/SidebarAgentUsageCoordinator.swift new file mode 100644 index 000000000000..251580740b6f --- /dev/null +++ b/Sources/Sidebar/SidebarAgentUsageCoordinator.swift @@ -0,0 +1,177 @@ +import CMUXAgentLaunch +import CmuxAgentChat +import CmuxSidebar +import Foundation + +/// Keeps the opt-in sidebar agent usage (`sidebar.showAgentUsage`) current. +/// +/// Agent hook events (Claude Code, Codex) mark a session's transcript as +/// dirty. After a short coalescing delay the transcript is sampled by the +/// ``AgentUsageSampler`` actor, which reads only newly appended bytes off the +/// main actor, and the resulting Sendable snapshot is stored on the +/// workspace's ``WorkspaceSidebarMetadataModel`` under the agent's status key. +/// +/// Nothing is read while the setting is off (or "Hide All Details" is on): +/// events are dropped, pending samples are cancelled, and previously shown +/// usage is cleared. There is no polling; usage refreshes only when the agent +/// reports a hook event. +@MainActor +final class SidebarAgentUsageCoordinator { + typealias MetadataLookup = @MainActor (UUID) -> WorkspaceSidebarMetadataModel? + + private struct PendingSample { + let transcriptPath: String + let source: AgentUsageSource + let workspaceID: UUID + } + + private let sampler: AgentUsageSampler + private let defaults: UserDefaults + private let coalesceInterval: Duration + private let clock: any Clock + private let metadataLookup: MetadataLookup + private var pendingBySessionID: [String: PendingSample] = [:] + private var flushTasks: [String: Task] = [:] + private var workspaceIDsShowingUsage: Set = [] + private var observationTask: Task? + + /// Creates a coordinator. + /// + /// - Parameters: + /// - sampler: Off-main transcript sampler. + /// - defaults: Settings store for `sidebar.showAgentUsage` and + /// `sidebar.hideAllDetails`. + /// - coalesceInterval: Delay that folds a burst of hook events (tool + /// storms) into one transcript read per session. + /// - clock: Clock driving the coalescing delay (tests inject their own). + /// - metadataLookup: Resolves a workspace id to its sidebar metadata. + init( + sampler: AgentUsageSampler = AgentUsageSampler(), + defaults: UserDefaults = .standard, + coalesceInterval: Duration = .milliseconds(750), + clock: any Clock = ContinuousClock(), + metadataLookup: @escaping MetadataLookup + ) { + self.sampler = sampler + self.defaults = defaults + self.coalesceInterval = coalesceInterval + self.clock = clock + self.metadataLookup = metadataLookup + } + + /// Starts consuming accepted agent hook events for the app's lifetime. + /// + /// The observation task holds the coordinator, so the composition root + /// can start it without storing it. Idempotent. + func start() { + guard observationTask == nil else { return } + observationTask = Task { @MainActor in + for await notification in NotificationCenter.default.notifications(named: .workstreamEventReceived) { + guard !Task.isCancelled else { return } + guard let event = notification.object as? WorkstreamEvent else { continue } + self.noteHookEvent(event) + } + } + } + + /// Whether usage is currently displayed (and therefore sampled). + var isEnabled: Bool { + SidebarWorkspaceDetailDefaults.auxiliaryDetailVisibility(defaults: defaults).showsAgentUsage + } + + /// Records one accepted hook event and schedules a coalesced sample of + /// its session transcript. + /// + /// - Parameter event: The hook event as accepted by the feed pipeline. + func noteHookEvent(_ event: WorkstreamEvent) { + guard let source = AgentUsageSource(hookSource: event.source), + let workspaceID = event.workspaceId.flatMap(UUID.init(uuidString:)) else { return } + guard isEnabled else { + clearAll() + return + } + switch event.hookEventName { + case .sessionStart, .sessionEnd: + // A new or finished session must not keep the previous one's numbers. + metadataLookup(workspaceID)?.updateAgentUsage(nil, forStatusKey: source.sidebarStatusKey) + if event.hookEventName == .sessionEnd { + cancelPending(sessionID: event.sessionId) + if let path = event.transcriptPath { + Task { [sampler] in await sampler.forget(transcriptPath: path) } + } + return + } + default: + break + } + guard let transcriptPath = event.transcriptPath, !transcriptPath.isEmpty else { return } + pendingBySessionID[event.sessionId] = PendingSample( + transcriptPath: transcriptPath, + source: source, + workspaceID: workspaceID + ) + scheduleFlush(sessionID: event.sessionId) + } + + private func scheduleFlush(sessionID: String) { + guard flushTasks[sessionID] == nil else { return } + flushTasks[sessionID] = Task { @MainActor [weak self, clock, coalesceInterval] in + do { + // Bounded, cancellable coalescing delay (not a poll). + try await clock.sleep(for: coalesceInterval) + } catch { + return + } + guard !Task.isCancelled else { return } + await self?.flush(sessionID: sessionID) + } + } + + private func flush(sessionID: String) async { + defer { + // A cancelled flush was already removed by whoever cancelled it. + if !Task.isCancelled { + flushTasks[sessionID] = nil + // Events that arrived while sampling get their own pass. + if pendingBySessionID[sessionID] != nil { scheduleFlush(sessionID: sessionID) } + } + } + guard let pending = pendingBySessionID.removeValue(forKey: sessionID), isEnabled else { return } + let snapshot = await sampler.sample(transcriptPath: pending.transcriptPath, source: pending.source) + // The setting may have flipped, or the session ended, while sampling. + guard isEnabled, !Task.isCancelled, + let metadata = metadataLookup(pending.workspaceID) else { return } + metadata.updateAgentUsage(snapshot.map(Self.sidebarUsage), forStatusKey: pending.source.sidebarStatusKey) + if snapshot != nil { + workspaceIDsShowingUsage.insert(pending.workspaceID) + } + } + + private func cancelPending(sessionID: String) { + flushTasks.removeValue(forKey: sessionID)?.cancel() + pendingBySessionID[sessionID] = nil + } + + private func clearAll() { + for task in flushTasks.values { task.cancel() } + flushTasks.removeAll() + pendingBySessionID.removeAll() + guard !workspaceIDsShowingUsage.isEmpty else { return } + for workspaceID in workspaceIDsShowingUsage { + guard let metadata = metadataLookup(workspaceID) else { continue } + for source in AgentUsageSource.allCases { + metadata.updateAgentUsage(nil, forStatusKey: source.sidebarStatusKey) + } + } + workspaceIDsShowingUsage.removeAll() + Task { [sampler] in await sampler.reset() } + } + + static func sidebarUsage(_ snapshot: AgentUsageSnapshot) -> SidebarAgentUsage { + SidebarAgentUsage( + modelName: snapshot.modelDisplayName, + contextFraction: snapshot.contextFraction, + estimatedCostUSD: snapshot.estimatedCostUSD + ) + } +} diff --git a/Sources/Sidebar/SidebarAgentUsageFormatter.swift b/Sources/Sidebar/SidebarAgentUsageFormatter.swift new file mode 100644 index 000000000000..b57a0866436f --- /dev/null +++ b/Sources/Sidebar/SidebarAgentUsageFormatter.swift @@ -0,0 +1,64 @@ +import CmuxSidebar +import Foundation + +/// Formats coding-agent usage for the sidebar and appends it to the matching +/// agent status entry, e.g. `Running · Opus 4.8 · 42% · ~$1.20`. +/// +/// The percentage is the share of the model's context window in use; the +/// dollar figure is an estimate at published API list prices and is always +/// prefixed with the localized "estimated" marker. +struct SidebarAgentUsageFormatter { + /// Separator between the status text and each usage component. + static let separator = " · " + + let locale: Locale + + /// Creates a formatter. + /// - Parameter locale: Locale for the percent and currency formats. + init(locale: Locale = .current) { + self.locale = locale + } + + /// The compact usage summary, e.g. `Opus 4.8 · 42% · ~$1.20`. + /// + /// Components that are unknown (context window, price) are omitted. + func summary(for usage: SidebarAgentUsage) -> String { + var parts = [usage.modelName] + if let fraction = usage.contextFraction { + parts.append(fraction.formatted(.percent.precision(.fractionLength(0)).locale(locale))) + } + if let cost = usage.estimatedCostUSD { + let costText = cost.formatted(.currency(code: "USD").precision(.fractionLength(2)).locale(locale)) + // The tilde marks the figure as an estimate, not a bill. + parts.append(String(localized: "sidebar.agentUsage.estimatedCost", defaultValue: "~\(costText)")) + } + return parts.joined(separator: Self.separator) + } + + /// Appends usage to each status entry whose key has usage. + /// + /// - Parameters: + /// - entries: Status entries in display order. + /// - usageByStatusKey: Usage keyed by agent status key; pass an empty + /// dictionary when `sidebar.showAgentUsage` is off. + /// - Returns: The entries, with matching values extended in place. + func decorate( + _ entries: [SidebarStatusEntry], + usageByStatusKey: [String: SidebarAgentUsage] + ) -> [SidebarStatusEntry] { + guard !usageByStatusKey.isEmpty else { return entries } + return entries.map { entry in + guard let usage = usageByStatusKey[entry.key] else { return entry } + return SidebarStatusEntry( + key: entry.key, + value: entry.value + Self.separator + summary(for: usage), + icon: entry.icon, + color: entry.color, + url: entry.url, + priority: entry.priority, + format: entry.format, + timestamp: entry.timestamp + ) + } + } +} diff --git a/Sources/SidebarTabItemSettingsSnapshot.swift b/Sources/SidebarTabItemSettingsSnapshot.swift index c873906e0c82..2041d8c48723 100644 --- a/Sources/SidebarTabItemSettingsSnapshot.swift +++ b/Sources/SidebarTabItemSettingsSnapshot.swift @@ -80,6 +80,7 @@ struct SidebarTabItemSettingsSnapshot: Equatable { showMetadata: details.showCustomMetadata, showLog: details.showLog, showProgress: details.showProgress, + showAgentUsage: details.showAgentUsage, showBranchDirectory: details.showBranchDirectory, showPullRequests: details.showPullRequests, showPorts: details.showPorts, diff --git a/Sources/SidebarWorkspaceDetailSettings.swift b/Sources/SidebarWorkspaceDetailSettings.swift index 0a47af5e5d99..51487e905d62 100644 --- a/Sources/SidebarWorkspaceDetailSettings.swift +++ b/Sources/SidebarWorkspaceDetailSettings.swift @@ -10,6 +10,7 @@ struct SidebarWorkspaceDetailSettings: Equatable { let showPorts: Bool let showLog: Bool let showProgress: Bool + let showAgentUsage: Bool let showAgentActivity: Bool let showCustomMetadata: Bool @@ -23,6 +24,7 @@ struct SidebarWorkspaceDetailSettings: Equatable { showPorts = settings.value(for: sidebar.showPorts) showLog = settings.value(for: sidebar.showLog) showProgress = settings.value(for: sidebar.showProgress) + showAgentUsage = settings.value(for: sidebar.showAgentUsage) showAgentActivity = settings.value(for: sidebar.showAgentActivity) showCustomMetadata = settings.value(for: sidebar.showCustomMetadata) } diff --git a/Sources/SidebarWorkspaceSnapshotFactory.swift b/Sources/SidebarWorkspaceSnapshotFactory.swift index 6cf1666d8462..76affae1e133 100644 --- a/Sources/SidebarWorkspaceSnapshotFactory.swift +++ b/Sources/SidebarWorkspaceSnapshotFactory.swift @@ -89,7 +89,12 @@ struct SidebarWorkspaceSnapshotFactory { copyableSidebarSSHError: copyableSidebarSSHError, latestConversationMessage: workspace.latestConversationMessage, metadataEntries: detailVisibility.showsMetadata - ? workspace.sidebarStatusEntriesInDisplayOrder() + ? SidebarAgentUsageFormatter().decorate( + workspace.sidebarStatusEntriesInDisplayOrder(), + usageByStatusKey: detailVisibility.showsAgentUsage + ? workspace.sidebarMetadata.agentUsageByStatusKey + : [:] + ) : [], metadataBlocks: detailVisibility.showsMetadata ? workspace.sidebarMetadataBlocksInDisplayOrder() diff --git a/Sources/WorkspaceSidebarObservation.swift b/Sources/WorkspaceSidebarObservation.swift index 889adf35675f..8c7b38243b88 100644 --- a/Sources/WorkspaceSidebarObservation.swift +++ b/Sources/WorkspaceSidebarObservation.swift @@ -220,6 +220,7 @@ private struct SidebarObservationState: Equatable { let panelDirectoryDisplayLabels: [UUID: String] let directoryChangeRevision: UInt64 let statusEntries: [String: SidebarStatusEntry] + let agentUsage: [String: SidebarAgentUsage] let metadataBlocks: [String: SidebarMetadataBlock] let logEntries: [SidebarLogEntry] let progress: SidebarProgressState? @@ -346,11 +347,15 @@ extension Workspace { gitFields, remoteFields ) - .combineLatest($listeningPorts, sidebarMetadata.panelDirectoryDisplayLabelsPublisher) + .combineLatest( + $listeningPorts, + sidebarMetadata.panelDirectoryDisplayLabelsPublisher, + sidebarMetadata.agentUsagePublisher + ) .combineLatest(directoryChangeRevision) .compactMap { [weak self] values, directoryChangeRevision -> SidebarObservationState? in guard let self else { return nil } - let (groupedFields, listeningPorts, panelDirectoryDisplayLabels) = values + let (groupedFields, listeningPorts, panelDirectoryDisplayLabels, agentUsage) = values let workspaceFields = groupedFields.0 let metadataFields = groupedFields.1 let gitFields = groupedFields.2 @@ -363,6 +368,7 @@ extension Workspace { panelDirectoryDisplayLabels: panelDirectoryDisplayLabels, directoryChangeRevision: directoryChangeRevision, statusEntries: metadataFields.0, + agentUsage: agentUsage, metadataBlocks: metadataFields.1, logEntries: metadataFields.2, progress: metadataFields.3, diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index c1355c0d22a5..db33dca4ad34 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -1470,6 +1470,7 @@ C0DE86060000000000000003 /* CommandPaletteRestoreFocusTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE86060000000000000004 /* CommandPaletteRestoreFocusTarget.swift */; }; A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008382 /* CommandPaletteSearchEngineTests.swift */; }; C0DEF4110000000000000001 /* CommandPaletteSettingsToggle.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEF4110000000000000002 /* CommandPaletteSettingsToggle.swift */; }; + C0DEF4E10000000000000001 /* CommandPaletteSettingsToggleCommands+AgentUsage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEF4E10000000000000002 /* CommandPaletteSettingsToggleCommands+AgentUsage.swift */; }; C0DEF4120000000000000001 /* CommandPaletteSettingsToggleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DEF4120000000000000002 /* CommandPaletteSettingsToggleTests.swift */; }; C1713006C1713006C1713006 /* CommandPaletteShortcutCustomizationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1713005C1713005C1713005 /* CommandPaletteShortcutCustomizationTests.swift */; }; C1713002C1713002C1713002 /* CommandPaletteShortcutRouting.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1713001C1713001C1713001 /* CommandPaletteShortcutRouting.swift */; }; @@ -2872,6 +2873,9 @@ 13019A000000000000000002 /* SidebarAccountTeamPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 13019A000000000000000001 /* SidebarAccountTeamPopover.swift */; }; A6AC71020000000000000001 /* SidebarAgentActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6AC71030000000000000001 /* SidebarAgentActivityIndicator.swift */; }; A6AC71040000000000000001 /* SidebarAgentActivitySummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6AC71050000000000000001 /* SidebarAgentActivitySummary.swift */; }; + A6AC7E510000000000000001 /* SidebarAgentUsageCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6AC7E510000000000000002 /* SidebarAgentUsageCoordinator.swift */; }; + A6AC7E520000000000000001 /* SidebarAgentUsageFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6AC7E520000000000000002 /* SidebarAgentUsageFormatter.swift */; }; + 1C4AC99C63561A296895F00D /* SidebarAgentUsageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96C3631F8DF0DA07B106A00C /* SidebarAgentUsageTests.swift */; }; 211A75777F433ED8BA5AE208 /* SidebarAppearanceSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 243632BA1DBBA36FD46E0610 /* SidebarAppearanceSupport.swift */; }; B804C0030000000000000003 /* SidebarAppKitRowCellTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B804D0030000000000000003 /* SidebarAppKitRowCellTests.swift */; }; D7AB34300000000000000003 /* SidebarBonsplitTabWorkspaceDropOverlay.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB34300000000000000004 /* SidebarBonsplitTabWorkspaceDropOverlay.swift */; }; @@ -5445,6 +5449,7 @@ C0DE86060000000000000004 /* CommandPaletteRestoreFocusTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteRestoreFocusTarget.swift; sourceTree = ""; }; A5008382 /* CommandPaletteSearchEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteSearchEngineTests.swift; sourceTree = ""; }; C0DEF4110000000000000002 /* CommandPaletteSettingsToggle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPalette/CommandPaletteSettingsToggle.swift; sourceTree = ""; }; + C0DEF4E10000000000000002 /* CommandPaletteSettingsToggleCommands+AgentUsage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CommandPalette/CommandPaletteSettingsToggleCommands+AgentUsage.swift"; sourceTree = ""; }; C0DEF4120000000000000002 /* CommandPaletteSettingsToggleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteSettingsToggleTests.swift; sourceTree = ""; }; C1713005C1713005C1713005 /* CommandPaletteShortcutCustomizationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteShortcutCustomizationTests.swift; sourceTree = ""; }; C1713001C1713001C1713001 /* CommandPaletteShortcutRouting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/CommandPaletteShortcutRouting.swift; sourceTree = ""; }; @@ -6832,6 +6837,9 @@ 13019A000000000000000001 /* SidebarAccountTeamPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SidebarAccountTeamPopover.swift"; sourceTree = ""; }; A6AC71030000000000000001 /* SidebarAgentActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/SidebarAgentActivityIndicator.swift; sourceTree = ""; }; A6AC71050000000000000001 /* SidebarAgentActivitySummary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarAgentActivitySummary.swift; sourceTree = ""; }; + A6AC7E510000000000000002 /* SidebarAgentUsageCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/SidebarAgentUsageCoordinator.swift; sourceTree = ""; }; + A6AC7E520000000000000002 /* SidebarAgentUsageFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/SidebarAgentUsageFormatter.swift; sourceTree = ""; }; + 96C3631F8DF0DA07B106A00C /* SidebarAgentUsageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SidebarAgentUsageTests.swift"; sourceTree = ""; }; 243632BA1DBBA36FD46E0610 /* SidebarAppearanceSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/SidebarAppearanceSupport.swift; sourceTree = ""; }; B804D0030000000000000003 /* SidebarAppKitRowCellTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarAppKitRowCellTests.swift; sourceTree = ""; }; D7AB34300000000000000004 /* SidebarBonsplitTabWorkspaceDropOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/SidebarBonsplitTabWorkspaceDropOverlay.swift; sourceTree = ""; }; @@ -8972,6 +8980,8 @@ A6AC73010000000000000002 /* SidebarWorkspaceStatusSlots.swift */, A6AC73330000000000000002 /* SidebarWorkspaceTrailingStatusSlot.swift */, A6AC73340000000000000002 /* SidebarWorkspaceUnreadBadge.swift */, + A6AC7E520000000000000002 /* SidebarAgentUsageFormatter.swift */, + A6AC7E510000000000000002 /* SidebarAgentUsageCoordinator.swift */, 5B11E5A100000000000000B2 /* GPUSpinner.swift */, A6AC72010000000000000002 /* GPUSpinnerNSView.swift */, A6AC72020000000000000002 /* GPUSpinnerStyle.swift */, @@ -9098,6 +9108,7 @@ C0DEFF200000000000000002 /* CommandPaletteOverlay.swift */, AFBCA7B529B723C1525D40DB /* CurrentWorkPalettePresentation.swift */, C0DEF4110000000000000002 /* CommandPaletteSettingsToggle.swift */, + C0DEF4E10000000000000002 /* CommandPaletteSettingsToggleCommands+AgentUsage.swift */, 9DAB808A8EC74C40B95F7672 /* GhosttySurfaceConfigurationRefresh.swift */, 6B8E2E03F4A64C61B729CF19 /* TerminalDirectoryOpenSupport.swift */, 8A4FE96C3F394FC6A6D4B018 /* CmuxCLIPathInstaller.swift */, @@ -12106,6 +12117,7 @@ 266B552D3FBFED20D1EDE625 /* SSHTuiOpenTests.swift */, F044F74E3A921F191E3F0EAF /* SSHTuiPreflightTests.swift */, 7AAA9FEEAC85B8AB216861F2 /* GotoSplitActionTests.swift */, + 96C3631F8DF0DA07B106A00C /* SidebarAgentUsageTests.swift */, ); path = cmuxTests; sourceTree = ""; @@ -13501,6 +13513,7 @@ C0DEFF200000000000000001 /* CommandPaletteOverlay.swift in Sources */, C0DE86060000000000000003 /* CommandPaletteRestoreFocusTarget.swift in Sources */, C0DEF4110000000000000001 /* CommandPaletteSettingsToggle.swift in Sources */, + C0DEF4E10000000000000001 /* CommandPaletteSettingsToggleCommands+AgentUsage.swift in Sources */, C1713002C1713002C1713002 /* CommandPaletteShortcutRouting.swift in Sources */, C0A7101A0000000000000002 /* ComputerUseAppDragSource.swift in Sources */, C0A7101B0000000000000002 /* ComputerUseAppDragSourceView.swift in Sources */, @@ -14421,6 +14434,8 @@ 13019A000000000000000002 /* SidebarAccountTeamPopover.swift in Sources */, A6AC71020000000000000001 /* SidebarAgentActivityIndicator.swift in Sources */, A6AC71040000000000000001 /* SidebarAgentActivitySummary.swift in Sources */, + A6AC7E510000000000000001 /* SidebarAgentUsageCoordinator.swift in Sources */, + A6AC7E520000000000000001 /* SidebarAgentUsageFormatter.swift in Sources */, 211A75777F433ED8BA5AE208 /* SidebarAppearanceSupport.swift in Sources */, D7AB34300000000000000003 /* SidebarBonsplitTabWorkspaceDropOverlay.swift in Sources */, 41C32603305047F29AA29D23 /* SidebarBonsplitTabWorkspaceDropView+Ownership.swift in Sources */, @@ -16441,6 +16456,7 @@ F6001000A1B2C3D4E5F60718 /* ShortcutUnbindingTests.swift in Sources */, 29B9EA7A64AF6741463D582B /* SidebarAccessibilityTreeTests.swift in Sources */, 18E25585AB1083F87E29B06F /* SidebarAccessibilityTreeWalk.swift in Sources */, + 1C4AC99C63561A296895F00D /* SidebarAgentUsageTests.swift in Sources */, B804C0030000000000000003 /* SidebarAppKitRowCellTests.swift in Sources */, 9BE45B94B05D471FA386203D /* SidebarCloudWorkspaceBadgeTests.swift in Sources */, A12481700000000000000001 /* SidebarDerivedStateScaleTests.swift in Sources */, diff --git a/cmuxTests/SidebarAgentUsageTests.swift b/cmuxTests/SidebarAgentUsageTests.swift new file mode 100644 index 000000000000..8eb175a5de76 --- /dev/null +++ b/cmuxTests/SidebarAgentUsageTests.swift @@ -0,0 +1,92 @@ +import CMUXAgentLaunch +import CmuxSettings +import CmuxSidebar +import Foundation +import Testing +@testable import cmux_DEV + +private struct UnlimitedSidebarLog: SidebarLogEntryLimitProviding { + let configuredMaxSidebarLogEntries: Int? = nil +} + +@MainActor +@Suite("SidebarAgentUsage") +struct SidebarAgentUsageTests { + private static let usage = SidebarAgentUsage(modelName: "Opus 4.8", contextFraction: 0.4213, estimatedCostUSD: 1.2) + + private static func makeDefaults() -> UserDefaults { + UserDefaults(suiteName: "SidebarAgentUsageTests.\(UUID().uuidString)")! + } + + private static func statusEntry(key: String, value: String) -> SidebarStatusEntry { + SidebarStatusEntry(key: key, value: value, icon: "bolt.fill", color: "#4C8DFF", priority: 5, timestamp: Date(timeIntervalSince1970: 10)) + } + + @Test func summaryShowsModelContextPercentAndEstimatedCost() { + let formatter = SidebarAgentUsageFormatter(locale: Locale(identifier: "en_US")) + #expect(formatter.summary(for: Self.usage) == "Opus 4.8 · 42% · ~$1.20") + } + + @Test func summaryOmitsUnknownWindowAndPrice() { + let formatter = SidebarAgentUsageFormatter(locale: Locale(identifier: "en_US")) + let usage = SidebarAgentUsage(modelName: "gpt-6-astra", contextFraction: nil, estimatedCostUSD: nil) + #expect(formatter.summary(for: usage) == "gpt-6-astra") + } + + @Test func decorationExtendsOnlyTheMatchingAgentEntry() { + let formatter = SidebarAgentUsageFormatter(locale: Locale(identifier: "en_US")) + let claude = Self.statusEntry(key: "claude_code", value: "Running") + let other = Self.statusEntry(key: "build", value: "Passing") + + let decorated = formatter.decorate([claude, other], usageByStatusKey: ["claude_code": Self.usage]) + + #expect(decorated[0].value == "Running · Opus 4.8 · 42% · ~$1.20") + #expect(decorated[0].icon == claude.icon) + #expect(decorated[0].color == claude.color) + #expect(decorated[0].priority == claude.priority) + #expect(decorated[1] == other) + #expect(formatter.decorate([claude], usageByStatusKey: [:]) == [claude]) + } + + @Test func settingIsOffByDefaultAndHiddenByHideAllDetails() { + let defaults = Self.makeDefaults() + #expect(!SidebarTabItemSettingsSnapshot(defaults: defaults).visibleAuxiliaryDetails.showsAgentUsage) + + defaults.set(true, forKey: SidebarWorkspaceDetailDefaults.showAgentUsageKey) + #expect(SidebarTabItemSettingsSnapshot(defaults: defaults).visibleAuxiliaryDetails.showsAgentUsage) + + defaults.set(true, forKey: SidebarCatalogSection().hideAllDetails.userDefaultsKey) + #expect(!SidebarTabItemSettingsSnapshot(defaults: defaults).visibleAuxiliaryDetails.showsAgentUsage) + } + + @Test func sessionStartClearsUsageAndOtherAgentsAreIgnored() { + let defaults = Self.makeDefaults() + let workspaceID = UUID() + let metadata = WorkspaceSidebarMetadataModel(limitProvider: UnlimitedSidebarLog()) + let coordinator = SidebarAgentUsageCoordinator(defaults: defaults) { id in + id == workspaceID ? metadata : nil + } + metadata.updateAgentUsage(Self.usage, forStatusKey: "claude_code") + + // Enabled: a session start clears the previous session's numbers. + defaults.set(true, forKey: SidebarWorkspaceDetailDefaults.showAgentUsageKey) + coordinator.noteHookEvent(WorkstreamEvent( + sessionId: "s1", + hookEventName: .sessionStart, + source: "claude", + workspaceId: workspaceID.uuidString, + transcriptPath: "/nonexistent/s1.jsonl" + )) + #expect(metadata.agentUsageByStatusKey["claude_code"] == nil) + + // Unsupported agents are ignored entirely. + metadata.updateAgentUsage(Self.usage, forStatusKey: "claude_code") + coordinator.noteHookEvent(WorkstreamEvent( + sessionId: "s2", + hookEventName: .sessionStart, + source: "gemini", + workspaceId: workspaceID.uuidString + )) + #expect(metadata.agentUsageByStatusKey["claude_code"] == Self.usage) + } +} diff --git a/skills/cmux-settings/references/all-keys.md b/skills/cmux-settings/references/all-keys.md index 605f3d20164f..5b0cdb3a0387 100644 --- a/skills/cmux-settings/references/all-keys.md +++ b/skills/cmux-settings/references/all-keys.md @@ -110,6 +110,7 @@ Sidebar content and metadata visibility from Settings > Sidebar. | `sidebar.showPorts` | boolean | `true` | Show listening ports. | | `sidebar.showLog` | boolean | `true` | Show recent log snippets. | | `sidebar.showProgress` | boolean | `true` | Show progress indicators. | +| `sidebar.showAgentUsage` | boolean | `false` | Append coding-agent usage (model, context window used, estimated API cost) to the Claude Code or Codex status entry. The cost is an estimate from published per-token API prices, not a bill. | | `sidebar.showCustomMetadata` | boolean | `true` | Show custom metadata pills. | | `sidebar.wrapWorkspaceTitles` | boolean | `false` | Allow workspace titles in the sidebar to wrap to multiple lines instead of truncating after one line. | | `sidebar.beta` | object | — | Experimental sidebar features. | diff --git a/web/data/cmux.schema.json b/web/data/cmux.schema.json index 125e5fae17d2..8d3b4b8a6e49 100644 --- a/web/data/cmux.schema.json +++ b/web/data/cmux.schema.json @@ -1282,6 +1282,11 @@ "default": true, "description": "Show progress indicators." }, + "showAgentUsage": { + "type": "boolean", + "default": false, + "description": "Append coding-agent usage (model, context window used, estimated API cost) to the Claude Code or Codex status entry. The cost is an estimate from published per-token API prices, not a bill." + }, "showAgentActivity": { "type": "boolean", "default": true, From 1b097a4723fd4599c5c0d63462955dd35a7ff976 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sat, 26 Sep 2026 13:45:47 -0400 Subject: [PATCH 03/13] Address review: bounded reads, per-session usage, live setting, subagent cost - Sampler: first read of a transcript over 24 MB takes only its last 4 MB (model and context; cost shown as unknown instead of a partial sum). Each transcript is read in a detached task that yields between chunks, so one big file no longer blocks other sessions. Lines are ingested without a copy; a skipped oversized line marks the cost as a lower bound. Replacement detection uses fstat on the open fd plus a head fingerprint, so a rewrite-in-place to a larger file restarts. forget/reset bump a generation so results for a forgotten transcript are dropped. - Claude Code subagent usage lives in /subagents/agent-*.jsonl (checked against real transcripts); it now counts toward cost, not context. - Accumulator: each message id is priced from its last line even when ids interleave; unknown models make the cost a lower bound ("~$1.20+") instead of hiding it; Vertex "@version" ids resolve; Codex context excludes reasoning tokens. OpenAI price rows removed (Codex shows model and context only); the Anthropic table is date-stamped and the unmodelled pricing factors are documented. - Coordinator: per-session records; a workspace's agent row shows the most recently active session of that agent, so one pane's SessionStart/End no longer clears another's usage. Observes settings changes: enabling samples known sessions immediately, disabling clears and drops cursors. Nothing is read while metadata rows are hidden. - Status entries gain an optional help text; the usage row's tooltip says the cost is an API list-price estimate, not a subscription bill. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Usage/AgentModelCatalog.swift | 39 +-- .../Usage/AgentModelPricing.swift | 19 +- .../CmuxAgentChat/Usage/AgentUsageCost.swift | 40 +++ .../Usage/AgentUsageFileCursor.swift | 26 ++ .../Usage/AgentUsageSampler.swift | 179 +++++------ .../Usage/AgentUsageSnapshot.swift | 25 +- .../AgentUsageTranscriptAccumulator.swift | 232 +++++++-------- .../Usage/AgentUsageTranscriptReader.swift | 147 +++++++++ .../AgentModelCatalogTests.swift | 23 +- .../AgentUsageFixtures.swift | 2 + .../AgentUsageSamplerTests.swift | 106 ++++++- ...AgentUsageTranscriptAccumulatorTests.swift | 78 +++-- .../CmuxConfigSchema.generated.swift | 280 +++++++++--------- .../CuratedSettingEntry+AgentUsage.swift | 2 +- .../Sections/SidebarSection+AgentUsage.swift | 2 +- .../Status/SidebarAgentUsage.swift | 11 +- .../SidebarStatusEntry+Presentation.swift | 6 + .../Status/SidebarStatusEntry.swift | 7 +- Resources/Localizable.xcstrings | 136 ++++++++- Sources/ContentView.swift | 2 +- .../Cells/SidebarRowTextView+PlainText.swift | 12 + .../SidebarWorkspaceRowSupportViews.swift | 4 +- .../SidebarAgentUsageCoordinator.swift | 247 ++++++++++----- .../Sidebar/SidebarAgentUsageFormatter.swift | 22 +- cmux.xcodeproj/project.pbxproj | 4 + cmuxTests/SidebarAgentUsageTests.swift | 127 ++++++-- skills/cmux-settings/references/all-keys.md | 2 +- web/data/cmux.schema.json | 2 +- 28 files changed, 1181 insertions(+), 601 deletions(-) create mode 100644 Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageCost.swift create mode 100644 Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageFileCursor.swift create mode 100644 Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptReader.swift create mode 100644 Sources/Sidebar/AppKitList/Cells/SidebarRowTextView+PlainText.swift diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelCatalog.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelCatalog.swift index 42daecf0b328..0f4fe445cfb6 100644 --- a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelCatalog.swift +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelCatalog.swift @@ -4,9 +4,8 @@ import Foundation /// window, and published list prices. /// /// The price table is deliberately small and explicit: a model that is not -/// listed gets no cost estimate instead of a guessed one. Prices are USD per -/// million tokens from the providers' public API price lists (Anthropic -/// prices as of 2026-06; OpenAI GPT-5 family list prices). +/// listed gets no cost estimate instead of a guessed one. Only Anthropic +/// models are priced; Codex/OpenAI models show model and context only. /// /// ```swift /// let info = AgentModelCatalog().info(forModelID: "claude-opus-4-8") @@ -26,7 +25,11 @@ public struct AgentModelCatalog: Sendable { "sonnet-5", "sonnet-4-6", ] - /// Anthropic list prices keyed by normalized family. + /// Anthropic first-party API list prices in USD per million tokens, + /// keyed by normalized family. Source: Anthropic model/pricing tables as + /// of 2026-06-24 (input, output, cache read; 5-minute and 1-hour cache + /// writes are 1.25x and 2x input). Update this table and its date + /// together. static let claudePrices: [String: AgentModelPricing] = [ "fable-5-1": .anthropic(input: 10, output: 50, cacheRead: 0.25), "mythos-5-1": .anthropic(input: 10, output: 50, cacheRead: 0.25), @@ -47,24 +50,14 @@ public struct AgentModelCatalog: Sendable { "haiku-4-5": .anthropic(input: 1, output: 5, cacheRead: 0.1), ] - /// OpenAI list prices keyed by exact model id. - static let openAIPrices: [String: AgentModelPricing] = [ - "gpt-5": .openAI(input: 1.25, output: 10, cachedInput: 0.125), - "gpt-5-codex": .openAI(input: 1.25, output: 10, cachedInput: 0.125), - "gpt-5.1": .openAI(input: 1.25, output: 10, cachedInput: 0.125), - "gpt-5.1-codex": .openAI(input: 1.25, output: 10, cachedInput: 0.125), - "gpt-5.1-codex-max": .openAI(input: 1.25, output: 10, cachedInput: 0.125), - "gpt-5-mini": .openAI(input: 0.25, output: 2, cachedInput: 0.025), - "gpt-5.1-codex-mini": .openAI(input: 0.25, output: 2, cachedInput: 0.025), - ] - /// Creates a catalog over the built-in tables. public init() {} /// Describes a model id as written in a transcript. /// /// Claude ids may carry a provider prefix (`us.anthropic.`), a date - /// suffix (`-20251001`), or a `[1m]` suffix; all are normalized before + /// suffix (`-20251001`), a Vertex version (`@20251001`), a Bedrock + /// version (`-v1:0`), or a `[1m]` suffix; all are normalized before /// lookup. `[1m]` forces a 1M context window. /// /// - Parameters: @@ -87,21 +80,17 @@ public struct AgentModelCatalog: Sendable { pricing: Self.claudePrices[family] ) } - return AgentModelInfo( - displayName: trimmed, - contextWindow: reportedContextWindow, - pricing: Self.openAIPrices[lowered] - ) + return AgentModelInfo(displayName: trimmed, contextWindow: reportedContextWindow, pricing: nil) } /// Extracts the normalized Claude family (`opus-4-8`) from an id such as - /// `us.anthropic.claude-opus-4-8-20260101-v1:0[1m]`, or `nil` when the id - /// is not a Claude id. + /// `us.anthropic.claude-opus-4-8-20260101-v1:0[1m]` or + /// `claude-haiku-4-5@20251001`, or `nil` when the id is not a Claude id. static func claudeFamily(fromModelID lowered: String) -> String? { guard let range = lowered.range(of: "claude-") else { return nil } var rest = String(lowered[range.upperBound...]) - if let bracket = rest.firstIndex(of: "[") { - rest = String(rest[.. Self { - Self( - inputPerMTok: input, - outputPerMTok: output, - cacheWrite5mPerMTok: input, - cacheWrite1hPerMTok: input, - cacheReadPerMTok: cachedInput - ) - } - /// Estimated cost of `tokens` at these prices, in US dollars. /// /// - Parameter tokens: Token counts split by billing class. diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageCost.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageCost.swift new file mode 100644 index 000000000000..c104c4ec126a --- /dev/null +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageCost.swift @@ -0,0 +1,40 @@ +import Foundation + +/// An estimated API cost and whether it is complete. +public struct AgentUsageCost: Sendable, Equatable { + /// Estimated cost in US dollars at published list prices. + public let usd: Double + /// `true` when some usage could not be priced (unknown model, or a + /// transcript line too large to read), so the real figure is higher. + public let isLowerBound: Bool + /// Whether any usage was actually priced. A cost built only from + /// unknown models carries no information and is not displayed. + public let hasPricedUsage: Bool + + /// Creates a cost. + public init(usd: Double, isLowerBound: Bool, hasPricedUsage: Bool = true) { + self.usd = usd + self.isLowerBound = isLowerBound + self.hasPricedUsage = hasPricedUsage + } + + /// This cost when it is worth showing, or `nil` when nothing was priced. + public var displayable: Self? { + hasPricedUsage ? self : nil + } + + /// Adds two estimates; the sum is a lower bound if either part is. + public static func + (lhs: Self, rhs: Self) -> Self { + Self( + usd: lhs.usd + rhs.usd, + isLowerBound: lhs.isLowerBound || rhs.isLowerBound, + hasPricedUsage: lhs.hasPricedUsage || rhs.hasPricedUsage + ) + } + + /// Adds two optional estimates; `nil` (unknown) is contagious. + static func combine(_ lhs: Self?, _ rhs: Self?) -> Self? { + guard let lhs, let rhs else { return nil } + return lhs + rhs + } +} diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageFileCursor.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageFileCursor.swift new file mode 100644 index 000000000000..67d14bd2f089 --- /dev/null +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageFileCursor.swift @@ -0,0 +1,26 @@ +import Foundation + +/// Incremental read position and parse state for one transcript file. +/// +/// Values are handed between the ``AgentUsageSampler`` actor and the +/// detached task that reads the file, so everything here is `Sendable`. +struct AgentUsageFileCursor: Sendable { + /// Usage folded from the lines read so far. + var accumulator: AgentUsageTranscriptAccumulator + /// Bytes consumed from the start of the file. + var byteOffset: UInt64 = 0 + /// Inode of the file when last read (from `fstat` on the open fd). + var inode: UInt64? + /// The first bytes of the file when last read. A mismatch means the file + /// was rewritten in place, even if it is now larger. + var head = Data() + /// Bytes of an incomplete trailing line, awaiting its newline. + var fragment = Data() + /// Skip bytes until the next newline (an oversized line, or the partial + /// first line of a tail read). + var discardingLine = false + + init(source: AgentUsageSource, catalog: AgentModelCatalog) { + accumulator = AgentUsageTranscriptAccumulator(source: source, catalog: catalog) + } +} diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSampler.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSampler.swift index c75240848415..e4332f1b9c66 100644 --- a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSampler.swift +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSampler.swift @@ -2,11 +2,11 @@ import Foundation /// Samples coding-agent usage from transcript files off the main actor. /// -/// Each transcript is read incrementally: the first sample scans the file -/// once in bounded chunks, and every later sample reads only the bytes -/// appended since the previous one, folding complete lines into a per-file -/// ``AgentUsageTranscriptAccumulator``. A truncated or atomically replaced -/// file (smaller size or new inode) restarts from the beginning. +/// Each transcript is read incrementally by ``AgentUsageTranscriptReader``: +/// the first sample scans the file once (only its tail when it is very +/// large), and later samples read only appended bytes. The actor only +/// bookkeeps cursors; the reading itself runs in a detached task per +/// transcript, so a large file never delays another session's sample. /// /// The sampler performs no polling and owns no timers; callers invoke /// ``sample(transcriptPath:source:)`` when an agent hook event says the @@ -17,47 +17,38 @@ import Foundation /// let snapshot = await sampler.sample(transcriptPath: path, source: .claude) /// ``` public actor AgentUsageSampler { - private struct TrackedTranscript { - var accumulator: AgentUsageTranscriptAccumulator - var byteOffset: UInt64 = 0 - var inode: UInt64? - var fragment = Data() - var discardingOversizedLine = false - var lastUse: UInt64 = 0 - } - - private struct Key: Hashable { + private struct Key: Hashable, Sendable { let path: String let source: AgentUsageSource } - private var tracked: [Key: TrackedTranscript] = [:] + private struct Generation: Equatable { + let epoch: UInt64 + let keyEpoch: UInt64 + } + + private var sessions: [Key: AgentUsageTranscriptReader.SessionCursor] = [:] + private var inFlight: Set = [] + private var epoch: UInt64 = 0 + private var keyEpochs: [Key: UInt64] = [:] private var useCounter: UInt64 = 0 private let maxTrackedTranscripts: Int - private let chunkSize: Int - private let maxLineBytes: Int - private let catalog: AgentModelCatalog + private let reader: AgentUsageTranscriptReader - /// Creates a sampler. + /// Creates a sampler with the default read limits. /// /// - Parameters: - /// - maxTrackedTranscripts: How many transcripts keep incremental state; - /// the least recently sampled is dropped beyond this (it is re-scanned - /// from the start if sampled again). - /// - chunkSize: Bytes read per `read` call. - /// - maxLineBytes: A single line longer than this is skipped rather than - /// buffered (huge tool outputs never carry usage worth the memory). + /// - maxTrackedTranscripts: How many sessions keep incremental state; + /// the least recently sampled is dropped beyond this. A dropped + /// session is re-read on its next sample (bounded by the tail limit). /// - catalog: Model table for display names, windows and prices. - public init( - maxTrackedTranscripts: Int = 32, - chunkSize: Int = 1 << 20, - maxLineBytes: Int = 32 << 20, - catalog: AgentModelCatalog = AgentModelCatalog() - ) { + public init(maxTrackedTranscripts: Int = 64, catalog: AgentModelCatalog = AgentModelCatalog()) { + self.init(maxTrackedTranscripts: maxTrackedTranscripts, reader: AgentUsageTranscriptReader(catalog: catalog)) + } + + init(maxTrackedTranscripts: Int = 64, reader: AgentUsageTranscriptReader) { self.maxTrackedTranscripts = max(1, maxTrackedTranscripts) - self.chunkSize = max(1, chunkSize) - self.maxLineBytes = max(1, maxLineBytes) - self.catalog = catalog + self.reader = reader } /// Reads any new transcript content and returns the current usage. @@ -65,101 +56,63 @@ public actor AgentUsageSampler { /// - Parameters: /// - transcriptPath: Absolute path of the agent transcript JSONL. /// - source: The transcript format. - /// - Returns: The usage snapshot, or `nil` when the file is unreadable or - /// carries no model/usage yet. - public func sample(transcriptPath: String, source: AgentUsageSource) -> AgentUsageSnapshot? { + /// - Returns: The usage snapshot, or `nil` when there is nothing new to + /// report: the file is unreadable or carries no model yet, a sample of + /// the same transcript is already running, or the transcript was + /// forgotten/reset while this sample ran. + public func sample(transcriptPath: String, source: AgentUsageSource) async -> AgentUsageSnapshot? { let key = Key(path: transcriptPath, source: source) + guard inFlight.insert(key).inserted else { return nil } + defer { inFlight.remove(key) } + let generation = generation(for: key) + let previous = sessions[key] ?? AgentUsageTranscriptReader.SessionCursor() + let reader = self.reader + let (updated, snapshot) = await Task.detached(priority: .utility) { + await reader.advanceSession(previous, path: transcriptPath, source: source) + }.value + guard generation == self.generation(for: key) else { return nil } useCounter &+= 1 - var state = tracked[key] ?? TrackedTranscript( - accumulator: AgentUsageTranscriptAccumulator(source: source, catalog: catalog) - ) - state.lastUse = useCounter - guard let handle = FileHandle(forReadingAtPath: transcriptPath) else { - tracked[key] = nil - return nil - } - defer { try? handle.close() } - let size = (try? handle.seekToEnd()) ?? 0 - let inode = Self.inode(ofPath: transcriptPath) - let replaced = state.inode != nil && inode != nil && state.inode != inode - if size < state.byteOffset || replaced { - state = TrackedTranscript( - accumulator: AgentUsageTranscriptAccumulator(source: source, catalog: catalog), - lastUse: useCounter - ) - } - state.inode = inode - if size > state.byteOffset { - readAppended(from: handle, into: &state) - } - let snapshot = state.accumulator.snapshot() - tracked[key] = state + var stored = updated + stored.lastUse = useCounter + sessions[key] = updated.main == nil ? nil : stored evictIfNeeded() return snapshot } /// Drops the incremental state for one transcript (for example when its - /// session ends). + /// session ends); a sample of it that is still running is discarded. /// - /// - Parameter transcriptPath: The transcript path passed to ``sample(transcriptPath:source:)``. + /// - Parameter transcriptPath: The path passed to ``sample(transcriptPath:source:)``. public func forget(transcriptPath: String) { - tracked = tracked.filter { $0.key.path != transcriptPath } - } - - /// Drops all incremental state (for example when the feature is turned off). - public func reset() { - tracked.removeAll() - } - - private func readAppended(from handle: FileHandle, into state: inout TrackedTranscript) { - do { - try handle.seek(toOffset: state.byteOffset) - } catch { - return + for source in AgentUsageSource.allCases { + let key = Key(path: transcriptPath, source: source) + sessions[key] = nil + keyEpochs[key, default: 0] &+= 1 } - while let chunk = try? handle.read(upToCount: chunkSize), !chunk.isEmpty { - state.byteOffset += UInt64(chunk.count) - consume(chunk, into: &state) + // Bound the bookkeeping: a global epoch bump invalidates the same + // in-flight samples a per-key bump would, and then some. + if keyEpochs.count > 1024 { + keyEpochs.removeAll() + epoch &+= 1 } } - private func consume(_ chunk: Data, into state: inout TrackedTranscript) { - var lineStart = chunk.startIndex - while let newline = chunk[lineStart...].firstIndex(of: 0x0A) { - let piece = chunk[lineStart.. maxLineBytes { - state.fragment = Data() - state.discardingOversizedLine = true - } + /// Drops all incremental state (for example when the feature is turned + /// off); samples still running are discarded. + public func reset() { + sessions.removeAll() + keyEpochs.removeAll() + epoch &+= 1 } - private func evictIfNeeded() { - while tracked.count > maxTrackedTranscripts, - let oldest = tracked.min(by: { $0.value.lastUse < $1.value.lastUse })?.key { - tracked[oldest] = nil - } + private func generation(for key: Key) -> Generation { + Generation(epoch: epoch, keyEpoch: keyEpochs[key] ?? 0) } - /// The inode of a path, or `nil` when it can't be stat'd; detects an - /// atomic replacement that size alone would miss. - private static func inode(ofPath path: String) -> UInt64? { - guard let attributes = try? FileManager.default.attributesOfItem(atPath: path), - let number = attributes[.systemFileNumber] as? NSNumber else { - return nil + private func evictIfNeeded() { + while sessions.count > maxTrackedTranscripts, + let oldest = sessions.min(by: { $0.value.lastUse < $1.value.lastUse })?.key { + sessions[oldest] = nil } - return number.uint64Value } } diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSnapshot.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSnapshot.swift index 4056aaeea7bf..6c6c88036411 100644 --- a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSnapshot.swift +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSnapshot.swift @@ -8,13 +8,15 @@ public struct AgentUsageSnapshot: Sendable, Equatable { public let modelID: String /// Short model name for display (`Opus 4.8`, `gpt-5-codex`). public let modelDisplayName: String - /// Tokens occupying the context window as of the latest request. + /// Tokens occupying the context window as of the latest main-thread + /// request. public let contextTokens: Int /// The context window size, or `nil` when unknown. public let contextWindow: Int? - /// Estimated cost of the whole session at published list prices, or - /// `nil` when the model has no price row. - public let estimatedCostUSD: Double? + /// Estimated cost of the whole session (main thread plus subagents), or + /// `nil` when it cannot be estimated: the model has no price row, or the + /// transcript was too large to read from the start. + public let estimatedCost: AgentUsageCost? /// Creates a snapshot. public init( @@ -22,13 +24,13 @@ public struct AgentUsageSnapshot: Sendable, Equatable { modelDisplayName: String, contextTokens: Int, contextWindow: Int?, - estimatedCostUSD: Double? + estimatedCost: AgentUsageCost? ) { self.modelID = modelID self.modelDisplayName = modelDisplayName self.contextTokens = contextTokens self.contextWindow = contextWindow - self.estimatedCostUSD = estimatedCostUSD + self.estimatedCost = estimatedCost } /// Fraction of the context window in use, clamped to `0...1`, or `nil` @@ -37,4 +39,15 @@ public struct AgentUsageSnapshot: Sendable, Equatable { guard let contextWindow, contextWindow > 0 else { return nil } return min(max(Double(contextTokens) / Double(contextWindow), 0), 1) } + + /// A copy with a different cost (used to fold in subagent transcripts). + public func withEstimatedCost(_ cost: AgentUsageCost?) -> Self { + Self( + modelID: modelID, + modelDisplayName: modelDisplayName, + contextTokens: contextTokens, + contextWindow: contextWindow, + estimatedCost: cost + ) + } } diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptAccumulator.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptAccumulator.swift index 11f6d028856e..8709958eb0d4 100644 --- a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptAccumulator.swift +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptAccumulator.swift @@ -1,52 +1,50 @@ import Foundation /// Folds transcript JSONL lines, in order, into running usage totals for one -/// agent session. +/// transcript file. /// /// The accumulator is incremental: feed it each complete line exactly once -/// (``ingest(line:)``), then read ``snapshot(catalog:)`` at any point. Lines -/// that carry no usage are rejected by a cheap byte search before any JSON -/// parsing. +/// (``ingest(line:)``), then read ``snapshot()`` or ``cost()`` at any point. +/// Lines that carry no usage are rejected by a cheap byte search before any +/// JSON parsing. /// -/// - Claude Code writes one `assistant` line per content block, repeating the -/// same `message.id` and `message.usage` (the final line carries the final -/// output count). Usage is therefore counted once per message id, using the -/// last line seen for it. Context size is the prompt size of the latest -/// main-chain request: `input_tokens + cache_creation_input_tokens + -/// cache_read_input_tokens`. Sidechain (subagent) lines count toward cost -/// but not the main context. -/// - Codex `token_count` events already carry cumulative totals -/// (`info.total_token_usage`), the latest request (`info.last_token_usage`), -/// and the window (`info.model_context_window`); the latest event wins. The -/// model comes from `turn_context`. +/// - Claude Code writes one `assistant` line per content block, repeating +/// the same `message.id` and `message.usage` (the last line carries the +/// final output count). Each message id is priced from the last line seen +/// for it, even when lines of different ids interleave. Context is the +/// prompt size of the latest main-chain request: `input_tokens + +/// cache_creation_input_tokens + cache_read_input_tokens`. Sidechain +/// (subagent) lines count toward cost but not the context or model. +/// - Codex `event_msg` `token_count` lines carry the latest request +/// (`info.last_token_usage`) and the window (`info.model_context_window`); +/// the latest event wins. Context excludes reasoning output tokens, which +/// Codex does not keep in the window. The model comes from `turn_context`. +/// Codex sessions are not priced. public struct AgentUsageTranscriptAccumulator: Sendable { /// Which transcript format this accumulator parses. public let source: AgentUsageSource - private var modelID: String? - - // Claude state. - private var committedMessageIDs: Set = [] - private var committedCostUSD: Double = 0 - private var hasUnpricedUsage = false - private var pendingMessageID: String? - private var pendingModelID: String? - private var pendingUsage: AgentUsageTokenCounts = .zero - private var claudeContextTokens: Int? - - // Codex state. - private var codexTotals: AgentUsageTokenCounts? - private var codexContextTokens: Int? - private var codexContextWindow: Int? - private let catalog: AgentModelCatalog + private var modelID: String? + private var contextTokens: Int? + private var contextWindow: Int? + + // Claude cost bookkeeping, per message id. + private var messageCosts: [String: Double?] = [:] + private var pricedTotal: Double = 0 + private var unpricedMessages = 0 + private var anonymousMessages = 0 + private var latestMainMessageID: String? + private var seenMainMessageIDs: Set = [] + private var historyIncomplete = false + private var droppedLines = false /// Creates an empty accumulator. /// /// - Parameters: /// - source: The transcript format. - /// - catalog: Model table used to price Claude messages as they are - /// committed (a session can switch models mid-way). + /// - catalog: Model table used to price Claude messages (a session can + /// switch models mid-way, so each message is priced by its own model). public init(source: AgentUsageSource, catalog: AgentModelCatalog = AgentModelCatalog()) { self.source = source self.catalog = catalog @@ -58,8 +56,8 @@ public struct AgentUsageTranscriptAccumulator: Sendable { /// Folds one complete JSONL line (without its trailing newline). /// - /// - Parameter line: The raw line bytes. Malformed or irrelevant lines - /// are ignored. + /// - Parameter line: The raw line bytes (a slice is fine; it is not + /// copied). Malformed or irrelevant lines are ignored. public mutating func ingest(line: Data) { switch source { case .claude: @@ -79,14 +77,54 @@ public struct AgentUsageTranscriptAccumulator: Sendable { ingest(line: Data(line.utf8)) } - /// The usage summary so far, or `nil` before any model or usage is known. + /// Records that reading started mid-file (the head was skipped to bound + /// the work), so the total cost is unknown. Model and context still + /// come from the lines that were read. + public mutating func markHistoryIncomplete() { + historyIncomplete = true + } + + /// Records that a line was skipped (too large to buffer), so the cost is + /// only a lower bound. + public mutating func markLineDropped() { + droppedLines = true + } + + /// The estimated cost of every line read so far, or `nil` when it is + /// unknowable (Codex, or the head of the file was skipped). Messages of + /// unknown models make the result a lower bound rather than hiding it. + public func cost() -> AgentUsageCost? { + guard source == .claude, !historyIncomplete else { return nil } + return AgentUsageCost( + usd: pricedTotal, + isLowerBound: unpricedMessages > 0 || droppedLines, + hasPricedUsage: messageCosts.count > unpricedMessages + ) + } + + /// The usage summary so far, or `nil` before any main-thread model is + /// known. /// /// - Returns: A snapshot combining model, context, and estimated cost. public func snapshot() -> AgentUsageSnapshot? { - switch source { - case .claude: return claudeSnapshot() - case .codex: return codexSnapshot() + guard let modelID, + let info = catalog.info(forModelID: modelID, reportedContextWindow: contextWindow) else { + return nil } + let tokens = contextTokens ?? 0 + var window = info.contextWindow + // A Claude request larger than the table's window proves a 1M window + // (for example a `[1m]` session whose model id carries no suffix). + if source == .claude, let current = window, tokens > current { + window = AgentModelCatalog.oneMillionContextWindow + } + return AgentUsageSnapshot( + modelID: modelID, + modelDisplayName: info.displayName, + contextTokens: tokens, + contextWindow: window, + estimatedCost: cost()?.displayable + ) } // MARK: Claude @@ -99,72 +137,36 @@ public struct AgentUsageTranscriptAccumulator: Sendable { let model = (message["model"] as? String).flatMap { Self.meaningfulModelID($0) } let counts = Self.claudeCounts(usage) let isSidechain = object["isSidechain"] as? Bool ?? false - let messageID = message["id"] as? String - - if let messageID, committedMessageIDs.contains(messageID) { return } - if messageID == nil || messageID != pendingMessageID { - commitPendingClaudeMessage() - pendingMessageID = messageID + let messageID: String + if let id = message["id"] as? String, !id.isEmpty { + messageID = id + } else { + anonymousMessages += 1 + messageID = "\u{0}anonymous-\(anonymousMessages)" } - pendingUsage = counts - pendingModelID = model ?? pendingModelID - if messageID == nil { - // No id to de-duplicate on: count this line on its own. - commitPendingClaudeMessage() + if counts != .zero { + let newCost = price(counts, modelID: model ?? modelID) + if let previous = messageCosts[messageID] { + if let previous { pricedTotal -= previous } else { unpricedMessages -= 1 } + } + messageCosts[messageID] = .some(newCost) + if let newCost { pricedTotal += newCost } else { unpricedMessages += 1 } } + guard !isSidechain else { return } + // A late line of an older message must not roll the context back. + let isNewMessage = seenMainMessageIDs.insert(messageID).inserted + guard isNewMessage || messageID == latestMainMessageID else { return } + latestMainMessageID = messageID if let model { modelID = model } - if counts.totalInput > 0 { claudeContextTokens = counts.totalInput } - } - - private mutating func commitPendingClaudeMessage() { - defer { - pendingMessageID = nil - pendingModelID = nil - pendingUsage = .zero - } - guard pendingUsage != .zero else { return } - if let pendingMessageID { committedMessageIDs.insert(pendingMessageID) } - if let messageCost = cost(of: pendingUsage, modelID: pendingModelID ?? modelID) { - committedCostUSD += messageCost - } else { - hasUnpricedUsage = true - } + if counts.totalInput > 0 { contextTokens = counts.totalInput } } - private func cost(of counts: AgentUsageTokenCounts, modelID: String?) -> Double? { + private func price(_ counts: AgentUsageTokenCounts, modelID: String?) -> Double? { guard let modelID, let pricing = catalog.info(forModelID: modelID)?.pricing else { return nil } return pricing.estimatedCostUSD(for: counts) } - private func claudeSnapshot() -> AgentUsageSnapshot? { - guard let modelID, let info = catalog.info(forModelID: modelID) else { return nil } - let contextTokens = claudeContextTokens ?? 0 - var window = info.contextWindow - // A request larger than the table's window proves a 1M window - // (for example a `[1m]` model whose id carries no suffix). - if let current = window, contextTokens > current { - window = AgentModelCatalog.oneMillionContextWindow - } - var cost: Double? = committedCostUSD - var unpriced = hasUnpricedUsage - if pendingUsage != .zero { - if let pendingCost = self.cost(of: pendingUsage, modelID: pendingModelID ?? modelID) { - cost = committedCostUSD + pendingCost - } else { - unpriced = true - } - } - if unpriced { cost = nil } - return AgentUsageSnapshot( - modelID: modelID, - modelDisplayName: info.displayName, - contextTokens: contextTokens, - contextWindow: window, - estimatedCostUSD: cost - ) - } - private static func claudeCounts(_ usage: [String: Any]) -> AgentUsageTokenCounts { let cacheCreation = int(usage["cache_creation_input_tokens"]) var write5m = cacheCreation @@ -199,52 +201,18 @@ public struct AgentUsageTranscriptAccumulator: Sendable { case "event_msg": guard payload["type"] as? String == "token_count", let info = payload["info"] as? [String: Any] else { return } - if let total = info["total_token_usage"] as? [String: Any] { - codexTotals = Self.codexCounts(total) - } if let last = info["last_token_usage"] as? [String: Any] { let total = Self.int(last["total_tokens"]) - codexContextTokens = total > 0 - ? total - : Self.int(last["input_tokens"]) + Self.int(last["output_tokens"]) + let all = total > 0 ? total : Self.int(last["input_tokens"]) + Self.int(last["output_tokens"]) + contextTokens = max(0, all - Self.int(last["reasoning_output_tokens"])) } let window = Self.int(info["model_context_window"]) - if window > 0 { codexContextWindow = window } + if window > 0 { contextWindow = window } default: return } } - private func codexSnapshot() -> AgentUsageSnapshot? { - guard let modelID, - let info = catalog.info(forModelID: modelID, reportedContextWindow: codexContextWindow) else { - return nil - } - return AgentUsageSnapshot( - modelID: modelID, - modelDisplayName: info.displayName, - contextTokens: codexContextTokens ?? 0, - contextWindow: info.contextWindow, - estimatedCostUSD: codexTotals.flatMap { totals in - info.pricing.map { $0.estimatedCostUSD(for: totals) } - } - ) - } - - /// Codex/OpenAI `input_tokens` includes cached input; split it so cached - /// tokens are priced at the cached rate only once. - private static func codexCounts(_ usage: [String: Any]) -> AgentUsageTokenCounts { - let input = int(usage["input_tokens"]) - let cached = int(usage["cached_input_tokens"]) - let cacheWrite = int(usage["cache_write_input_tokens"]) - return AgentUsageTokenCounts( - uncachedInput: max(0, input - cached - cacheWrite), - cacheWrite5m: cacheWrite, - cacheRead: cached, - output: int(usage["output_tokens"]) - ) - } - // MARK: Helpers private static func jsonObject(_ line: Data) -> [String: Any]? { diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptReader.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptReader.swift new file mode 100644 index 000000000000..0a7460d24a30 --- /dev/null +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptReader.swift @@ -0,0 +1,147 @@ +import Foundation + +/// Reads agent transcripts incrementally for ``AgentUsageSampler``. +/// +/// Blocking file reads happen here, inside a detached task the sampler +/// starts per transcript, so one large file never holds the sampler actor. +/// A file larger than ``fullScanLimit`` on first sight is read only from its +/// last ``tailBytes``: model and context come from that tail, and the cost is +/// reported as unknown rather than as a misleading partial sum. +struct AgentUsageTranscriptReader: Sendable { + /// Per-session cursors: the main transcript and, for Claude Code, each + /// subagent transcript in `/subagents/agent-*.jsonl`. + struct SessionCursor: Sendable { + var main: AgentUsageFileCursor? + var subagents: [String: AgentUsageFileCursor] = [:] + var lastUse: UInt64 = 0 + } + + let chunkSize: Int + let maxLineBytes: Int + let fullScanLimit: UInt64 + let tailBytes: UInt64 + let catalog: AgentModelCatalog + private static let headLength = 256 + + init( + chunkSize: Int = 1 << 20, + maxLineBytes: Int = 16 << 20, + fullScanLimit: UInt64 = 24 << 20, + tailBytes: UInt64 = 4 << 20, + catalog: AgentModelCatalog = AgentModelCatalog() + ) { + self.chunkSize = max(1, chunkSize) + self.maxLineBytes = max(1, maxLineBytes) + self.fullScanLimit = fullScanLimit + self.tailBytes = min(max(1, tailBytes), max(1, fullScanLimit)) + self.catalog = catalog + } + + /// Advances every file of one session and returns the combined usage. + /// + /// Subagent transcripts add to the cost only; model and context always + /// describe the main thread. + func advanceSession( + _ cursor: SessionCursor, + path: String, + source: AgentUsageSource + ) async -> (SessionCursor, AgentUsageSnapshot?) { + var session = cursor + session.main = await advance(session.main, path: path, source: source) + guard let main = session.main else { return (SessionCursor(), nil) } + guard source == .claude, let snapshot = main.accumulator.snapshot() else { + return (session, main.accumulator.snapshot()) + } + let directory = Self.subagentsDirectory(forTranscriptPath: path) + let names = ((try? FileManager.default.contentsOfDirectory(atPath: directory)) ?? []) + .filter { $0.hasPrefix("agent-") && $0.hasSuffix(".jsonl") } + .sorted() + var cost = main.accumulator.cost() + var subagents: [String: AgentUsageFileCursor] = [:] + for name in names { + let subPath = (directory as NSString).appendingPathComponent(name) + guard let sub = await advance(session.subagents[name], path: subPath, source: .claude) else { continue } + subagents[name] = sub + cost = AgentUsageCost.combine(cost, sub.accumulator.cost()) + } + session.subagents = subagents + return (session, snapshot.withEstimatedCost(cost?.displayable)) + } + + /// `/.jsonl` → `//subagents` (Claude Code). + static func subagentsDirectory(forTranscriptPath path: String) -> String { + ((path as NSString).deletingPathExtension as NSString).appendingPathComponent("subagents") + } + + /// Reads whatever was appended to one file since `cursor`, restarting + /// when the file was truncated or replaced. Returns `nil` when the file + /// cannot be opened. + func advance( + _ cursor: AgentUsageFileCursor?, + path: String, + source: AgentUsageSource + ) async -> AgentUsageFileCursor? { + guard let handle = FileHandle(forReadingAtPath: path) else { return nil } + defer { try? handle.close() } + var info = stat() + guard fstat(handle.fileDescriptor, &info) == 0 else { return nil } + let size = UInt64(max(0, info.st_size)) + let inode = UInt64(info.st_ino) + let head = (try? handle.read(upToCount: Self.headLength)) ?? Data() + + var state: AgentUsageFileCursor + if var existing = cursor, + existing.inode == inode, + size >= existing.byteOffset, + head.starts(with: existing.head) { + existing.head = head + state = existing + } else { + state = AgentUsageFileCursor(source: source, catalog: catalog) + state.inode = inode + state.head = head + if size > fullScanLimit { + state.byteOffset = size - tailBytes + state.discardingLine = true + state.accumulator.markHistoryIncomplete() + } + } + guard size > state.byteOffset else { return state } + do { + try handle.seek(toOffset: state.byteOffset) + } catch { + return state + } + while let chunk = try? handle.read(upToCount: chunkSize), !chunk.isEmpty { + state.byteOffset += UInt64(chunk.count) + consume(chunk, into: &state) + // Let other transcripts' reads interleave on the cooperative pool. + await Task.yield() + } + return state + } + + private func consume(_ chunk: Data, into state: inout AgentUsageFileCursor) { + var lineStart = chunk.startIndex + while let newline = chunk[lineStart...].firstIndex(of: 0x0A) { + let piece = chunk[lineStart.. maxLineBytes { + state.fragment = Data() + state.discardingLine = true + state.accumulator.markLineDropped() + } + } +} diff --git a/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentModelCatalogTests.swift b/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentModelCatalogTests.swift index 945d38fde12d..aedd6e968265 100644 --- a/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentModelCatalogTests.swift +++ b/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentModelCatalogTests.swift @@ -11,6 +11,7 @@ struct AgentModelCatalogTests { ("claude-opus-4-8", "Opus 4.8", 1_000_000), ("claude-opus-5-5", "Opus 5.5", 1_000_000), ("claude-haiku-4-5-20251001", "Haiku 4.5", 200_000), + ("claude-haiku-4-5@20251001", "Haiku 4.5", 200_000), ("claude-sonnet-4-5-20250929", "Sonnet 4.5", 200_000), ("claude-sonnet-4-5-20250929[1m]", "Sonnet 4.5", 1_000_000), ("us.anthropic.claude-opus-4-1-20250805-v1:0", "Opus 4.1", 200_000), @@ -27,16 +28,16 @@ struct AgentModelCatalogTests { let opus48 = try #require(catalog.info(forModelID: "claude-opus-4-8")?.pricing) #expect(opus4.inputPerMTok == 15) #expect(opus48.inputPerMTok == 5) - #expect(catalog.info(forModelID: "gpt-5.2")?.pricing == nil) - #expect(catalog.info(forModelID: "gpt-5")?.pricing?.inputPerMTok == 1.25) } - @Test func legacyAndUnknownIDsFallBackToRawName() throws { + @Test func nonClaudeAndLegacyIDsAreUnpriced() throws { let legacy = try #require(catalog.info(forModelID: "claude-3-5-sonnet-20241022")) #expect(legacy.displayName == "claude-3-5-sonnet-20241022") #expect(legacy.pricing == nil) - let codex = try #require(catalog.info(forModelID: "gpt-6-astra")) - #expect(codex.contextWindow == nil) + let codex = try #require(catalog.info(forModelID: "gpt-5-codex", reportedContextWindow: 272_000)) + #expect(codex.pricing == nil) + #expect(codex.contextWindow == 272_000) + #expect(catalog.info(forModelID: "gpt-6-astra")?.contextWindow == nil) #expect(catalog.info(forModelID: " ") == nil) } @@ -51,10 +52,18 @@ struct AgentModelCatalogTests { #expect(pricing.estimatedCostUSD(for: .zero) == 0) } + @Test func costCombinationPropagatesUnknownAndLowerBound() { + let exact = AgentUsageCost(usd: 1, isLowerBound: false) + let partial = AgentUsageCost(usd: 2, isLowerBound: true) + #expect(AgentUsageCost.combine(exact, partial) == AgentUsageCost(usd: 3, isLowerBound: true)) + #expect(AgentUsageCost.combine(exact, nil) == nil) + #expect(AgentUsageCost(usd: 0, isLowerBound: true, hasPricedUsage: false).displayable == nil) + } + @Test func snapshotFractionClampsAndHandlesUnknownWindow() { - let over = AgentUsageSnapshot(modelID: "m", modelDisplayName: "m", contextTokens: 300, contextWindow: 200, estimatedCostUSD: nil) + let over = AgentUsageSnapshot(modelID: "m", modelDisplayName: "m", contextTokens: 300, contextWindow: 200, estimatedCost: nil) #expect(over.contextFraction == 1) - let unknown = AgentUsageSnapshot(modelID: "m", modelDisplayName: "m", contextTokens: 300, contextWindow: nil, estimatedCostUSD: nil) + let unknown = AgentUsageSnapshot(modelID: "m", modelDisplayName: "m", contextTokens: 300, contextWindow: nil, estimatedCost: nil) #expect(unknown.contextFraction == nil) } } diff --git a/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageFixtures.swift b/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageFixtures.swift index ba992cd043a5..e258a24ee20e 100644 --- a/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageFixtures.swift +++ b/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageFixtures.swift @@ -68,6 +68,7 @@ struct AgentUsageFixtures { totalCached: Int, totalOutput: Int, lastTotal: Int, + lastReasoning: Int = 0, window: Int ) -> String { json([ @@ -88,6 +89,7 @@ struct AgentUsageFixtures { "input_tokens": lastTotal - 100, "cached_input_tokens": 0, "output_tokens": 100, + "reasoning_output_tokens": lastReasoning, "total_tokens": lastTotal, ], "model_context_window": window, diff --git a/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageSamplerTests.swift b/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageSamplerTests.swift index 51cb01620547..45b8e07c1fb2 100644 --- a/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageSamplerTests.swift +++ b/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageSamplerTests.swift @@ -7,9 +7,10 @@ import Testing struct AgentUsageSamplerTests { private typealias Fixture = AgentUsageFixtures - private func makeTranscriptURL() -> URL { - FileManager.default.temporaryDirectory - .appendingPathComponent("agent-usage-\(UUID().uuidString).jsonl") + private func makeDirectory() throws -> URL { + let url = FileManager.default.temporaryDirectory.appendingPathComponent("agent-usage-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true) + return url } private func append(_ text: String, to url: URL) throws { @@ -20,14 +21,15 @@ struct AgentUsageSamplerTests { } @Test func readsIncrementallyAcrossPartialLines() async throws { - let url = makeTranscriptURL() - defer { try? FileManager.default.removeItem(at: url) } + let directory = try makeDirectory() + defer { try? FileManager.default.removeItem(at: directory) } + let url = directory.appendingPathComponent("s.jsonl") let first = Fixture.claudeAssistant(id: "msg_a", input: 10, cacheRead: 1000, output: 1) let second = Fixture.claudeAssistant(id: "msg_b", input: 20, cacheRead: 2000, output: 2) let splitIndex = second.index(second.startIndex, offsetBy: second.count / 2) try Data((first + "\n" + second[.. WorkspaceSidebarMetadataModel? - private struct PendingSample { - let transcriptPath: String + private struct SessionRecord { let source: AgentUsageSource + var workspaceID: UUID + var transcriptPath: String? + var lastEventAt: Date + /// Bumped on `SessionStart`, so a sample started for the previous + /// run of a resumed session id is discarded. + var epoch: UInt64 + var snapshot: AgentUsageSnapshot? + } + + private struct RowKey: Hashable { let workspaceID: UUID + let source: AgentUsageSource } + /// Records kept for idle sessions; the least recently active is dropped. + static let maxTrackedSessions = 64 + private let sampler: AgentUsageSampler private let defaults: UserDefaults private let coalesceInterval: Duration private let clock: any Clock private let metadataLookup: MetadataLookup - private var pendingBySessionID: [String: PendingSample] = [:] + private var sessions: [String: SessionRecord] = [:] private var flushTasks: [String: Task] = [:] - private var workspaceIDsShowingUsage: Set = [] - private var observationTask: Task? + private var dirtyWhileReading: Set = [] + private var readingSessionIDs: Set = [] + private var rowsShowingUsage: Set = [] + private var epochCounter: UInt64 = 0 + private var lastKnownEnabled: Bool + private var observationTasks: [Task] = [] /// Creates a coordinator. /// /// - Parameters: /// - sampler: Off-main transcript sampler. - /// - defaults: Settings store for `sidebar.showAgentUsage` and - /// `sidebar.hideAllDetails`. + /// - defaults: Settings store for `sidebar.showAgentUsage`, + /// `sidebar.showCustomMetadata` and `sidebar.hideAllDetails`. /// - coalesceInterval: Delay that folds a burst of hook events (tool /// storms) into one transcript read per session. /// - clock: Clock driving the coalescing delay (tests inject their own). @@ -57,64 +82,113 @@ final class SidebarAgentUsageCoordinator { self.coalesceInterval = coalesceInterval self.clock = clock self.metadataLookup = metadataLookup + lastKnownEnabled = Self.isEnabled(defaults: defaults) } - /// Starts consuming accepted agent hook events for the app's lifetime. - /// - /// The observation task holds the coordinator, so the composition root - /// can start it without storing it. Idempotent. + /// Starts consuming accepted agent hook events and settings changes for + /// the app's lifetime. The observation tasks hold the coordinator, so + /// the composition root can start it without storing it. Idempotent. func start() { - guard observationTask == nil else { return } - observationTask = Task { @MainActor in + guard observationTasks.isEmpty else { return } + observationTasks.append(Task { @MainActor in for await notification in NotificationCenter.default.notifications(named: .workstreamEventReceived) { guard !Task.isCancelled else { return } guard let event = notification.object as? WorkstreamEvent else { continue } self.noteHookEvent(event) } - } + }) + observationTasks.append(Task { @MainActor in + for await _ in NotificationCenter.default.notifications(named: UserDefaults.didChangeNotification) { + guard !Task.isCancelled else { return } + self.settingsDidChange() + } + }) } - /// Whether usage is currently displayed (and therefore sampled). + /// Whether usage is displayed, and therefore sampled. var isEnabled: Bool { - SidebarWorkspaceDetailDefaults.auxiliaryDetailVisibility(defaults: defaults).showsAgentUsage + Self.isEnabled(defaults: defaults) } - /// Records one accepted hook event and schedules a coalesced sample of - /// its session transcript. + private static func isEnabled(defaults: UserDefaults) -> Bool { + let visibility = SidebarWorkspaceDetailDefaults.auxiliaryDetailVisibility(defaults: defaults) + // Usage renders inside the metadata rows; no rows, no reads. + return visibility.showsAgentUsage && visibility.showsMetadata + } + + /// Reacts to a settings change: samples every known session when usage + /// becomes visible, clears everything when it stops being visible. + func settingsDidChange() { + let enabled = isEnabled + guard enabled != lastKnownEnabled else { return } + lastKnownEnabled = enabled + if enabled { + for sessionID in sessions.keys { scheduleFlush(sessionID: sessionID) } + } else { + clearAll() + } + } + + /// Records one accepted hook event and, while enabled, schedules a + /// coalesced sample of its session transcript. /// /// - Parameter event: The hook event as accepted by the feed pipeline. func noteHookEvent(_ event: WorkstreamEvent) { guard let source = AgentUsageSource(hookSource: event.source), let workspaceID = event.workspaceId.flatMap(UUID.init(uuidString:)) else { return } - guard isEnabled else { - clearAll() + let sessionID = event.sessionId + if event.hookEventName == .sessionEnd { + endSession(sessionID) return } - switch event.hookEventName { - case .sessionStart, .sessionEnd: - // A new or finished session must not keep the previous one's numbers. - metadataLookup(workspaceID)?.updateAgentUsage(nil, forStatusKey: source.sidebarStatusKey) - if event.hookEventName == .sessionEnd { - cancelPending(sessionID: event.sessionId) - if let path = event.transcriptPath { - Task { [sampler] in await sampler.forget(transcriptPath: path) } - } - return - } - default: - break - } - guard let transcriptPath = event.transcriptPath, !transcriptPath.isEmpty else { return } - pendingBySessionID[event.sessionId] = PendingSample( - transcriptPath: transcriptPath, + var record = sessions[sessionID] ?? SessionRecord( source: source, - workspaceID: workspaceID + workspaceID: workspaceID, + transcriptPath: nil, + lastEventAt: event.receivedAt, + epoch: 0, + snapshot: nil ) - scheduleFlush(sessionID: event.sessionId) + if event.hookEventName == .sessionStart || sessions[sessionID] == nil { + // A new or resumed run must not show the previous run's numbers. + epochCounter &+= 1 + record.epoch = epochCounter + record.snapshot = nil + } + let previousWorkspaceID = record.workspaceID + record.workspaceID = workspaceID + record.lastEventAt = max(record.lastEventAt, event.receivedAt) + if let path = event.transcriptPath, !path.isEmpty { + record.transcriptPath = path + } + sessions[sessionID] = record + evictIfNeeded() + publish(RowKey(workspaceID: workspaceID, source: source)) + if previousWorkspaceID != workspaceID { + publish(RowKey(workspaceID: previousWorkspaceID, source: source)) + } + if isEnabled, record.transcriptPath != nil { + scheduleFlush(sessionID: sessionID) + } + } + + private func endSession(_ sessionID: String) { + flushTasks.removeValue(forKey: sessionID)?.cancel() + dirtyWhileReading.remove(sessionID) + readingSessionIDs.remove(sessionID) + guard let record = sessions.removeValue(forKey: sessionID) else { return } + if let path = record.transcriptPath { + // Also discards a sample of this transcript that is still running. + Task { [sampler] in await sampler.forget(transcriptPath: path) } + } + publish(RowKey(workspaceID: record.workspaceID, source: record.source)) } private func scheduleFlush(sessionID: String) { - guard flushTasks[sessionID] == nil else { return } + guard flushTasks[sessionID] == nil else { + if readingSessionIDs.contains(sessionID) { dirtyWhileReading.insert(sessionID) } + return + } flushTasks[sessionID] = Task { @MainActor [weak self, clock, coalesceInterval] in do { // Bounded, cancellable coalescing delay (not a poll). @@ -132,46 +206,77 @@ final class SidebarAgentUsageCoordinator { // A cancelled flush was already removed by whoever cancelled it. if !Task.isCancelled { flushTasks[sessionID] = nil - // Events that arrived while sampling get their own pass. - if pendingBySessionID[sessionID] != nil { scheduleFlush(sessionID: sessionID) } + readingSessionIDs.remove(sessionID) + // Events that arrived while reading get their own pass. + if dirtyWhileReading.remove(sessionID) != nil { scheduleFlush(sessionID: sessionID) } } } - guard let pending = pendingBySessionID.removeValue(forKey: sessionID), isEnabled else { return } - let snapshot = await sampler.sample(transcriptPath: pending.transcriptPath, source: pending.source) - // The setting may have flipped, or the session ended, while sampling. - guard isEnabled, !Task.isCancelled, - let metadata = metadataLookup(pending.workspaceID) else { return } - metadata.updateAgentUsage(snapshot.map(Self.sidebarUsage), forStatusKey: pending.source.sidebarStatusKey) - if snapshot != nil { - workspaceIDsShowingUsage.insert(pending.workspaceID) + guard isEnabled, let record = sessions[sessionID], let path = record.transcriptPath else { return } + readingSessionIDs.insert(sessionID) + let snapshot = await sampler.sample(transcriptPath: path, source: record.source) + // The setting may have flipped, or the session ended or restarted, + // while sampling; `nil` means "nothing new" and keeps what is shown. + guard !Task.isCancelled, isEnabled, let snapshot, + var current = sessions[sessionID], + current.epoch == record.epoch, + current.transcriptPath == path else { return } + current.snapshot = snapshot + sessions[sessionID] = current + publish(RowKey(workspaceID: current.workspaceID, source: current.source)) + } + + /// Shows the usage of the most recently active session of `row.source` + /// in `row.workspaceID` (see the type documentation). + private func publish(_ row: RowKey) { + let owner = sessions.values + .filter { $0.workspaceID == row.workspaceID && $0.source == row.source } + .max { $0.lastEventAt < $1.lastEventAt } + let usage = isEnabled ? owner?.snapshot.map(Self.sidebarUsage) : nil + if usage == nil, !rowsShowingUsage.contains(row) { return } + metadataLookup(row.workspaceID)?.updateAgentUsage(usage, forStatusKey: row.source.sidebarStatusKey) + if usage == nil { + rowsShowingUsage.remove(row) + } else { + rowsShowingUsage.insert(row) } } - private func cancelPending(sessionID: String) { - flushTasks.removeValue(forKey: sessionID)?.cancel() - pendingBySessionID[sessionID] = nil + private func evictIfNeeded() { + while sessions.count > Self.maxTrackedSessions, + let oldest = sessions.min(by: { $0.value.lastEventAt < $1.value.lastEventAt })?.key { + endSession(oldest) + } } private func clearAll() { for task in flushTasks.values { task.cancel() } flushTasks.removeAll() - pendingBySessionID.removeAll() - guard !workspaceIDsShowingUsage.isEmpty else { return } - for workspaceID in workspaceIDsShowingUsage { - guard let metadata = metadataLookup(workspaceID) else { continue } - for source in AgentUsageSource.allCases { - metadata.updateAgentUsage(nil, forStatusKey: source.sidebarStatusKey) - } + dirtyWhileReading.removeAll() + readingSessionIDs.removeAll() + for sessionID in Array(sessions.keys) { + sessions[sessionID]?.snapshot = nil } - workspaceIDsShowingUsage.removeAll() + for row in rowsShowingUsage { + metadataLookup(row.workspaceID)?.updateAgentUsage(nil, forStatusKey: row.source.sidebarStatusKey) + } + rowsShowingUsage.removeAll() Task { [sampler] in await sampler.reset() } } + /// Waits until no sample is scheduled or running (tests use this to + /// observe the result of a coalesced sample deterministically). + func waitUntilIdle() async { + while let task = flushTasks.values.first { + await task.value + } + } + static func sidebarUsage(_ snapshot: AgentUsageSnapshot) -> SidebarAgentUsage { SidebarAgentUsage( modelName: snapshot.modelDisplayName, contextFraction: snapshot.contextFraction, - estimatedCostUSD: snapshot.estimatedCostUSD + estimatedCostUSD: snapshot.estimatedCost?.usd, + costIsLowerBound: snapshot.estimatedCost?.isLowerBound ?? false ) } } diff --git a/Sources/Sidebar/SidebarAgentUsageFormatter.swift b/Sources/Sidebar/SidebarAgentUsageFormatter.swift index b57a0866436f..f1f2e341bec3 100644 --- a/Sources/Sidebar/SidebarAgentUsageFormatter.swift +++ b/Sources/Sidebar/SidebarAgentUsageFormatter.swift @@ -5,8 +5,9 @@ import Foundation /// agent status entry, e.g. `Running · Opus 4.8 · 42% · ~$1.20`. /// /// The percentage is the share of the model's context window in use; the -/// dollar figure is an estimate at published API list prices and is always -/// prefixed with the localized "estimated" marker. +/// dollar figure is an estimate at published API list prices, prefixed with +/// the localized "estimated" marker, suffixed with `+` when some usage could +/// not be priced, and explained in the entry's tooltip. struct SidebarAgentUsageFormatter { /// Separator between the status text and each usage component. static let separator = " · " @@ -19,6 +20,14 @@ struct SidebarAgentUsageFormatter { self.locale = locale } + /// Tooltip for an entry that shows an estimated cost. + static var costHelpText: String { + String( + localized: "sidebar.agentUsage.costHelp", + defaultValue: "Cost is an API list-price estimate, not your subscription bill." + ) + } + /// The compact usage summary, e.g. `Opus 4.8 · 42% · ~$1.20`. /// /// Components that are unknown (context window, price) are omitted. @@ -29,8 +38,10 @@ struct SidebarAgentUsageFormatter { } if let cost = usage.estimatedCostUSD { let costText = cost.formatted(.currency(code: "USD").precision(.fractionLength(2)).locale(locale)) - // The tilde marks the figure as an estimate, not a bill. - parts.append(String(localized: "sidebar.agentUsage.estimatedCost", defaultValue: "~\(costText)")) + // The tilde marks the figure as an estimate, the plus a lower bound. + parts.append(usage.costIsLowerBound + ? String(localized: "sidebar.agentUsage.estimatedCostAtLeast", defaultValue: "~\(costText)+") + : String(localized: "sidebar.agentUsage.estimatedCost", defaultValue: "~\(costText)")) } return parts.joined(separator: Self.separator) } @@ -57,7 +68,8 @@ struct SidebarAgentUsageFormatter { url: entry.url, priority: entry.priority, format: entry.format, - timestamp: entry.timestamp + timestamp: entry.timestamp, + helpText: usage.estimatedCostUSD == nil ? entry.helpText : Self.costHelpText ) } } diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj index db33dca4ad34..cb0e16528ed0 100644 --- a/cmux.xcodeproj/project.pbxproj +++ b/cmux.xcodeproj/project.pbxproj @@ -2954,6 +2954,7 @@ B804A0320000000000000032 /* SidebarRowTaskStatusGlyphButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B804B0320000000000000032 /* SidebarRowTaskStatusGlyphButton.swift */; }; B804A03A000000000000003A /* SidebarRowTextAccessibilityLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B804B03A000000000000003A /* SidebarRowTextAccessibilityLink.swift */; }; 5CDA4D369D5728DCD6B39BEF /* SidebarRowTextLinkLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 28782D804EEE07F5CC3E872C /* SidebarRowTextLinkLayout.swift */; }; + A6AC7E530000000000000001 /* SidebarRowTextView+PlainText.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6AC7E530000000000000002 /* SidebarRowTextView+PlainText.swift */; }; A6D6A6767F0FE4BAD2BF41DA /* SidebarRowTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A11C0657D4156EAAAA586FF /* SidebarRowTextView.swift */; }; C0DE35010000000000000001 /* SidebarScrim.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0DE35010000000000000002 /* SidebarScrim.swift */; }; C9A57513C9A57513C9A57513 /* SidebarScrollViewConfiguratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57514C9A57514C9A57514 /* SidebarScrollViewConfiguratorTests.swift */; }; @@ -6917,6 +6918,7 @@ B804B0320000000000000032 /* SidebarRowTaskStatusGlyphButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/AppKitList/Cells/SidebarRowTaskStatusGlyphButton.swift; sourceTree = ""; }; B804B03A000000000000003A /* SidebarRowTextAccessibilityLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/AppKitList/Cells/SidebarRowTextAccessibilityLink.swift; sourceTree = ""; }; 28782D804EEE07F5CC3E872C /* SidebarRowTextLinkLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/AppKitList/Cells/SidebarRowTextLinkLayout.swift; sourceTree = ""; }; + A6AC7E530000000000000002 /* SidebarRowTextView+PlainText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sidebar/AppKitList/Cells/SidebarRowTextView+PlainText.swift"; sourceTree = ""; }; 0A11C0657D4156EAAAA586FF /* SidebarRowTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar/AppKitList/Cells/SidebarRowTextView.swift; sourceTree = ""; }; C0DE35010000000000000002 /* SidebarScrim.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarScrim.swift; sourceTree = ""; }; C9A57514C9A57514C9A57514 /* SidebarScrollViewConfiguratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarScrollViewConfiguratorTests.swift; sourceTree = ""; }; @@ -9049,6 +9051,7 @@ B804B0240000000000000024 /* SidebarWorkspaceRowCellView.swift */, B804B0250000000000000025 /* SidebarWorkspaceRowSlotViews.swift */, 0A11C0657D4156EAAAA586FF /* SidebarRowTextView.swift */, + A6AC7E530000000000000002 /* SidebarRowTextView+PlainText.swift */, 28782D804EEE07F5CC3E872C /* SidebarRowTextLinkLayout.swift */, B804B03A000000000000003A /* SidebarRowTextAccessibilityLink.swift */, B804B0260000000000000026 /* SidebarWorkspaceRowSupportViews.swift */, @@ -14488,6 +14491,7 @@ B804A0320000000000000032 /* SidebarRowTaskStatusGlyphButton.swift in Sources */, B804A03A000000000000003A /* SidebarRowTextAccessibilityLink.swift in Sources */, 5CDA4D369D5728DCD6B39BEF /* SidebarRowTextLinkLayout.swift in Sources */, + A6AC7E530000000000000001 /* SidebarRowTextView+PlainText.swift in Sources */, A6D6A6767F0FE4BAD2BF41DA /* SidebarRowTextView.swift in Sources */, C0DE35010000000000000001 /* SidebarScrim.swift in Sources */, 68EA24C78ED1459C99DEFCB6 /* SidebarSearchField.swift in Sources */, diff --git a/cmuxTests/SidebarAgentUsageTests.swift b/cmuxTests/SidebarAgentUsageTests.swift index 8eb175a5de76..0bd9b4bd3736 100644 --- a/cmuxTests/SidebarAgentUsageTests.swift +++ b/cmuxTests/SidebarAgentUsageTests.swift @@ -1,4 +1,5 @@ import CMUXAgentLaunch +import CmuxAgentChat import CmuxSettings import CmuxSidebar import Foundation @@ -14,17 +15,51 @@ private struct UnlimitedSidebarLog: SidebarLogEntryLimitProviding { struct SidebarAgentUsageTests { private static let usage = SidebarAgentUsage(modelName: "Opus 4.8", contextFraction: 0.4213, estimatedCostUSD: 1.2) - private static func makeDefaults() -> UserDefaults { - UserDefaults(suiteName: "SidebarAgentUsageTests.\(UUID().uuidString)")! + private static func makeDefaults(enabled: Bool) -> UserDefaults { + let defaults = UserDefaults(suiteName: "SidebarAgentUsageTests.\(UUID().uuidString)")! + defaults.set(enabled, forKey: SidebarWorkspaceDetailDefaults.showAgentUsageKey) + return defaults } private static func statusEntry(key: String, value: String) -> SidebarStatusEntry { SidebarStatusEntry(key: key, value: value, icon: "bolt.fill", color: "#4C8DFF", priority: 5, timestamp: Date(timeIntervalSince1970: 10)) } + private static func claudeTranscript(contextTokens: Int, in directory: URL, name: String) throws -> String { + let line = """ + {"type":"assistant","isSidechain":false,"message":{"id":"msg_\(name)","model":"claude-opus-4-8","usage":{"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":\(contextTokens),"output_tokens":1}}} + + """ + let url = directory.appendingPathComponent("\(name).jsonl") + try Data(line.utf8).write(to: url) + return url.path + } + + private static func event( + _ name: WorkstreamEvent.HookEventName, + session: String, + workspace: UUID, + transcript: String?, + at seconds: TimeInterval, + source: String = "claude" + ) -> WorkstreamEvent { + WorkstreamEvent( + sessionId: session, + hookEventName: name, + source: source, + workspaceId: workspace.uuidString, + transcriptPath: transcript, + receivedAt: Date(timeIntervalSince1970: seconds) + ) + } + + // MARK: Formatting + @Test func summaryShowsModelContextPercentAndEstimatedCost() { let formatter = SidebarAgentUsageFormatter(locale: Locale(identifier: "en_US")) #expect(formatter.summary(for: Self.usage) == "Opus 4.8 · 42% · ~$1.20") + let partial = SidebarAgentUsage(modelName: "Opus 4.8", contextFraction: nil, estimatedCostUSD: 1.2, costIsLowerBound: true) + #expect(formatter.summary(for: partial) == "Opus 4.8 · ~$1.20+") } @Test func summaryOmitsUnknownWindowAndPrice() { @@ -33,7 +68,7 @@ struct SidebarAgentUsageTests { #expect(formatter.summary(for: usage) == "gpt-6-astra") } - @Test func decorationExtendsOnlyTheMatchingAgentEntry() { + @Test func decorationExtendsOnlyTheMatchingAgentEntryAndExplainsTheCost() { let formatter = SidebarAgentUsageFormatter(locale: Locale(identifier: "en_US")) let claude = Self.statusEntry(key: "claude_code", value: "Running") let other = Self.statusEntry(key: "build", value: "Passing") @@ -41,6 +76,8 @@ struct SidebarAgentUsageTests { let decorated = formatter.decorate([claude, other], usageByStatusKey: ["claude_code": Self.usage]) #expect(decorated[0].value == "Running · Opus 4.8 · 42% · ~$1.20") + #expect(decorated[0].helpText == SidebarAgentUsageFormatter.costHelpText) + #expect(decorated[0].sidebarHelpText.hasSuffix(SidebarAgentUsageFormatter.costHelpText)) #expect(decorated[0].icon == claude.icon) #expect(decorated[0].color == claude.color) #expect(decorated[0].priority == claude.priority) @@ -49,7 +86,7 @@ struct SidebarAgentUsageTests { } @Test func settingIsOffByDefaultAndHiddenByHideAllDetails() { - let defaults = Self.makeDefaults() + let defaults = UserDefaults(suiteName: "SidebarAgentUsageTests.\(UUID().uuidString)")! #expect(!SidebarTabItemSettingsSnapshot(defaults: defaults).visibleAuxiliaryDetails.showsAgentUsage) defaults.set(true, forKey: SidebarWorkspaceDetailDefaults.showAgentUsageKey) @@ -59,34 +96,70 @@ struct SidebarAgentUsageTests { #expect(!SidebarTabItemSettingsSnapshot(defaults: defaults).visibleAuxiliaryDetails.showsAgentUsage) } - @Test func sessionStartClearsUsageAndOtherAgentsAreIgnored() { - let defaults = Self.makeDefaults() + // MARK: Coordinator + + @Test func rowShowsTheMostRecentlyActiveSessionAndPanesDoNotClearEachOther() async throws { + let directory = FileManager.default.temporaryDirectory.appendingPathComponent("usage-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: directory) } + let pathA = try Self.claudeTranscript(contextTokens: 100_000, in: directory, name: "a") + let pathB = try Self.claudeTranscript(contextTokens: 300_000, in: directory, name: "b") + let workspaceID = UUID() + let metadata = WorkspaceSidebarMetadataModel(limitProvider: UnlimitedSidebarLog()) + let coordinator = SidebarAgentUsageCoordinator( + defaults: Self.makeDefaults(enabled: true), + coalesceInterval: .zero + ) { id in id == workspaceID ? metadata : nil } + func shownFraction() -> Double? { metadata.agentUsageByStatusKey["claude_code"]?.contextFraction } + + coordinator.noteHookEvent(Self.event(.preToolUse, session: "A", workspace: workspaceID, transcript: pathA, at: 1)) + await coordinator.waitUntilIdle() + #expect(shownFraction() == 0.1) + + // Pane B starts: the row follows B, and A's record survives. + coordinator.noteHookEvent(Self.event(.sessionStart, session: "B", workspace: workspaceID, transcript: pathB, at: 2)) + await coordinator.waitUntilIdle() + #expect(shownFraction() == 0.3) + + coordinator.noteHookEvent(Self.event(.postToolUse, session: "A", workspace: workspaceID, transcript: pathA, at: 3)) + await coordinator.waitUntilIdle() + #expect(shownFraction() == 0.1) + + // B ending does not clear A's usage. + coordinator.noteHookEvent(Self.event(.sessionEnd, session: "B", workspace: workspaceID, transcript: pathB, at: 4)) + await coordinator.waitUntilIdle() + #expect(shownFraction() == 0.1) + + // Unsupported agents are ignored. + coordinator.noteHookEvent(Self.event(.sessionStart, session: "G", workspace: workspaceID, transcript: nil, at: 5, source: "gemini")) + #expect(shownFraction() == 0.1) + } + + @Test func togglingTheSettingClearsAndResamplesWithoutAHookEvent() async throws { + let directory = FileManager.default.temporaryDirectory.appendingPathComponent("usage-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: directory) } + let path = try Self.claudeTranscript(contextTokens: 200_000, in: directory, name: "a") let workspaceID = UUID() + let defaults = Self.makeDefaults(enabled: false) let metadata = WorkspaceSidebarMetadataModel(limitProvider: UnlimitedSidebarLog()) - let coordinator = SidebarAgentUsageCoordinator(defaults: defaults) { id in + let coordinator = SidebarAgentUsageCoordinator(defaults: defaults, coalesceInterval: .zero) { id in id == workspaceID ? metadata : nil } - metadata.updateAgentUsage(Self.usage, forStatusKey: "claude_code") - // Enabled: a session start clears the previous session's numbers. + // Off: the event is recorded but nothing is read or shown. + coordinator.noteHookEvent(Self.event(.preToolUse, session: "A", workspace: workspaceID, transcript: path, at: 1)) + await coordinator.waitUntilIdle() + #expect(metadata.agentUsageByStatusKey.isEmpty) + defaults.set(true, forKey: SidebarWorkspaceDetailDefaults.showAgentUsageKey) - coordinator.noteHookEvent(WorkstreamEvent( - sessionId: "s1", - hookEventName: .sessionStart, - source: "claude", - workspaceId: workspaceID.uuidString, - transcriptPath: "/nonexistent/s1.jsonl" - )) - #expect(metadata.agentUsageByStatusKey["claude_code"] == nil) - - // Unsupported agents are ignored entirely. - metadata.updateAgentUsage(Self.usage, forStatusKey: "claude_code") - coordinator.noteHookEvent(WorkstreamEvent( - sessionId: "s2", - hookEventName: .sessionStart, - source: "gemini", - workspaceId: workspaceID.uuidString - )) - #expect(metadata.agentUsageByStatusKey["claude_code"] == Self.usage) + coordinator.settingsDidChange() + await coordinator.waitUntilIdle() + #expect(metadata.agentUsageByStatusKey["claude_code"]?.contextFraction == 0.2) + + // Hiding metadata rows also stops (and clears) agent usage. + defaults.set(false, forKey: SidebarCatalogSection().showCustomMetadata.userDefaultsKey) + coordinator.settingsDidChange() + #expect(metadata.agentUsageByStatusKey.isEmpty) } } diff --git a/skills/cmux-settings/references/all-keys.md b/skills/cmux-settings/references/all-keys.md index 5b0cdb3a0387..ce0953be265f 100644 --- a/skills/cmux-settings/references/all-keys.md +++ b/skills/cmux-settings/references/all-keys.md @@ -110,7 +110,7 @@ Sidebar content and metadata visibility from Settings > Sidebar. | `sidebar.showPorts` | boolean | `true` | Show listening ports. | | `sidebar.showLog` | boolean | `true` | Show recent log snippets. | | `sidebar.showProgress` | boolean | `true` | Show progress indicators. | -| `sidebar.showAgentUsage` | boolean | `false` | Append coding-agent usage (model, context window used, estimated API cost) to the Claude Code or Codex status entry. The cost is an estimate from published per-token API prices, not a bill. | +| `sidebar.showAgentUsage` | boolean | `false` | Append coding-agent usage to the Claude Code or Codex status entry: model and context window used, plus for Claude Code an estimated API cost (main thread and subagents) at published Anthropic list prices. The cost is an estimate, not your subscription bill; batch/priority tiers, partner pricing, fast mode and server-tool fees are not modelled. | | `sidebar.showCustomMetadata` | boolean | `true` | Show custom metadata pills. | | `sidebar.wrapWorkspaceTitles` | boolean | `false` | Allow workspace titles in the sidebar to wrap to multiple lines instead of truncating after one line. | | `sidebar.beta` | object | — | Experimental sidebar features. | diff --git a/web/data/cmux.schema.json b/web/data/cmux.schema.json index 8d3b4b8a6e49..2388b870cc36 100644 --- a/web/data/cmux.schema.json +++ b/web/data/cmux.schema.json @@ -1285,7 +1285,7 @@ "showAgentUsage": { "type": "boolean", "default": false, - "description": "Append coding-agent usage (model, context window used, estimated API cost) to the Claude Code or Codex status entry. The cost is an estimate from published per-token API prices, not a bill." + "description": "Append coding-agent usage to the Claude Code or Codex status entry: model and context window used, plus for Claude Code an estimated API cost (main thread and subagents) at published Anthropic list prices. The cost is an estimate, not your subscription bill; batch/priority tiers, partner pricing, fast mode and server-tool fees are not modelled." }, "showAgentActivity": { "type": "boolean", From 22a2f93c774d099ea6db07f0da7ff562327be61d Mon Sep 17 00:00:00 2001 From: Leo Li Date: Sat, 26 Sep 2026 14:07:20 -0400 Subject: [PATCH 04/13] Bound agent usage reads per session and cap concurrent transcript reads - Reader: files whose inode, size and mtime are unchanged are skipped with a plain stat; the subagents directory is re-listed only when its mtime changes; all files of a session share a 32 MB per-sample budget and at most 128 subagent files are tracked. Skipped or excess files make the cost a lower bound. A tail read with no model line is extended once to 16 MB. The reader is now synchronous. - Sampler: blocking reads run on a dedicated dispatch queue, at most two at a time; a sample of a transcript that is already being read waits and then reads again instead of returning nothing. - Coordinator: flushes wait for a pending sampler reset, so a quick off/on cannot discard the fresh read; the settings observer reads only the three gating keys. - Tooltips: one sidebarToolTip(linkURL:) used by the AppKit plain, link and markdown paths and the SwiftUI link row; the cost help string is resolved once. - Fix: SidebarStatusEntry+Presentation needs `public import Foundation` under InternalImportsByDefault now that it exposes a URL parameter. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Usage/AgentUsageFileCursor.swift | 5 + .../Usage/AgentUsageSampler.swift | 90 ++++++++-- .../Usage/AgentUsageTranscriptReader.swift | 165 +++++++++++++++--- .../AgentUsageSamplerTests.swift | 72 ++++++++ .../SidebarStatusEntry+Presentation.swift | 13 +- Sources/ContentView.swift | 2 +- .../SidebarWorkspaceRowSupportViews.swift | 5 +- .../SidebarAgentUsageCoordinator.swift | 22 ++- .../Sidebar/SidebarAgentUsageFormatter.swift | 13 +- 9 files changed, 327 insertions(+), 60 deletions(-) diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageFileCursor.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageFileCursor.swift index 67d14bd2f089..e4ecd62f6e5c 100644 --- a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageFileCursor.swift +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageFileCursor.swift @@ -11,6 +11,11 @@ struct AgentUsageFileCursor: Sendable { var byteOffset: UInt64 = 0 /// Inode of the file when last read (from `fstat` on the open fd). var inode: UInt64? + /// Size and modification time (ns) from the last read's `fstat`; when a + /// plain `stat` still matches them (and the inode), the file is skipped + /// without being opened. + var statSize: UInt64 = 0 + var modificationNanos: Int64 = 0 /// The first bytes of the file when last read. A mismatch means the file /// was rewritten in place, even if it is now larger. var head = Data() diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSampler.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSampler.swift index e4332f1b9c66..fa0b5d83f47b 100644 --- a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSampler.swift +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageSampler.swift @@ -1,12 +1,16 @@ +import Dispatch import Foundation /// Samples coding-agent usage from transcript files off the main actor. /// /// Each transcript is read incrementally by ``AgentUsageTranscriptReader``: /// the first sample scans the file once (only its tail when it is very -/// large), and later samples read only appended bytes. The actor only -/// bookkeeps cursors; the reading itself runs in a detached task per -/// transcript, so a large file never delays another session's sample. +/// large), and later samples read only appended bytes, within a per-sample +/// byte budget. The actor only bookkeeps cursors. The blocking reads run on +/// a dedicated dispatch queue, never on the cooperative thread pool, and at +/// most ``maxConcurrentReads`` run at a time; further samples wait their +/// turn. A second sample of a transcript that is already being read waits +/// for that read and then reads again, so no request is dropped. /// /// The sampler performs no polling and owns no timers; callers invoke /// ``sample(transcriptPath:source:)`` when an agent hook event says the @@ -27,13 +31,27 @@ public actor AgentUsageSampler { let keyEpoch: UInt64 } + /// Reads allowed to run at once. + public let maxConcurrentReads: Int + private var sessions: [Key: AgentUsageTranscriptReader.SessionCursor] = [:] private var inFlight: Set = [] + private var keyWaiters: [Key: [CheckedContinuation]] = [:] + private var activeReads = 0 + private var readWaiters: [CheckedContinuation] = [] private var epoch: UInt64 = 0 private var keyEpochs: [Key: UInt64] = [:] private var useCounter: UInt64 = 0 private let maxTrackedTranscripts: Int private let reader: AgentUsageTranscriptReader + // Blocking file reads are handed to this queue so they never occupy a + // cooperative-pool thread. It only runs work; it guards no state (all + // state lives in the actor), and `maxConcurrentReads` bounds its load. + private let readQueue = DispatchQueue( + label: "com.cmux.agent-usage.transcript-reads", + qos: .utility, + attributes: .concurrent + ) /// Creates a sampler with the default read limits. /// @@ -41,13 +59,23 @@ public actor AgentUsageSampler { /// - maxTrackedTranscripts: How many sessions keep incremental state; /// the least recently sampled is dropped beyond this. A dropped /// session is re-read on its next sample (bounded by the tail limit). + /// - maxConcurrentReads: Transcript reads allowed to run at once. /// - catalog: Model table for display names, windows and prices. - public init(maxTrackedTranscripts: Int = 64, catalog: AgentModelCatalog = AgentModelCatalog()) { - self.init(maxTrackedTranscripts: maxTrackedTranscripts, reader: AgentUsageTranscriptReader(catalog: catalog)) + public init( + maxTrackedTranscripts: Int = 64, + maxConcurrentReads: Int = 2, + catalog: AgentModelCatalog = AgentModelCatalog() + ) { + self.init( + maxTrackedTranscripts: maxTrackedTranscripts, + maxConcurrentReads: maxConcurrentReads, + reader: AgentUsageTranscriptReader(catalog: catalog) + ) } - init(maxTrackedTranscripts: Int = 64, reader: AgentUsageTranscriptReader) { + init(maxTrackedTranscripts: Int = 64, maxConcurrentReads: Int = 2, reader: AgentUsageTranscriptReader) { self.maxTrackedTranscripts = max(1, maxTrackedTranscripts) + self.maxConcurrentReads = max(1, maxConcurrentReads) self.reader = reader } @@ -56,20 +84,31 @@ public actor AgentUsageSampler { /// - Parameters: /// - transcriptPath: Absolute path of the agent transcript JSONL. /// - source: The transcript format. - /// - Returns: The usage snapshot, or `nil` when there is nothing new to - /// report: the file is unreadable or carries no model yet, a sample of - /// the same transcript is already running, or the transcript was - /// forgotten/reset while this sample ran. + /// - Returns: The usage snapshot, or `nil` when there is nothing to + /// report: the file is unreadable or carries no model yet, or the + /// transcript was forgotten/reset while this sample ran. public func sample(transcriptPath: String, source: AgentUsageSource) async -> AgentUsageSnapshot? { let key = Key(path: transcriptPath, source: source) - guard inFlight.insert(key).inserted else { return nil } - defer { inFlight.remove(key) } + while inFlight.contains(key) { + await withCheckedContinuation { keyWaiters[key, default: []].append($0) } + } + inFlight.insert(key) + defer { + inFlight.remove(key) + for waiter in keyWaiters.removeValue(forKey: key) ?? [] { waiter.resume() } + } + await acquireReadSlot() + defer { releaseReadSlot() } + let generation = generation(for: key) let previous = sessions[key] ?? AgentUsageTranscriptReader.SessionCursor() let reader = self.reader - let (updated, snapshot) = await Task.detached(priority: .utility) { - await reader.advanceSession(previous, path: transcriptPath, source: source) - }.value + let readQueue = self.readQueue + let (updated, snapshot) = await withCheckedContinuation { continuation in + readQueue.async { + continuation.resume(returning: reader.advanceSession(previous, path: transcriptPath, source: source)) + } + } guard generation == self.generation(for: key) else { return nil } useCounter &+= 1 var stored = updated @@ -98,13 +137,32 @@ public actor AgentUsageSampler { } /// Drops all incremental state (for example when the feature is turned - /// off); samples still running are discarded. + /// off). Reads already running finish (each is bounded) but their + /// results are discarded; a sample requested afterwards waits for them + /// and then reads from scratch. public func reset() { sessions.removeAll() keyEpochs.removeAll() epoch &+= 1 } + private func acquireReadSlot() async { + if activeReads < maxConcurrentReads { + activeReads += 1 + return + } + // The slot is handed over directly by `releaseReadSlot`. + await withCheckedContinuation { readWaiters.append($0) } + } + + private func releaseReadSlot() { + if readWaiters.isEmpty { + activeReads -= 1 + } else { + readWaiters.removeFirst().resume() + } + } + private func generation(for key: Key) -> Generation { Generation(epoch: epoch, keyEpoch: keyEpochs[key] ?? 0) } diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptReader.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptReader.swift index 0a7460d24a30..e01f6554819f 100644 --- a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptReader.swift +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptReader.swift @@ -2,17 +2,32 @@ import Foundation /// Reads agent transcripts incrementally for ``AgentUsageSampler``. /// -/// Blocking file reads happen here, inside a detached task the sampler -/// starts per transcript, so one large file never holds the sampler actor. -/// A file larger than ``fullScanLimit`` on first sight is read only from its -/// last ``tailBytes``: model and context come from that tail, and the cost is -/// reported as unknown rather than as a misleading partial sum. +/// All methods are synchronous and do blocking file I/O; the sampler runs +/// them on a dedicated dispatch queue, at most two at a time, never on the +/// cooperative thread pool. +/// +/// Work per sample is bounded: +/// - A file larger than ``fullScanLimit`` on first sight is read only from +/// its last ``tailBytes`` (extended once to ``extendedTailBytes`` when the +/// tail of a main transcript holds no model line). Model and context come +/// from that tail; the cost is reported as unknown rather than as a +/// misleading partial sum. +/// - A file whose inode, size and modification time are unchanged is skipped +/// with a plain `stat`, without being opened. +/// - All files of a session share ``sessionByteBudget`` per sample; subagent +/// files that do not fit wait for the next sample, and the cost is marked +/// as a lower bound meanwhile. At most ``maxSubagentFiles`` subagent +/// transcripts are tracked per session; beyond that the cost is a lower +/// bound. struct AgentUsageTranscriptReader: Sendable { /// Per-session cursors: the main transcript and, for Claude Code, each /// subagent transcript in `/subagents/agent-*.jsonl`. struct SessionCursor: Sendable { var main: AgentUsageFileCursor? var subagents: [String: AgentUsageFileCursor] = [:] + /// Cached listing of the subagents directory, keyed by its mtime. + var subagentDirectoryModificationNanos: Int64? + var subagentNames: [String] = [] var lastUse: UInt64 = 0 } @@ -20,6 +35,9 @@ struct AgentUsageTranscriptReader: Sendable { let maxLineBytes: Int let fullScanLimit: UInt64 let tailBytes: UInt64 + let extendedTailBytes: UInt64 + let sessionByteBudget: Int64 + let maxSubagentFiles: Int let catalog: AgentModelCatalog private static let headLength = 256 @@ -28,12 +46,18 @@ struct AgentUsageTranscriptReader: Sendable { maxLineBytes: Int = 16 << 20, fullScanLimit: UInt64 = 24 << 20, tailBytes: UInt64 = 4 << 20, + extendedTailBytes: UInt64 = 16 << 20, + sessionByteBudget: Int64 = 32 << 20, + maxSubagentFiles: Int = 128, catalog: AgentModelCatalog = AgentModelCatalog() ) { self.chunkSize = max(1, chunkSize) self.maxLineBytes = max(1, maxLineBytes) self.fullScanLimit = fullScanLimit self.tailBytes = min(max(1, tailBytes), max(1, fullScanLimit)) + self.extendedTailBytes = max(self.tailBytes, extendedTailBytes) + self.sessionByteBudget = max(1, sessionByteBudget) + self.maxSubagentFiles = max(0, maxSubagentFiles) self.catalog = catalog } @@ -45,26 +69,36 @@ struct AgentUsageTranscriptReader: Sendable { _ cursor: SessionCursor, path: String, source: AgentUsageSource - ) async -> (SessionCursor, AgentUsageSnapshot?) { + ) -> (SessionCursor, AgentUsageSnapshot?) { var session = cursor - session.main = await advance(session.main, path: path, source: source) + var budget = sessionByteBudget + var skipped = false + session.main = advance(session.main, path: path, source: source, isMain: true, budget: &budget, skipped: &skipped) guard let main = session.main else { return (SessionCursor(), nil) } guard source == .claude, let snapshot = main.accumulator.snapshot() else { return (session, main.accumulator.snapshot()) } let directory = Self.subagentsDirectory(forTranscriptPath: path) - let names = ((try? FileManager.default.contentsOfDirectory(atPath: directory)) ?? []) - .filter { $0.hasPrefix("agent-") && $0.hasSuffix(".jsonl") } - .sorted() + refreshSubagentNames(of: &session, directory: directory) var cost = main.accumulator.cost() + var incomplete = session.subagentNames.count > maxSubagentFiles var subagents: [String: AgentUsageFileCursor] = [:] - for name in names { + for name in session.subagentNames.prefix(maxSubagentFiles) { let subPath = (directory as NSString).appendingPathComponent(name) - guard let sub = await advance(session.subagents[name], path: subPath, source: .claude) else { continue } + var subSkipped = false + let sub = advance( + session.subagents[name], path: subPath, source: .claude, + isMain: false, budget: &budget, skipped: &subSkipped + ) + incomplete = incomplete || subSkipped + guard let sub else { continue } subagents[name] = sub cost = AgentUsageCost.combine(cost, sub.accumulator.cost()) } session.subagents = subagents + if incomplete, let partial = cost { + cost = AgentUsageCost(usd: partial.usd, isLowerBound: true, hasPricedUsage: partial.hasPricedUsage) + } return (session, snapshot.withEstimatedCost(cost?.displayable)) } @@ -73,14 +107,52 @@ struct AgentUsageTranscriptReader: Sendable { ((path as NSString).deletingPathExtension as NSString).appendingPathComponent("subagents") } + /// Re-lists the subagents directory only when its mtime changed (a new + /// file changes it; appends to existing files do not). + private func refreshSubagentNames(of session: inout SessionCursor, directory: String) { + guard let info = Self.statInfo(directory) else { + session.subagentDirectoryModificationNanos = nil + session.subagentNames = [] + return + } + let modified = Self.modificationNanos(info) + guard modified != session.subagentDirectoryModificationNanos else { return } + session.subagentDirectoryModificationNanos = modified + session.subagentNames = ((try? FileManager.default.contentsOfDirectory(atPath: directory)) ?? []) + .filter { $0.hasPrefix("agent-") && $0.hasSuffix(".jsonl") } + .sorted() + } + /// Reads whatever was appended to one file since `cursor`, restarting - /// when the file was truncated or replaced. Returns `nil` when the file - /// cannot be opened. + /// when the file was truncated or replaced. + /// + /// - Parameters: + /// - isMain: The session's main transcript: exempt from the budget + /// (its first read is already bounded) and eligible for the tail + /// extension when the tail holds no model line. + /// - budget: Bytes left for this sample; decremented by bytes read. + /// - skipped: Set when a changed file was not read for lack of budget. + /// - Returns: The updated cursor (the previous one when skipped), or + /// `nil` when the file cannot be read. func advance( _ cursor: AgentUsageFileCursor?, path: String, - source: AgentUsageSource - ) async -> AgentUsageFileCursor? { + source: AgentUsageSource, + isMain: Bool, + budget: inout Int64, + skipped: inout Bool + ) -> AgentUsageFileCursor? { + guard let pathInfo = Self.statInfo(path) else { return nil } + if let cursor, + cursor.inode == UInt64(pathInfo.st_ino), + cursor.statSize == UInt64(max(0, pathInfo.st_size)), + cursor.modificationNanos == Self.modificationNanos(pathInfo) { + return cursor + } + if !isMain, budget <= 0 { + skipped = true + return cursor + } guard let handle = FileHandle(forReadingAtPath: path) else { return nil } defer { try? handle.close() } var info = stat() @@ -90,6 +162,7 @@ struct AgentUsageTranscriptReader: Sendable { let head = (try? handle.read(upToCount: Self.headLength)) ?? Data() var state: AgentUsageFileCursor + var tailRead = false if var existing = cursor, existing.inode == inode, size >= existing.byteOffset, @@ -97,28 +170,52 @@ struct AgentUsageTranscriptReader: Sendable { existing.head = head state = existing } else { - state = AgentUsageFileCursor(source: source, catalog: catalog) - state.inode = inode - state.head = head - if size > fullScanLimit { - state.byteOffset = size - tailBytes - state.discardingLine = true - state.accumulator.markHistoryIncomplete() - } + state = freshCursor(source: source, head: head, inode: inode, size: size, tail: tailBytes) + tailRead = size > fullScanLimit } - guard size > state.byteOffset else { return state } + budget -= readToEnd(handle, into: &state) + if tailRead, isMain, extendedTailBytes > tailBytes, state.accumulator.snapshot() == nil { + state = freshCursor(source: source, head: head, inode: inode, size: size, tail: extendedTailBytes) + budget -= readToEnd(handle, into: &state) + } + state.statSize = size + state.modificationNanos = Self.modificationNanos(info) + return state + } + + private func freshCursor( + source: AgentUsageSource, + head: Data, + inode: UInt64, + size: UInt64, + tail: UInt64 + ) -> AgentUsageFileCursor { + var state = AgentUsageFileCursor(source: source, catalog: catalog) + state.inode = inode + state.head = head + if size > fullScanLimit { + state.byteOffset = size - min(size, tail) + // Starting mid-file lands mid-line; skip to the next line start. + state.discardingLine = state.byteOffset > 0 + state.accumulator.markHistoryIncomplete() + } + return state + } + + /// Reads from `state.byteOffset` to end of file; returns bytes read. + private func readToEnd(_ handle: FileHandle, into state: inout AgentUsageFileCursor) -> Int64 { do { try handle.seek(toOffset: state.byteOffset) } catch { - return state + return 0 } + var total: Int64 = 0 while let chunk = try? handle.read(upToCount: chunkSize), !chunk.isEmpty { state.byteOffset += UInt64(chunk.count) + total += Int64(chunk.count) consume(chunk, into: &state) - // Let other transcripts' reads interleave on the cooperative pool. - await Task.yield() } - return state + return total } private func consume(_ chunk: Data, into state: inout AgentUsageFileCursor) { @@ -144,4 +241,14 @@ struct AgentUsageTranscriptReader: Sendable { state.accumulator.markLineDropped() } } + + private static func statInfo(_ path: String) -> stat? { + var info = stat() + guard stat(path, &info) == 0 else { return nil } + return info + } + + private static func modificationNanos(_ info: stat) -> Int64 { + Int64(info.st_mtimespec.tv_sec) * 1_000_000_000 + Int64(info.st_mtimespec.tv_nsec) + } } diff --git a/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageSamplerTests.swift b/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageSamplerTests.swift index 45b8e07c1fb2..8320b233d34a 100644 --- a/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageSamplerTests.swift +++ b/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageSamplerTests.swift @@ -117,6 +117,78 @@ struct AgentUsageSamplerTests { #expect(abs(try #require(snapshot.estimatedCost).usd - 0.0299) < 1e-9) } + private func writeSession(in directory: URL, subagentCount: Int) throws -> URL { + let url = directory.appendingPathComponent("session.jsonl") + try Data((Fixture.claudeAssistant(id: "msg_main", input: 0, cacheRead: 1000, output: 0) + "\n").utf8).write(to: url) + let subagents = directory.appendingPathComponent("session/subagents") + try FileManager.default.createDirectory(at: subagents, withIntermediateDirectories: true) + for index in 0.. String? { + var parts: [String] = [] + if let linkURL { parts.append(linkURL.absoluteString) } + if let helpText, !helpText.isEmpty { parts.append(sidebarHelpText) } + return parts.isEmpty ? nil : parts.joined(separator: "\n") + } } diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index 8e19dbadf608..6be440f041f1 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -16955,7 +16955,7 @@ private struct SidebarMetadataEntryRow: View { rowContent(underlined: true) } .buttonStyle(.plain) - .safeHelp(url.absoluteString) + .safeHelp(entry.sidebarToolTip(linkURL: url) ?? url.absoluteString) } else { rowContent(underlined: false) .contentShape(Rectangle()) diff --git a/Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSupportViews.swift b/Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSupportViews.swift index 2fa0f6b106b6..97c1ee80f84e 100644 --- a/Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSupportViews.swift +++ b/Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSupportViews.swift @@ -318,6 +318,7 @@ final class SidebarRowIconTextLine: NSView { explicitURL: entry.url, onOpenURL: onOpenURL ) + if entry.helpText != nil { markdownTextView.toolTip = entry.sidebarToolTip(linkURL: entry.url) } } else if let url = entry.url { textView.isHidden = true metadataButton.isHidden = false @@ -326,13 +327,13 @@ final class SidebarRowIconTextLine: NSView { font: font, color: color, underlined: true, - toolTip: url.absoluteString, + toolTip: entry.sidebarToolTip(linkURL: url), onClick: { onOpenURL(url) } ) } else { metadataButton.isHidden = true textView.isHidden = false - textView.configurePlainText(entry.sidebarDisplayText, font: font, color: color, toolTip: entry.helpText) + textView.configurePlainText(entry.sidebarDisplayText, font: font, color: color, toolTip: entry.sidebarToolTip(linkURL: nil)) } needsLayout = true } diff --git a/Sources/Sidebar/SidebarAgentUsageCoordinator.swift b/Sources/Sidebar/SidebarAgentUsageCoordinator.swift index d8bb3a45e8d7..30a01afbb7b7 100644 --- a/Sources/Sidebar/SidebarAgentUsageCoordinator.swift +++ b/Sources/Sidebar/SidebarAgentUsageCoordinator.swift @@ -1,5 +1,6 @@ import CMUXAgentLaunch import CmuxAgentChat +import CmuxSettings import CmuxSidebar import Foundation @@ -59,6 +60,9 @@ final class SidebarAgentUsageCoordinator { private var epochCounter: UInt64 = 0 private var lastKnownEnabled: Bool private var observationTasks: [Task] = [] + /// The sampler reset issued by the last disable; flushes wait for it so + /// a quick off→on cannot have its fresh read discarded by a late reset. + private var resetTask: Task? /// Creates a coordinator. /// @@ -110,10 +114,15 @@ final class SidebarAgentUsageCoordinator { Self.isEnabled(defaults: defaults) } + /// Reads only the three settings that gate usage (this runs on every + /// `UserDefaults` change notification). Usage renders inside the + /// metadata rows, so hidden rows mean no reads. private static func isEnabled(defaults: UserDefaults) -> Bool { - let visibility = SidebarWorkspaceDetailDefaults.auxiliaryDetailVisibility(defaults: defaults) - // Usage renders inside the metadata rows; no rows, no reads. - return visibility.showsAgentUsage && visibility.showsMetadata + let settings = UserDefaultsSettingsClient(defaults: defaults) + let sidebar = SidebarCatalogSection() + return settings.value(for: sidebar.showAgentUsage) + && settings.value(for: sidebar.showCustomMetadata) + && !settings.value(for: sidebar.hideAllDetails) } /// Reacts to a settings change: samples every known session when usage @@ -213,6 +222,7 @@ final class SidebarAgentUsageCoordinator { } guard isEnabled, let record = sessions[sessionID], let path = record.transcriptPath else { return } readingSessionIDs.insert(sessionID) + await resetTask?.value let snapshot = await sampler.sample(transcriptPath: path, source: record.source) // The setting may have flipped, or the session ended or restarted, // while sampling; `nil` means "nothing new" and keeps what is shown. @@ -260,7 +270,11 @@ final class SidebarAgentUsageCoordinator { metadataLookup(row.workspaceID)?.updateAgentUsage(nil, forStatusKey: row.source.sidebarStatusKey) } rowsShowingUsage.removeAll() - Task { [sampler] in await sampler.reset() } + let previousReset = resetTask + resetTask = Task { [sampler] in + await previousReset?.value + await sampler.reset() + } } /// Waits until no sample is scheduled or running (tests use this to diff --git a/Sources/Sidebar/SidebarAgentUsageFormatter.swift b/Sources/Sidebar/SidebarAgentUsageFormatter.swift index f1f2e341bec3..411cdaa5606b 100644 --- a/Sources/Sidebar/SidebarAgentUsageFormatter.swift +++ b/Sources/Sidebar/SidebarAgentUsageFormatter.swift @@ -20,13 +20,12 @@ struct SidebarAgentUsageFormatter { self.locale = locale } - /// Tooltip for an entry that shows an estimated cost. - static var costHelpText: String { - String( - localized: "sidebar.agentUsage.costHelp", - defaultValue: "Cost is an API list-price estimate, not your subscription bill." - ) - } + /// Tooltip for an entry that shows an estimated cost (resolved once; + /// the app language only changes across launches). + static let costHelpText = String( + localized: "sidebar.agentUsage.costHelp", + defaultValue: "Cost is an API list-price estimate, not your subscription bill." + ) /// The compact usage summary, e.g. `Opus 4.8 · 42% · ~$1.20`. /// From 713b6a0d0ae435433a723d3076ce59cda8fcf93a Mon Sep 17 00:00:00 2001 From: Leo Date: Sun, 27 Sep 2026 16:41:42 +0800 Subject: [PATCH 05/13] Fix sidebar usage refresh edge cases --- .../Usage/AgentUsageTranscriptReader.swift | 14 +++---- .../AgentUsageSamplerTests.swift | 37 +++++++++++++++++++ .../SidebarWorkspaceRowSupportViews.swift | 1 + 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptReader.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptReader.swift index e01f6554819f..6e0c5cfe868d 100644 --- a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptReader.swift +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentUsageTranscriptReader.swift @@ -25,8 +25,6 @@ struct AgentUsageTranscriptReader: Sendable { struct SessionCursor: Sendable { var main: AgentUsageFileCursor? var subagents: [String: AgentUsageFileCursor] = [:] - /// Cached listing of the subagents directory, keyed by its mtime. - var subagentDirectoryModificationNanos: Int64? var subagentNames: [String] = [] var lastUse: UInt64 = 0 } @@ -107,17 +105,15 @@ struct AgentUsageTranscriptReader: Sendable { ((path as NSString).deletingPathExtension as NSString).appendingPathComponent("subagents") } - /// Re-lists the subagents directory only when its mtime changed (a new - /// file changes it; appends to existing files do not). + /// Re-lists the bounded subagent directory on every sample. Files + /// themselves still use stat/inode cursors, so unchanged transcripts are + /// not reopened. Directory mtimes can collide at filesystem timestamp + /// resolution and are not reliable discovery cursors. private func refreshSubagentNames(of session: inout SessionCursor, directory: String) { - guard let info = Self.statInfo(directory) else { - session.subagentDirectoryModificationNanos = nil + guard Self.statInfo(directory) != nil else { session.subagentNames = [] return } - let modified = Self.modificationNanos(info) - guard modified != session.subagentDirectoryModificationNanos else { return } - session.subagentDirectoryModificationNanos = modified session.subagentNames = ((try? FileManager.default.contentsOfDirectory(atPath: directory)) ?? []) .filter { $0.hasPrefix("agent-") && $0.hasSuffix(".jsonl") } .sorted() diff --git a/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageSamplerTests.swift b/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageSamplerTests.swift index 8320b233d34a..f888c2c252ed 100644 --- a/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageSamplerTests.swift +++ b/Packages/Shared/CmuxAgentChat/Tests/CmuxAgentChatTests/AgentUsageSamplerTests.swift @@ -130,6 +130,43 @@ struct AgentUsageSamplerTests { return url } + @Test func newSubagentIsDiscoveredWhenDirectoryMtimeDoesNotChange() async throws { + let directory = try makeDirectory() + defer { try? FileManager.default.removeItem(at: directory) } + let url = try writeSession(in: directory, subagentCount: 1) + let subagents = directory.appendingPathComponent("session/subagents") + let originalModificationDate = try #require( + FileManager.default.attributesOfItem(atPath: subagents.path)[.modificationDate] as? Date + ) + let sampler = AgentUsageSampler() + + let first = try #require( + await sampler.sample(transcriptPath: url.path, source: .claude)?.estimatedCost + ) + #expect(!first.isLowerBound) + #expect(abs(first.usd - 0.0255) < 1e-9) + + let secondLine = Fixture.claudeAssistant( + id: "msg_sub_same_mtime", + input: 0, + output: 1000, + isSidechain: true + ) + try Data((secondLine + "\n").utf8).write( + to: subagents.appendingPathComponent("agent-same-mtime.jsonl") + ) + try FileManager.default.setAttributes( + [.modificationDate: originalModificationDate], + ofItemAtPath: subagents.path + ) + + let second = try #require( + await sampler.sample(transcriptPath: url.path, source: .claude)?.estimatedCost + ) + #expect(!second.isLowerBound) + #expect(abs(second.usd - 0.0505) < 1e-9) + } + @Test func subagentReadsRespectTheSessionBudgetAndSkipUnchangedFiles() async throws { let directory = try makeDirectory() defer { try? FileManager.default.removeItem(at: directory) } diff --git a/Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSupportViews.swift b/Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSupportViews.swift index 97c1ee80f84e..e581beebe738 100644 --- a/Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSupportViews.swift +++ b/Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSupportViews.swift @@ -444,6 +444,7 @@ final class SidebarRowIconTextLine: NSView { private func resetPrimaryContent() { textView.isHidden = true + textView.toolTip = nil textView.stringValue = "" textView.attributedStringValue = NSAttributedString(string: "") metadataButton.isHidden = true From 10099487d170e1fef4d8bb8f12e6b730fd8669fc Mon Sep 17 00:00:00 2001 From: teamleaderleo Date: Mon, 28 Sep 2026 02:49:32 -0400 Subject: [PATCH 06/13] Address review: gate agent usage on custom metadata, keep usage across same-transcript SessionStart, order forget before resumed reads, trim hook workspace ids - Settings row and palette toggle for sidebar.showAgentUsage are disabled/hidden while custom metadata is off, since usage renders inside those rows. - SessionStart for the same transcript (resume, compaction) bumps the epoch to drop in-flight reads but keeps the shown usage; a new transcript clears it. - SessionEnd's sampler forget is chained on the reset task that flushes await, so a resumed session's first read is not discarded. - Hook workspace ids are trimmed before UUID parsing, matching the feed. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../Sections/SidebarSection+AgentUsage.swift | 7 ++-- .../Sections/SidebarSection.swift | 2 +- ...tteSettingsToggleCommands+AgentUsage.swift | 11 ++++-- .../SidebarAgentUsageCoordinator.swift | 31 +++++++++++----- cmuxTests/SidebarAgentUsageTests.swift | 35 +++++++++++++++++-- 5 files changed, 68 insertions(+), 18 deletions(-) diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection+AgentUsage.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection+AgentUsage.swift index 151c42f456b1..7cb4391b75e1 100644 --- a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection+AgentUsage.swift +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection+AgentUsage.swift @@ -2,8 +2,9 @@ import CmuxSettings import SwiftUI extension SidebarSection { - /// The opt-in `sidebar.showAgentUsage` toggle row. Like the other detail - /// rows it is disabled while "Hide All Details" is on. + /// The opt-in `sidebar.showAgentUsage` toggle row. Usage renders inside + /// the custom metadata rows, so it is disabled while "Hide All Details" + /// is on or custom metadata is off. @ViewBuilder var agentUsageRow: some View { SettingsCardRow( @@ -15,7 +16,7 @@ extension SidebarSection { .labelsHidden() .controlSize(.small) } - .disabled(hideAll.current) + .disabled(hideAll.current || !showMetadata.current) SettingsCardDivider() } } diff --git a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swift b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swift index 3ac1f188ef1c..489a1822376d 100644 --- a/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swift +++ b/Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/SidebarSection.swift @@ -34,7 +34,7 @@ public struct SidebarSection: View { @State var showAgentUsage: DefaultsValueModel @State var loadingSpinnerPosition: DefaultsValueModel @State var notificationBadgePosition: DefaultsValueModel - @State private var showMetadata: DefaultsValueModel + @State var showMetadata: DefaultsValueModel @State private var rightMaxWidth: DefaultsValueModel @State private var rememberedRightMaxWidth: DefaultsValueModel public init(defaultsStore: UserDefaultsSettingsStore, catalog: SettingCatalog, hostActions: SettingsHostActions) { diff --git a/Sources/CommandPalette/CommandPaletteSettingsToggleCommands+AgentUsage.swift b/Sources/CommandPalette/CommandPaletteSettingsToggleCommands+AgentUsage.swift index fa98f72924df..105f6cec11a9 100644 --- a/Sources/CommandPalette/CommandPaletteSettingsToggleCommands+AgentUsage.swift +++ b/Sources/CommandPalette/CommandPaletteSettingsToggleCommands+AgentUsage.swift @@ -1,9 +1,11 @@ +import CmuxSettings import Foundation extension CommandPaletteSettingsToggleCommands { /// The palette toggle for `sidebar.showAgentUsage`, kept out of the - /// length-budgeted descriptor table. It shares the sidebar-details - /// availability rule, so it is hidden while "Hide All Details" is on. + /// length-budgeted descriptor table. Usage renders inside the custom + /// metadata rows, so the toggle is hidden while "Hide All Details" is on + /// (`isAvailable`) or custom metadata is off. static func sidebarAgentUsageDescriptor( sectionTitle: @escaping @Sendable () -> String, isAvailable: @escaping @Sendable (UserDefaults) -> Bool @@ -18,7 +20,10 @@ extension CommandPaletteSettingsToggleCommands { keywords: ["sidebar.showAgentUsage", "sidebar", "agent", "usage", "model", "context", "tokens", "cost"], defaultValue: SidebarWorkspaceDetailDefaults.showAgentUsage, defaultsKey: SidebarWorkspaceDetailDefaults.showAgentUsageKey, - isAvailable: isAvailable + isAvailable: { defaults in + isAvailable(defaults) + && UserDefaultsSettingsClient(defaults: defaults).value(for: SettingCatalog().sidebar.showCustomMetadata) + } ) } } diff --git a/Sources/Sidebar/SidebarAgentUsageCoordinator.swift b/Sources/Sidebar/SidebarAgentUsageCoordinator.swift index 30a01afbb7b7..fc599d1c1db4 100644 --- a/Sources/Sidebar/SidebarAgentUsageCoordinator.swift +++ b/Sources/Sidebar/SidebarAgentUsageCoordinator.swift @@ -60,8 +60,9 @@ final class SidebarAgentUsageCoordinator { private var epochCounter: UInt64 = 0 private var lastKnownEnabled: Bool private var observationTasks: [Task] = [] - /// The sampler reset issued by the last disable; flushes wait for it so - /// a quick off→on cannot have its fresh read discarded by a late reset. + /// The last sampler reset (disable) or forget (session end), chained in + /// issue order; flushes wait for it so a quick off→on or a resumed + /// session cannot have its fresh read discarded by a late reset/forget. private var resetTask: Task? /// Creates a coordinator. @@ -144,7 +145,9 @@ final class SidebarAgentUsageCoordinator { /// - Parameter event: The hook event as accepted by the feed pipeline. func noteHookEvent(_ event: WorkstreamEvent) { guard let source = AgentUsageSource(hookSource: event.source), - let workspaceID = event.workspaceId.flatMap(UUID.init(uuidString:)) else { return } + let workspaceID = event.workspaceId + .map({ $0.trimmingCharacters(in: .whitespacesAndNewlines) }) + .flatMap(UUID.init(uuidString:)) else { return } let sessionID = event.sessionId if event.hookEventName == .sessionEnd { endSession(sessionID) @@ -158,17 +161,22 @@ final class SidebarAgentUsageCoordinator { epoch: 0, snapshot: nil ) - if event.hookEventName == .sessionStart || sessions[sessionID] == nil { - // A new or resumed run must not show the previous run's numbers. + let isNewSession = sessions[sessionID] == nil + let newTranscriptPath = event.transcriptPath.flatMap { $0.isEmpty ? nil : $0 } + let transcriptChanged = newTranscriptPath != nil && newTranscriptPath != record.transcriptPath + if event.hookEventName == .sessionStart || isNewSession || transcriptChanged { + // Discard any in-flight read. Usage is cumulative per transcript, + // so a SessionStart for the same transcript (resume, compaction) + // keeps what is shown; only a different transcript clears it. epochCounter &+= 1 record.epoch = epochCounter - record.snapshot = nil + if isNewSession || transcriptChanged { record.snapshot = nil } } let previousWorkspaceID = record.workspaceID record.workspaceID = workspaceID record.lastEventAt = max(record.lastEventAt, event.receivedAt) - if let path = event.transcriptPath, !path.isEmpty { - record.transcriptPath = path + if let newTranscriptPath { + record.transcriptPath = newTranscriptPath } sessions[sessionID] = record evictIfNeeded() @@ -188,7 +196,12 @@ final class SidebarAgentUsageCoordinator { guard let record = sessions.removeValue(forKey: sessionID) else { return } if let path = record.transcriptPath { // Also discards a sample of this transcript that is still running. - Task { [sampler] in await sampler.forget(transcriptPath: path) } + // Chained so a resumed session's first flush waits for it. + let previous = resetTask + resetTask = Task { [sampler] in + await previous?.value + await sampler.forget(transcriptPath: path) + } } publish(RowKey(workspaceID: record.workspaceID, source: record.source)) } diff --git a/cmuxTests/SidebarAgentUsageTests.swift b/cmuxTests/SidebarAgentUsageTests.swift index 0bd9b4bd3736..8e8c6f4c34c0 100644 --- a/cmuxTests/SidebarAgentUsageTests.swift +++ b/cmuxTests/SidebarAgentUsageTests.swift @@ -41,13 +41,14 @@ struct SidebarAgentUsageTests { workspace: UUID, transcript: String?, at seconds: TimeInterval, - source: String = "claude" + source: String = "claude", + workspaceIdPadding: String = "" ) -> WorkstreamEvent { WorkstreamEvent( sessionId: session, hookEventName: name, source: source, - workspaceId: workspace.uuidString, + workspaceId: workspace.uuidString + workspaceIdPadding, transcriptPath: transcript, receivedAt: Date(timeIntervalSince1970: seconds) ) @@ -162,4 +163,34 @@ struct SidebarAgentUsageTests { coordinator.settingsDidChange() #expect(metadata.agentUsageByStatusKey.isEmpty) } + + @Test func resumingTheSameTranscriptKeepsUsageAndPaddedWorkspaceIdsResolve() async throws { + let directory = FileManager.default.temporaryDirectory.appendingPathComponent("usage-\(UUID().uuidString)") + try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true) + defer { try? FileManager.default.removeItem(at: directory) } + let path = try Self.claudeTranscript(contextTokens: 100_000, in: directory, name: "a") + let workspaceID = UUID() + let metadata = WorkspaceSidebarMetadataModel(limitProvider: UnlimitedSidebarLog()) + let coordinator = SidebarAgentUsageCoordinator( + defaults: Self.makeDefaults(enabled: true), + coalesceInterval: .zero + ) { id in id == workspaceID ? metadata : nil } + func shownFraction() -> Double? { metadata.agentUsageByStatusKey["claude_code"]?.contextFraction } + + coordinator.noteHookEvent(Self.event(.preToolUse, session: "A", workspace: workspaceID, transcript: path, at: 1, workspaceIdPadding: "\n")) + await coordinator.waitUntilIdle() + #expect(shownFraction() == 0.1) + + // A SessionStart for the same transcript (resume, compaction) does not blank the row. + coordinator.noteHookEvent(Self.event(.sessionStart, session: "A", workspace: workspaceID, transcript: path, at: 2)) + #expect(shownFraction() == 0.1) + await coordinator.waitUntilIdle() + #expect(shownFraction() == 0.1) + + // End then resume with the same id and transcript: the forget is ordered before the new read. + coordinator.noteHookEvent(Self.event(.sessionEnd, session: "A", workspace: workspaceID, transcript: path, at: 3)) + coordinator.noteHookEvent(Self.event(.sessionStart, session: "A", workspace: workspaceID, transcript: path, at: 4)) + await coordinator.waitUntilIdle() + #expect(shownFraction() == 0.1) + } } From de5222fc0d6b495832bf26f9c6d6463e22867682 Mon Sep 17 00:00:00 2001 From: teamleaderleo Date: Mon, 28 Sep 2026 02:58:00 -0400 Subject: [PATCH 07/13] Localizable.xcstrings: add only the agent-usage keys on top of main The branch catalog had reordered about seven blocks of existing keys; rebuild it from main with the six new keys appended so the diff is additions only. Co-Authored-By: Claude Opus 5.5 (1M context) --- Resources/Localizable.xcstrings | 3316 +++++++++++++++---------------- 1 file changed, 1658 insertions(+), 1658 deletions(-) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index a9d634bff443..b437e5280dfa 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -1,6 +1,478 @@ { "sourceLanguage": "en", "strings": { + "browser.omnibar.accessibilityLabel": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Address and search bar" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Adress- und Suchleiste" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Barre d’adresse et de recherche" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "شريط العنوان والبحث" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Barra de direcciones y búsqueda" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "網址與搜尋列" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "地址和搜索栏" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "주소 및 검색 막대" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "アドレスと検索バー" + } + } + } + }, + "projectPanel.reload": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reload Project" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Projekt neu laden" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Recharger le projet" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "إعادة تحميل المشروع" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Recargar proyecto" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "重新載入專案" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "重新加载项目" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "프로젝트 다시 불러오기" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "プロジェクトを再読み込み" + } + } + } + }, + "projectPanel.reloadErrors": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Reload returned errors: %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Beim Neuladen sind Fehler aufgetreten: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le rechargement a renvoyé des erreurs : %@" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "أعادت إعادة التحميل أخطاء: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "La recarga devolvió errores: %@" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "重新載入時發生錯誤:%@" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "重新加载时出现错误:%@" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "다시 불러오기 중 오류 발생: %@" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "再読み込みでエラーが発生しました: %@" + } + } + } + }, + "projectPanel.dismissReloadErrors": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Dismiss Errors" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Fehler ausblenden" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ignorer les erreurs" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "تجاهل الأخطاء" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Descartar errores" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "關閉錯誤" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "关闭错误" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "오류 닫기" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "エラーを閉じる" + } + } + } + }, + "projectPanel.scheme": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Scheme" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Schema" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Schéma" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "المخطط" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Esquema" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "方案" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "方案" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "스킴" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "スキーム" + } + } + } + }, + "projectPanel.configuration": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Configuration" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Konfiguration" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Configuration de build" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "التكوين" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Configuración" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "組態" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "配置" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "구성" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "構成" + } + } + } + }, + "projectPanel.loadingFormat": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Loading %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "%@ wird geladen" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Chargement de %@" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "جارٍ تحميل %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Cargando %@" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "正在載入 %@" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "正在加载 %@" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "%@ 불러오는 중" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "%@ を読み込み中" + } + } + } + }, + "projectPanel.failedFormat": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Failed: %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Fehlgeschlagen: %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Échec : %@" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "فشل: %@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Error: %@" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "失敗:%@" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "失败:%@" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "실패: %@" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "失敗: %@" + } + } + } + }, "settings.automation.rules.createFailed.title": { "extractionState": "manual", "localizations": { @@ -175827,6 +176299,71 @@ } } }, + "dialog.close.dontAskAgain": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Don’t ask again" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "لا تسأل مرة أخرى" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Nicht mehr fragen" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "No volver a preguntar" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ne plus demander" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "今後確認しない" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "다시 묻지 않기" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Більше не запитувати" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "不再询问" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "不再詢問" + } + } + } + }, "dialog.closeAnchor.message.lone": { "extractionState": "manual", "localizations": { @@ -227519,6 +228056,65 @@ } } }, + "globalSearch.kind.terminal": { + "extractionState": "manual", + "localizations": { + "en": { + "stringUnit": { + "state": "translated", + "value": "Terminal" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "ターミナル" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "터미널" + } + }, + "ar": { + "stringUnit": { + "state": "translated", + "value": "طرفية" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "終端" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "终端" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Terminal" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Terminal" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Terminal" + } + } + } + }, "globalSearch.kind.title": { "extractionState": "manual", "localizations": { @@ -290419,6 +291015,596 @@ } } }, + "settings.app.accentColor": { + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "لون التمييز" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Boja naglaska" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Accentfarve" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Akzentfarbe" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Accent Color" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Color de acento" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couleur d’accentuation" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Colore di evidenziazione" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "アクセントカラー" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "강조 색상" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Aksentfarge" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Kolor akcentu" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cor de destaque" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Цвет акцента" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "สีเน้น" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Vurgu Rengi" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Колір акценту" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "强调色" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "強調色" + } + } + } + }, + "settings.app.accentColor.cmux": { + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "أزرق cmux" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "cmux plava" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "cmux-blå" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "cmux-Blau" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "cmux Blue" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Azul de cmux" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Bleu cmux" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Blu cmux" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "cmux ブルー" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "cmux 파란색" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "cmux-blå" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Niebieski cmux" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Azul do cmux" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Синий cmux" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "สีน้ำเงิน cmux" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "cmux Mavisi" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Синій cmux" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "cmux 蓝" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "cmux 藍" + } + } + } + }, + "settings.app.accentColor.subtitle": { + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "لون مساحة العمل المحددة وحلقة التنبيه وحالة الوكيل وإبرازات cmux الأخرى. يتبع خيار النظام لون التمييز في macOS." + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Boja odabranog radnog prostora, prstena pažnje, statusa agenta i ostalih cmux istaknutih elemenata. Sistem prati boju naglaska macOS-a." + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Farve for det valgte arbejdsområde, opmærksomhedsringen, agentstatus og andre cmux-fremhævninger. System følger macOS-accentfarven." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Farbe des ausgewählten Workspace, des Aufmerksamkeitsrings, des Agentenstatus und anderer cmux-Hervorhebungen. „Systemfarbe“ folgt der macOS-Akzentfarbe." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Color of the selected workspace, attention ring, agent status, and other cmux highlights. System follows the macOS accent color." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Color del espacio de trabajo seleccionado, el anillo de atención, el estado del agente y otros resaltados de cmux. Sistema sigue el color de acento de macOS." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couleur de l’espace de travail sélectionné, de l’anneau d’attention, de l’état de l’agent et des autres mises en évidence de cmux. Système suit la couleur d’accentuation de macOS." + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Colore dell’area di lavoro selezionata, dell’anello di attenzione, dello stato dell’agente e degli altri elementi evidenziati di cmux. Sistema segue il colore di evidenziazione di macOS." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "選択中のワークスペース、注意リング、エージェントの状態など、cmux のハイライトの色です。「システム」は macOS のアクセントカラーに従います。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "선택한 워크스페이스, 주의 링, 에이전트 상태 등 cmux 강조 표시의 색상입니다. 시스템은 macOS 강조 색상을 따릅니다." + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Farge for det valgte arbeidsområdet, oppmerksomhetsringen, agentstatus og andre cmux-uthevinger. System følger macOS-aksentfargen." + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Kolor wybranego obszaru roboczego, pierścienia uwagi, stanu agenta i innych wyróżnień cmux. Systemowy podąża za kolorem akcentu macOS." + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cor do espaço de trabalho selecionado, do anel de atenção, do status do agente e de outros destaques do cmux. Sistema segue a cor de destaque do macOS." + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Цвет выбранного рабочего пространства, кольца внимания, статуса агента и других выделений cmux. «Системный» следует цвету акцента macOS." + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "สีของพื้นที่ทำงานที่เลือก วงแจ้งเตือน สถานะเอเจนต์ และไฮไลต์อื่นๆ ของ cmux ตัวเลือกระบบจะใช้สีเน้นของ macOS" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Seçili çalışma alanı, dikkat halkası, ajan durumu ve diğer cmux vurgularının rengi. Sistem, macOS vurgu rengini izler." + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Колір вибраного робочого простору, кільця уваги, статусу агента та інших виділень cmux. «Системний» використовує колір акценту macOS." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "所选工作区、提醒环、代理状态和其他 cmux 高亮的颜色。“系统”跟随 macOS 强调色。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "所選工作區、提醒環、代理狀態和其他 cmux 醒目提示的顏色。「系統」跟隨 macOS 強調色。" + } + } + } + }, + "settings.app.accentColor.system": { + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "النظام" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Sistem" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "System" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Systemfarbe" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "System" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Sistema" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Système" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Sistema" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "システム" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "시스템" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "System" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Systemowy" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Sistema" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Системный" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "ระบบ" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Sistem" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Системний" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "系统" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "系統" + } + } + } + }, + "settings.app.accentColor.systemToggle": { + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "لون التمييز للنظام" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "Sistemska boja naglaska" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "Systemets accentfarve" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "System-Akzentfarbe" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "System Accent Color" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Color de acento del sistema" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Couleur d’accentuation du système" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "Colore di evidenziazione di sistema" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "システムのアクセントカラー" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "시스템 강조 색상" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "Systemets aksentfarge" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "Systemowy kolor akcentu" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "Cor de destaque do sistema" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "Системный цвет акцента" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "สีเน้นของระบบ" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "Sistem Vurgu Rengi" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "Системний колір акценту" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "系统强调色" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "系統強調色" + } + } + } + }, "settings.app.appearance": { "extractionState": "manual", "localizations": { @@ -299674,360 +300860,6 @@ } } }, - "settings.app.showAgentUsage": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "عرض استخدام الوكيل في الشريط الجانبي" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Agent-Nutzung in der Seitenleiste anzeigen" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Show Agent Usage in Sidebar" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Mostrar el uso del agente en la barra lateral" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Afficher l’utilisation des agents dans la barre latérale" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "サイドバーにエージェントの使用状況を表示" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "사이드바에 에이전트 사용량 표시" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "在侧边栏显示代理用量" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "在側邊欄顯示代理用量" - } - } - } - }, - "settings.app.showAgentUsage.subtitle": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "يُلحق النموذج ونسبة نافذة السياق المستخدمة بإدخالات حالة Claude Code وCodex، بالإضافة إلى تكلفة API تقديرية لـ Claude Code. التكلفة تقدير مبني على أسعار API المعلنة، وليست فاتورة اشتراكك." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Hängt Modell und belegtes Kontextfenster an die Statuseinträge von Claude Code und Codex an, für Claude Code zusätzlich die geschätzten API-Kosten. Die Kosten sind eine Schätzung nach API-Listenpreisen, nicht deine Abo-Rechnung." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Append the model and context window used to Claude Code and Codex status entries, plus an estimated API cost for Claude Code. The cost is an API list-price estimate, not your subscription bill." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Añade el modelo y la ventana de contexto usada a las entradas de estado de Claude Code y Codex, además de un coste estimado de la API para Claude Code. El coste es una estimación según los precios de lista de la API, no la factura de tu suscripción." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Ajoute le modèle et la part de la fenêtre de contexte utilisée aux entrées d’état de Claude Code et Codex, ainsi qu’un coût API estimé pour Claude Code. Le coût est une estimation d’après les tarifs API publics, pas votre facture d’abonnement." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "Claude Code と Codex のステータス項目にモデルと使用中のコンテキストウィンドウを追加し、Claude Code には推定 API コストも表示します。コストは API の定価に基づく推定値で、サブスクリプションの請求額ではありません。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "Claude Code 및 Codex 상태 항목에 모델과 사용 중인 컨텍스트 창을 덧붙이고, Claude Code에는 예상 API 비용도 표시합니다. 비용은 API 정가 기준의 추정치이며 구독 요금이 아닙니다." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "在 Claude Code 和 Codex 的状态条目后附加模型和已用的上下文窗口,并为 Claude Code 附加估算的 API 费用。费用是根据 API 公开价格估算的,并非你的订阅账单。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "在 Claude Code 與 Codex 的狀態項目後附加模型與已用的上下文視窗,並為 Claude Code 附加估算的 API 費用。費用是依 API 公開價格估算,並非你的訂閱帳單。" - } - } - } - }, - "settings.search.alias.setting.app.show-agent-usage": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex وكيل استخدام نموذج نافذة السياق رموز نسبة تكلفة سعر تقدير الشريط الجانبي" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex Agent Nutzung Modell Kontextfenster Token Prozent Kosten Preis Schätzung Seitenleiste" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex agente uso modelo ventana de contexto tokens porcentaje coste precio estimación barra lateral" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex agent utilisation modèle fenêtre de contexte jetons pourcentage coût prix estimation barre latérale" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex エージェント 使用状況 モデル コンテキストウィンドウ トークン パーセント コスト 料金 推定 サイドバー" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex 에이전트 사용량 모델 컨텍스트 창 토큰 퍼센트 비용 가격 추정 사이드바" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex 代理 用量 模型 上下文窗口 令牌 百分比 费用 价格 估算 侧边栏" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex 代理 用量 模型 上下文視窗 權杖 百分比 費用 價格 估算 側邊欄" - } - } - } - }, - "sidebar.agentUsage.estimatedCost": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "حوالي %@" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "ca. %@" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "~%@" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "aprox. %@" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "env. %@" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "約%@" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "약 %@" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "约 %@" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "約 %@" - } - } - } - }, - "sidebar.agentUsage.estimatedCostAtLeast": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "حوالي %@+" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "ca. %@+" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "~%@+" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "aprox. %@+" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "env. %@+" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "約%@+" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "약 %@+" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "约 %@+" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "約 %@+" - } - } - } - }, - "sidebar.agentUsage.costHelp": { - "extractionState": "manual", - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "التكلفة تقدير مبني على أسعار API المعلنة، وليست فاتورة اشتراكك." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Die Kosten sind eine Schätzung nach API-Listenpreisen, nicht deine Abo-Rechnung." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Cost is an API list-price estimate, not your subscription bill." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "El coste es una estimación según los precios de lista de la API, no la factura de tu suscripción." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Le coût est une estimation d’après les tarifs API publics, pas votre facture d’abonnement." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "コストは API の定価に基づく推定値で、サブスクリプションの請求額ではありません。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "비용은 API 정가 기준의 추정치이며 구독 요금이 아닙니다." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "费用是根据 API 公开价格估算的,并非你的订阅账单。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "費用是依 API 公開價格估算,並非你的訂閱帳單。" - } - } - } - }, "settings.app.showProgress.subtitle": { "extractionState": "manual", "localizations": { @@ -343401,6 +344233,124 @@ } } }, + "settings.search.alias.setting.app.accent-color": { + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue لون التمييز أزرق النظام" + } + }, + "bs": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue boja naglaska plava sistem" + } + }, + "da": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue accentfarve blå system" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue Akzentfarbe blau System" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue color de acento azul sistema" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue couleur d’accentuation bleu système" + } + }, + "it": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue colore di evidenziazione blu sistema" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue アクセントカラー 青 システム" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue 강조 색상 파란색 시스템" + } + }, + "nb": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue aksentfarge blå system" + } + }, + "pl": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue kolor akcentu niebieski systemowy" + } + }, + "pt-BR": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue cor de destaque azul sistema" + } + }, + "ru": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue цвет акцента синий системный" + } + }, + "th": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue สีเน้น สีน้ำเงิน ระบบ" + } + }, + "tr": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue vurgu rengi mavi sistem" + } + }, + "uk": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue колір акценту синій системний" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue 强调色 蓝色 系统" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue 強調色 藍色 系統" + } + } + } + }, "settings.search.alias.setting.app.appearance": { "extractionState": "manual", "localizations": { @@ -546452,65 +547402,6 @@ } } }, - "globalSearch.kind.terminal": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Terminal" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "ターミナル" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "터미널" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "طرفية" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "終端" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "终端" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Terminal" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Terminal" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Terminal" - } - } - } - }, "terminal.passwordInput.indicator": { "extractionState": "manual", "localizations": { @@ -547278,1251 +548169,6 @@ } } }, - "browser.omnibar.accessibilityLabel": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Address and search bar" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Adress- und Suchleiste" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Barre d’adresse et de recherche" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "شريط العنوان والبحث" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Barra de direcciones y búsqueda" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "網址與搜尋列" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "地址和搜索栏" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "주소 및 검색 막대" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "アドレスと検索バー" - } - } - } - }, - "projectPanel.reload": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Reload Project" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Projekt neu laden" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Recharger le projet" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "إعادة تحميل المشروع" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Recargar proyecto" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "重新載入專案" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重新加载项目" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "프로젝트 다시 불러오기" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "プロジェクトを再読み込み" - } - } - } - }, - "projectPanel.reloadErrors": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Reload returned errors: %@" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Beim Neuladen sind Fehler aufgetreten: %@" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Le rechargement a renvoyé des erreurs : %@" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "أعادت إعادة التحميل أخطاء: %@" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "La recarga devolvió errores: %@" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "重新載入時發生錯誤:%@" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "重新加载时出现错误:%@" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "다시 불러오기 중 오류 발생: %@" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "再読み込みでエラーが発生しました: %@" - } - } - } - }, - "projectPanel.dismissReloadErrors": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Dismiss Errors" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Fehler ausblenden" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Ignorer les erreurs" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "تجاهل الأخطاء" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Descartar errores" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "關閉錯誤" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "关闭错误" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "오류 닫기" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "エラーを閉じる" - } - } - } - }, - "projectPanel.scheme": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Scheme" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Schema" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Schéma" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "المخطط" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Esquema" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "方案" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "方案" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "스킴" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "スキーム" - } - } - } - }, - "projectPanel.configuration": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Configuration" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Konfiguration" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Configuration de build" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "التكوين" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Configuración" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "組態" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "配置" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "구성" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "構成" - } - } - } - }, - "projectPanel.loadingFormat": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Loading %@" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "%@ wird geladen" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Chargement de %@" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "جارٍ تحميل %@" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Cargando %@" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "正在載入 %@" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "正在加载 %@" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "%@ 불러오는 중" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "%@ を読み込み中" - } - } - } - }, - "projectPanel.failedFormat": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Failed: %@" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Fehlgeschlagen: %@" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Échec : %@" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "فشل: %@" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Error: %@" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "失敗:%@" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "失败:%@" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "실패: %@" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "失敗: %@" - } - } - } - }, - "dialog.close.dontAskAgain": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "Don’t ask again" - } - }, - "ar": { - "stringUnit": { - "state": "translated", - "value": "لا تسأل مرة أخرى" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Nicht mehr fragen" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "No volver a preguntar" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Ne plus demander" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "今後確認しない" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "다시 묻지 않기" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Більше не запитувати" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "不再询问" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "不再詢問" - } - } - } - }, - "settings.app.accentColor": { - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "لون التمييز" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Boja naglaska" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Accentfarve" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Akzentfarbe" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Accent Color" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Color de acento" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Couleur d’accentuation" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Colore di evidenziazione" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "アクセントカラー" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "강조 색상" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Aksentfarge" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Kolor akcentu" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Cor de destaque" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Цвет акцента" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "สีเน้น" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Vurgu Rengi" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Колір акценту" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "强调色" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "強調色" - } - } - } - }, - "settings.app.accentColor.cmux": { - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "أزرق cmux" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "cmux plava" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "cmux-blå" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "cmux-Blau" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "cmux Blue" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Azul de cmux" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Bleu cmux" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Blu cmux" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "cmux ブルー" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "cmux 파란색" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "cmux-blå" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Niebieski cmux" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Azul do cmux" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Синий cmux" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "สีน้ำเงิน cmux" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "cmux Mavisi" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Синій cmux" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "cmux 蓝" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "cmux 藍" - } - } - } - }, - "settings.app.accentColor.subtitle": { - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "لون مساحة العمل المحددة وحلقة التنبيه وحالة الوكيل وإبرازات cmux الأخرى. يتبع خيار النظام لون التمييز في macOS." - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Boja odabranog radnog prostora, prstena pažnje, statusa agenta i ostalih cmux istaknutih elemenata. Sistem prati boju naglaska macOS-a." - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Farve for det valgte arbejdsområde, opmærksomhedsringen, agentstatus og andre cmux-fremhævninger. System følger macOS-accentfarven." - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Farbe des ausgewählten Workspace, des Aufmerksamkeitsrings, des Agentenstatus und anderer cmux-Hervorhebungen. „Systemfarbe“ folgt der macOS-Akzentfarbe." - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "Color of the selected workspace, attention ring, agent status, and other cmux highlights. System follows the macOS accent color." - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Color del espacio de trabajo seleccionado, el anillo de atención, el estado del agente y otros resaltados de cmux. Sistema sigue el color de acento de macOS." - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Couleur de l’espace de travail sélectionné, de l’anneau d’attention, de l’état de l’agent et des autres mises en évidence de cmux. Système suit la couleur d’accentuation de macOS." - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Colore dell’area di lavoro selezionata, dell’anello di attenzione, dello stato dell’agente e degli altri elementi evidenziati di cmux. Sistema segue il colore di evidenziazione di macOS." - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "選択中のワークスペース、注意リング、エージェントの状態など、cmux のハイライトの色です。「システム」は macOS のアクセントカラーに従います。" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "선택한 워크스페이스, 주의 링, 에이전트 상태 등 cmux 강조 표시의 색상입니다. 시스템은 macOS 강조 색상을 따릅니다." - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Farge for det valgte arbeidsområdet, oppmerksomhetsringen, agentstatus og andre cmux-uthevinger. System følger macOS-aksentfargen." - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Kolor wybranego obszaru roboczego, pierścienia uwagi, stanu agenta i innych wyróżnień cmux. Systemowy podąża za kolorem akcentu macOS." - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Cor do espaço de trabalho selecionado, do anel de atenção, do status do agente e de outros destaques do cmux. Sistema segue a cor de destaque do macOS." - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Цвет выбранного рабочего пространства, кольца внимания, статуса агента и других выделений cmux. «Системный» следует цвету акцента macOS." - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "สีของพื้นที่ทำงานที่เลือก วงแจ้งเตือน สถานะเอเจนต์ และไฮไลต์อื่นๆ ของ cmux ตัวเลือกระบบจะใช้สีเน้นของ macOS" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Seçili çalışma alanı, dikkat halkası, ajan durumu ve diğer cmux vurgularının rengi. Sistem, macOS vurgu rengini izler." - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Колір вибраного робочого простору, кільця уваги, статусу агента та інших виділень cmux. «Системний» використовує колір акценту macOS." - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "所选工作区、提醒环、代理状态和其他 cmux 高亮的颜色。“系统”跟随 macOS 强调色。" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "所選工作區、提醒環、代理狀態和其他 cmux 醒目提示的顏色。「系統」跟隨 macOS 強調色。" - } - } - } - }, - "settings.app.accentColor.system": { - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "النظام" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Sistem" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "System" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "Systemfarbe" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "System" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Sistema" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Système" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Sistema" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "システム" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "시스템" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "System" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Systemowy" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Sistema" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Системный" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "ระบบ" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Sistem" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Системний" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "系统" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "系統" - } - } - } - }, - "settings.app.accentColor.systemToggle": { - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "لون التمييز للنظام" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "Sistemska boja naglaska" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "Systemets accentfarve" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "System-Akzentfarbe" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "System Accent Color" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "Color de acento del sistema" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "Couleur d’accentuation du système" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "Colore di evidenziazione di sistema" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "システムのアクセントカラー" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "시스템 강조 색상" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "Systemets aksentfarge" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "Systemowy kolor akcentu" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "Cor de destaque do sistema" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "Системный цвет акцента" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "สีเน้นของระบบ" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "Sistem Vurgu Rengi" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "Системний колір акценту" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "系统强调色" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "系統強調色" - } - } - } - }, - "settings.search.alias.setting.app.accent-color": { - "localizations": { - "ar": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue لون التمييز أزرق النظام" - } - }, - "bs": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue boja naglaska plava sistem" - } - }, - "da": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue accentfarve blå system" - } - }, - "de": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue Akzentfarbe blau System" - } - }, - "en": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue" - } - }, - "es": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue color de acento azul sistema" - } - }, - "fr": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue couleur d’accentuation bleu système" - } - }, - "it": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue colore di evidenziazione blu sistema" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue アクセントカラー 青 システム" - } - }, - "ko": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue 강조 색상 파란색 시스템" - } - }, - "nb": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue aksentfarge blå system" - } - }, - "pl": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue kolor akcentu niebieski systemowy" - } - }, - "pt-BR": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue cor de destaque azul sistema" - } - }, - "ru": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue цвет акцента синий системный" - } - }, - "th": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue สีเน้น สีน้ำเงิน ระบบ" - } - }, - "tr": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue vurgu rengi mavi sistem" - } - }, - "uk": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue колір акценту синій системний" - } - }, - "zh-Hans": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue 强调色 蓝色 系统" - } - }, - "zh-Hant": { - "stringUnit": { - "state": "translated", - "value": "app.accentColor accent color highlight tint blue purple system accent macOS accent cmux blue 強調色 藍色 系統" - } - } - } - }, "command.shortcutKeymap.title": { "extractionState": "manual", "localizations": { @@ -566507,6 +566153,360 @@ } } } + }, + "settings.app.showAgentUsage": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "عرض استخدام الوكيل في الشريط الجانبي" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Agent-Nutzung in der Seitenleiste anzeigen" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Show Agent Usage in Sidebar" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Mostrar el uso del agente en la barra lateral" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Afficher l’utilisation des agents dans la barre latérale" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "サイドバーにエージェントの使用状況を表示" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "사이드바에 에이전트 사용량 표시" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "在侧边栏显示代理用量" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "在側邊欄顯示代理用量" + } + } + } + }, + "settings.app.showAgentUsage.subtitle": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "يُلحق النموذج ونسبة نافذة السياق المستخدمة بإدخالات حالة Claude Code وCodex، بالإضافة إلى تكلفة API تقديرية لـ Claude Code. التكلفة تقدير مبني على أسعار API المعلنة، وليست فاتورة اشتراكك." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Hängt Modell und belegtes Kontextfenster an die Statuseinträge von Claude Code und Codex an, für Claude Code zusätzlich die geschätzten API-Kosten. Die Kosten sind eine Schätzung nach API-Listenpreisen, nicht deine Abo-Rechnung." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Append the model and context window used to Claude Code and Codex status entries, plus an estimated API cost for Claude Code. The cost is an API list-price estimate, not your subscription bill." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "Añade el modelo y la ventana de contexto usada a las entradas de estado de Claude Code y Codex, además de un coste estimado de la API para Claude Code. El coste es una estimación según los precios de lista de la API, no la factura de tu suscripción." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Ajoute le modèle et la part de la fenêtre de contexte utilisée aux entrées d’état de Claude Code et Codex, ainsi qu’un coût API estimé pour Claude Code. Le coût est une estimation d’après les tarifs API publics, pas votre facture d’abonnement." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "Claude Code と Codex のステータス項目にモデルと使用中のコンテキストウィンドウを追加し、Claude Code には推定 API コストも表示します。コストは API の定価に基づく推定値で、サブスクリプションの請求額ではありません。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "Claude Code 및 Codex 상태 항목에 모델과 사용 중인 컨텍스트 창을 덧붙이고, Claude Code에는 예상 API 비용도 표시합니다. 비용은 API 정가 기준의 추정치이며 구독 요금이 아닙니다." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "在 Claude Code 和 Codex 的状态条目后附加模型和已用的上下文窗口,并为 Claude Code 附加估算的 API 费用。费用是根据 API 公开价格估算的,并非你的订阅账单。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "在 Claude Code 與 Codex 的狀態項目後附加模型與已用的上下文視窗,並為 Claude Code 附加估算的 API 費用。費用是依 API 公開價格估算,並非你的訂閱帳單。" + } + } + } + }, + "settings.search.alias.setting.app.show-agent-usage": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex وكيل استخدام نموذج نافذة السياق رموز نسبة تكلفة سعر تقدير الشريط الجانبي" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex Agent Nutzung Modell Kontextfenster Token Prozent Kosten Preis Schätzung Seitenleiste" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex agente uso modelo ventana de contexto tokens porcentaje coste precio estimación barra lateral" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex agent utilisation modèle fenêtre de contexte jetons pourcentage coût prix estimation barre latérale" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex エージェント 使用状況 モデル コンテキストウィンドウ トークン パーセント コスト 料金 推定 サイドバー" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex 에이전트 사용량 모델 컨텍스트 창 토큰 퍼센트 비용 가격 추정 사이드바" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex 代理 用量 模型 上下文窗口 令牌 百分比 费用 价格 估算 侧边栏" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "sidebar.showAgentUsage agent usage model context window tokens percent cost price estimate spend claude codex 代理 用量 模型 上下文視窗 權杖 百分比 費用 價格 估算 側邊欄" + } + } + } + }, + "sidebar.agentUsage.estimatedCost": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "حوالي %@" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "ca. %@" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "~%@" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "aprox. %@" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "env. %@" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "約%@" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "약 %@" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "约 %@" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "約 %@" + } + } + } + }, + "sidebar.agentUsage.estimatedCostAtLeast": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "حوالي %@+" + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "ca. %@+" + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "~%@+" + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "aprox. %@+" + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "env. %@+" + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "約%@+" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "약 %@+" + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "约 %@+" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "約 %@+" + } + } + } + }, + "sidebar.agentUsage.costHelp": { + "extractionState": "manual", + "localizations": { + "ar": { + "stringUnit": { + "state": "translated", + "value": "التكلفة تقدير مبني على أسعار API المعلنة، وليست فاتورة اشتراكك." + } + }, + "de": { + "stringUnit": { + "state": "translated", + "value": "Die Kosten sind eine Schätzung nach API-Listenpreisen, nicht deine Abo-Rechnung." + } + }, + "en": { + "stringUnit": { + "state": "translated", + "value": "Cost is an API list-price estimate, not your subscription bill." + } + }, + "es": { + "stringUnit": { + "state": "translated", + "value": "El coste es una estimación según los precios de lista de la API, no la factura de tu suscripción." + } + }, + "fr": { + "stringUnit": { + "state": "translated", + "value": "Le coût est une estimation d’après les tarifs API publics, pas votre facture d’abonnement." + } + }, + "ja": { + "stringUnit": { + "state": "translated", + "value": "コストは API の定価に基づく推定値で、サブスクリプションの請求額ではありません。" + } + }, + "ko": { + "stringUnit": { + "state": "translated", + "value": "비용은 API 정가 기준의 추정치이며 구독 요금이 아닙니다." + } + }, + "zh-Hans": { + "stringUnit": { + "state": "translated", + "value": "费用是根据 API 公开价格估算的,并非你的订阅账单。" + } + }, + "zh-Hant": { + "stringUnit": { + "state": "translated", + "value": "費用是依 API 公開價格估算,並非你的訂閱帳單。" + } + } + } } }, "version": "1.0" From 14af25d19150a99418caf381eb4890b6f538cb28 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Mon, 28 Sep 2026 02:48:23 -0700 Subject: [PATCH 08/13] Localizable.xcstrings: restore two settings subtitles the catch-up merge reverted The merge of main into this branch resolved Resources/Localizable.xcstrings without the repository's key-wise merge driver, so it kept this branch's older copy of two keys instead of main's newer one. That silently dropped a sentence from settings.terminal.showPasswordInputDots.subtitle and settings.terminal.showPasswordInputIndicator.subtitle in all nine languages: the note that pasted text is not counted, and the note that only local password prompts are detected so ssh's own prompt counts but sudo inside an ssh session does not. Merging the branch in that state would have reverted both sentences. Both keys now match main byte for byte. The catalog is main's content plus only the six agent-usage keys this branch adds, so a later merge of main sees no disagreement on these keys at all. Co-Authored-By: Claude Opus 5 --- Resources/Localizable.xcstrings | 36 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 831bd0c73509..51c42a69ac3a 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -555137,55 +555137,55 @@ "en": { "stringUnit": { "state": "translated", - "value": "Shows a lock badge in the terminal corner while a program such as sudo or ssh reads a password with echo off." + "value": "Shows a lock badge in the terminal corner while a program such as sudo or ssh reads a password with echo off. Only local prompts are detected: ssh's own password prompt counts, but sudo inside an ssh session does not." } }, "de": { "stringUnit": { "state": "translated", - "value": "Zeigt in der Terminalecke ein Schloss-Symbol an, während ein Programm wie sudo oder ssh ein Passwort ohne Echo einliest." + "value": "Zeigt in der Terminalecke ein Schloss-Symbol an, während ein Programm wie sudo oder ssh ein Passwort ohne Echo einliest. Nur lokale Abfragen werden erkannt: Die Passwortabfrage von ssh selbst zählt, sudo innerhalb einer ssh-Sitzung nicht." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Affiche un badge de cadenas dans le coin du terminal pendant qu'un programme comme sudo ou ssh lit un mot de passe sans écho." + "value": "Affiche un badge de cadenas dans le coin du terminal pendant qu'un programme comme sudo ou ssh lit un mot de passe sans écho. Seules les invites locales sont détectées : l'invite de mot de passe de ssh compte, mais pas sudo dans une session ssh." } }, "ar": { "stringUnit": { "state": "translated", - "value": "يعرض شارة قفل في زاوية الطرفية أثناء قراءة برنامج مثل sudo أو ssh لكلمة مرور مع إيقاف الصدى." + "value": "يعرض شارة قفل في زاوية الطرفية أثناء قراءة برنامج مثل sudo أو ssh لكلمة مرور مع إيقاف الصدى. تُكتشف المطالبات المحلية فقط: تُحتسب مطالبة كلمة مرور ssh نفسها، أما sudo داخل جلسة ssh فلا." } }, "es": { "stringUnit": { "state": "translated", - "value": "Muestra una insignia de candado en la esquina del terminal mientras un programa como sudo o ssh lee una contraseña sin eco." + "value": "Muestra una insignia de candado en la esquina del terminal mientras un programa como sudo o ssh lee una contraseña sin eco. Solo se detectan las solicitudes locales: la solicitud de contraseña de ssh cuenta, pero sudo dentro de una sesión ssh no." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "當 sudo 或 ssh 等程式在關閉回顯的情況下讀取密碼時,在終端機角落顯示鎖頭標記。" + "value": "當 sudo 或 ssh 等程式在關閉回顯的情況下讀取密碼時,在終端機角落顯示鎖頭標記。僅偵測本機提示:ssh 本身的密碼提示會被偵測,但 ssh 工作階段中的 sudo 不會。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "当 sudo 或 ssh 等程序在关闭回显的情况下读取密码时,在终端角落显示锁形标记。" + "value": "当 sudo 或 ssh 等程序在关闭回显的情况下读取密码时,在终端角落显示锁形标记。仅检测本地提示:ssh 自身的密码提示会被检测,但 ssh 会话中的 sudo 不会。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "sudo나 ssh 같은 프로그램이 에코를 끈 상태로 비밀번호를 읽는 동안 터미널 모서리에 자물쇠 배지를 표시합니다." + "value": "sudo나 ssh 같은 프로그램이 에코를 끈 상태로 비밀번호를 읽는 동안 터미널 모서리에 자물쇠 배지를 표시합니다. 로컬 프롬프트만 감지됩니다. ssh 자체의 비밀번호 프롬프트는 포함되지만 ssh 세션 안의 sudo는 포함되지 않습니다." } }, "ja": { "stringUnit": { "state": "translated", - "value": "sudo や ssh などのプログラムがエコーをオフにしてパスワードを読み取っている間、ターミナルの隅に鍵のバッジを表示します。" + "value": "sudo や ssh などのプログラムがエコーをオフにしてパスワードを読み取っている間、ターミナルの隅に鍵のバッジを表示します。検出されるのはローカルのプロンプトだけです。ssh 自体のパスワード入力は対象ですが、ssh セッション内の sudo は対象外です。" } } } @@ -555196,55 +555196,55 @@ "en": { "stringUnit": { "state": "translated", - "value": "Shows one dot in the badge per typed character. cmux keeps only a count, never the characters." + "value": "Shows one dot in the badge per typed character. cmux keeps only a count, never the characters. Pasted text is not counted." } }, "de": { "stringUnit": { "state": "translated", - "value": "Zeigt im Symbol einen Punkt pro eingegebenem Zeichen. cmux speichert nur die Anzahl, niemals die Zeichen." + "value": "Zeigt im Symbol einen Punkt pro eingegebenem Zeichen. cmux speichert nur die Anzahl, niemals die Zeichen. Eingefügter Text wird nicht gezählt." } }, "fr": { "stringUnit": { "state": "translated", - "value": "Affiche dans le badge un point par caractère saisi. cmux ne conserve qu'un compteur, jamais les caractères." + "value": "Affiche dans le badge un point par caractère saisi. cmux ne conserve qu'un compteur, jamais les caractères. Le texte collé n'est pas compté." } }, "ar": { "stringUnit": { "state": "translated", - "value": "يعرض في الشارة نقطة واحدة لكل حرف مكتوب. يحتفظ cmux بالعدد فقط، ولا يحتفظ بالأحرف أبدًا." + "value": "يعرض في الشارة نقطة واحدة لكل حرف مكتوب. يحتفظ cmux بالعدد فقط، ولا يحتفظ بالأحرف أبدًا. لا يُحتسب النص الملصق." } }, "es": { "stringUnit": { "state": "translated", - "value": "Muestra en la insignia un punto por cada carácter escrito. cmux solo guarda un recuento, nunca los caracteres." + "value": "Muestra en la insignia un punto por cada carácter escrito. cmux solo guarda un recuento, nunca los caracteres. El texto pegado no se cuenta." } }, "zh-Hant": { "stringUnit": { "state": "translated", - "value": "在標記中為每個已輸入的字元顯示一個圓點。cmux 只保留數量,絕不保留字元本身。" + "value": "在標記中為每個已輸入的字元顯示一個圓點。cmux 只保留數量,絕不保留字元本身。貼上的文字不計入。" } }, "zh-Hans": { "stringUnit": { "state": "translated", - "value": "在标记中为每个已输入的字符显示一个圆点。cmux 只保留数量,绝不保留字符本身。" + "value": "在标记中为每个已输入的字符显示一个圆点。cmux 只保留数量,绝不保留字符本身。粘贴的文本不计入。" } }, "ko": { "stringUnit": { "state": "translated", - "value": "배지에 입력한 문자마다 점을 하나씩 표시합니다. cmux는 개수만 유지하며 문자는 절대 보관하지 않습니다." + "value": "배지에 입력한 문자마다 점을 하나씩 표시합니다. cmux는 개수만 유지하며 문자는 절대 보관하지 않습니다. 붙여넣은 텍스트는 세지 않습니다." } }, "ja": { "stringUnit": { "state": "translated", - "value": "入力した文字ごとにバッジにドットを 1 つ表示します。cmux が保持するのは数だけで、文字そのものは保持しません。" + "value": "入力した文字ごとにバッジにドットを 1 つ表示します。cmux が保持するのは数だけで、文字そのものは保持しません。貼り付けたテキストは数えません。" } } } From 8cd4f9f4931cc3f5420dbc1ce7e0ba7456346df3 Mon Sep 17 00:00:00 2001 From: Leo Li Date: Mon, 28 Sep 2026 03:16:33 -0700 Subject: [PATCH 09/13] Address review: name legacy Claude models, prove the off path schedules nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pre-merge review found one user-visible defect and one hole in the tests. Legacy Claude ids put the version before the family name (`3-5-sonnet`), so `claudeDisplayName` returned nil for them and the row fell back to the whole raw id. On Bedrock that meant a status line reading `Running · us.anthropic.claude-3-7-sonnet-20250219-v1:0 · …`, and because usage is appended to a line that truncates at the tail, the context percentage and the cost this feature exists to show were the first things cut. The display name is now taken from the first component that starts with a letter, with the numbers on either side of it joined by dots, which covers both id orders: `3-5-sonnet` reads `Sonnet 3.5` and `opus-4-8` still reads `Opus 4.8`. Legacy ids stay unpriced. Five of them are now asserted by name. The off-path test only checked that no usage was shown, which would also hold if the transcript had been read and the result thrown away. The coordinator now counts scheduled samples and the test asserts the count is zero while the setting is off, so the property the feature rests on, that nothing is read until you ask for it, is what the test actually measures. Also from the review, both cheap: - `SidebarAgentUsageFormatter()` reads `Locale.current` and was built for every row on every sidebar rebuild even with usage off. It is built only when usage is shown. - `isEnabled` declared a fresh `SidebarCatalogSection`, building all of its keys, on every `UserDefaults` write anywhere in the app. It is built once. - Which session owns a row was decided by `max` over equal `lastEventAt` values, whose result among equal maxima is unspecified. Two panes of one workspace can share a timestamp, so the row could flip between their numbers with no user action. The session id now breaks the tie. Co-Authored-By: Claude Opus 5 --- .../Usage/AgentModelCatalog.swift | 18 ++++++++++--- .../AgentModelCatalogTests.swift | 20 +++++++++++--- .../SidebarAgentUsageCoordinator.swift | 26 ++++++++++++++++--- Sources/SidebarWorkspaceSnapshotFactory.swift | 15 ++++++----- cmuxTests/SidebarAgentUsageTests.swift | 7 ++++- 5 files changed, 68 insertions(+), 18 deletions(-) diff --git a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelCatalog.swift b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelCatalog.swift index 0f4fe445cfb6..d5d9a5fd0b41 100644 --- a/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelCatalog.swift +++ b/Packages/Shared/CmuxAgentChat/Sources/CmuxAgentChat/Usage/AgentModelCatalog.swift @@ -103,12 +103,22 @@ public struct AgentModelCatalog: Sendable { return parts.joined(separator: "-") } - /// `opus-4-8` → `Opus 4.8`. Returns `nil` for legacy ids such as - /// `3-5-sonnet` whose first component is not a family name. + /// `opus-4-8` → `Opus 4.8`, and the legacy order `3-5-sonnet` → `Sonnet + /// 3.5`. Current ids put the name first and the version after it; ids up + /// to Claude 3.7 put the version first. Both forms are named after the + /// first component that starts with a letter, with the numbers on either + /// side of it joined by dots. Returns `nil` only when no component starts + /// with a letter. static func claudeDisplayName(family: String) -> String? { let parts = family.split(separator: "-") - guard let name = parts.first, name.first?.isLetter == true else { return nil } - let version = parts.dropFirst().prefix { $0.allSatisfy(\.isNumber) } + guard let nameIndex = parts.firstIndex(where: { $0.first?.isLetter == true }) else { + return nil + } + let name = parts[nameIndex] + let leadingVersion = parts[..