From acb62263f4c57d10af3ae38dc5c9b548ae42abc6 Mon Sep 17 00:00:00 2001
From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
Date: Thu, 17 Sep 2026 16:34:33 -0700
Subject: [PATCH 1/9] feat(cloud): brand agent terminal icons by provider
---
Assets.xcassets/AgentIcons/ATTRIBUTIONS.md | 5 ++
.../CodeBuddy.imageset/CodeBuddy.svg | 1 +
.../CodeBuddy.imageset/Contents.json | 15 ++++++
.../AgentIcons/Copilot.imageset/Contents.json | 25 +++++++++
.../Copilot.imageset/Copilot-dark.svg | 1 +
.../AgentIcons/Copilot.imageset/Copilot.svg | 1 +
.../AgentIcons/Cursor.imageset/Contents.json | 25 +++++++++
.../Cursor.imageset/Cursor-dark.svg | 1 +
.../AgentIcons/Cursor.imageset/Cursor.svg | 1 +
.../AgentIcons/Factory.imageset/Contents.json | 15 ++++++
.../AgentIcons/Factory.imageset/Factory.svg | 8 +++
.../AgentIcons/Gemini.imageset/Contents.json | 15 ++++++
.../AgentIcons/Gemini.imageset/Gemini.svg | 1 +
.../AgentIcons/Kimi.imageset/Contents.json | 15 ++++++
.../AgentIcons/Kimi.imageset/Kimi.svg | 1 +
.../AgentIcons/Kiro.imageset/Contents.json | 15 ++++++
.../AgentIcons/Kiro.imageset/Kiro.svg | 1 +
Assets.xcassets/AgentIcons/LOBE-LICENSE.txt | 21 ++++++++
.../AgentIcons/Ollama.imageset/Contents.json | 25 +++++++++
.../Ollama.imageset/Ollama-dark.svg | 1 +
.../AgentIcons/Ollama.imageset/Ollama.svg | 1 +
.../AgentIcons/Qoder.imageset/Contents.json | 25 +++++++++
.../AgentIcons/Qoder.imageset/Qoder-dark.svg | 1 +
.../AgentIcons/Qoder.imageset/Qoder.svg | 1 +
Sources/Cloud/CloudTreeRowContentView.swift | 18 +++++--
Sources/Cloud/CloudTreeRowIcon.swift | 20 +++++++
...anagerCodingAgentDefinition+BuiltIns.swift | 22 ++++----
Sources/Surfaces/CmuxTuiSnapshotParser.swift | 25 +++++++--
.../CmuxTuiSurfaceProvider+ManualMirror.swift | 5 ++
.../Surfaces/SurfaceCatalog+AgentIcons.swift | 24 +++++++++
Sources/Surfaces/SurfaceCatalog.swift | 8 +++
Sources/Surfaces/SurfaceCatalogModel.swift | 3 ++
...SurfacePaneFactory+CloudManualMirror.swift | 2 +
.../Surfaces/SurfaceResource+AgentIcon.swift | 18 +++++++
.../Workspace+CloudManualMirror.swift | 31 ++++++++---
Sources/TerminalTabAgentIcon.swift | 52 +++++++++++++++++++
Sources/Workspace+PanelLifecycle.swift | 7 +++
Sources/Workspace+TitleOwnership.swift | 4 ++
THIRD_PARTY_LICENSES.md | 12 +++++
cmux.xcodeproj/project.pbxproj | 16 ++++++
cmuxTests/CloudSidebarConsistencyTests.swift | 20 +++++++
cmuxTests/CmuxTuiSurfaceProviderTests.swift | 10 ++++
42 files changed, 491 insertions(+), 27 deletions(-)
create mode 100644 Assets.xcassets/AgentIcons/ATTRIBUTIONS.md
create mode 100644 Assets.xcassets/AgentIcons/CodeBuddy.imageset/CodeBuddy.svg
create mode 100644 Assets.xcassets/AgentIcons/CodeBuddy.imageset/Contents.json
create mode 100644 Assets.xcassets/AgentIcons/Copilot.imageset/Contents.json
create mode 100644 Assets.xcassets/AgentIcons/Copilot.imageset/Copilot-dark.svg
create mode 100644 Assets.xcassets/AgentIcons/Copilot.imageset/Copilot.svg
create mode 100644 Assets.xcassets/AgentIcons/Cursor.imageset/Contents.json
create mode 100644 Assets.xcassets/AgentIcons/Cursor.imageset/Cursor-dark.svg
create mode 100644 Assets.xcassets/AgentIcons/Cursor.imageset/Cursor.svg
create mode 100644 Assets.xcassets/AgentIcons/Factory.imageset/Contents.json
create mode 100644 Assets.xcassets/AgentIcons/Factory.imageset/Factory.svg
create mode 100644 Assets.xcassets/AgentIcons/Gemini.imageset/Contents.json
create mode 100644 Assets.xcassets/AgentIcons/Gemini.imageset/Gemini.svg
create mode 100644 Assets.xcassets/AgentIcons/Kimi.imageset/Contents.json
create mode 100644 Assets.xcassets/AgentIcons/Kimi.imageset/Kimi.svg
create mode 100644 Assets.xcassets/AgentIcons/Kiro.imageset/Contents.json
create mode 100644 Assets.xcassets/AgentIcons/Kiro.imageset/Kiro.svg
create mode 100644 Assets.xcassets/AgentIcons/LOBE-LICENSE.txt
create mode 100644 Assets.xcassets/AgentIcons/Ollama.imageset/Contents.json
create mode 100644 Assets.xcassets/AgentIcons/Ollama.imageset/Ollama-dark.svg
create mode 100644 Assets.xcassets/AgentIcons/Ollama.imageset/Ollama.svg
create mode 100644 Assets.xcassets/AgentIcons/Qoder.imageset/Contents.json
create mode 100644 Assets.xcassets/AgentIcons/Qoder.imageset/Qoder-dark.svg
create mode 100644 Assets.xcassets/AgentIcons/Qoder.imageset/Qoder.svg
create mode 100644 Sources/Surfaces/SurfaceCatalog+AgentIcons.swift
create mode 100644 Sources/Surfaces/SurfaceResource+AgentIcon.swift
create mode 100644 Sources/TerminalTabAgentIcon.swift
diff --git a/Assets.xcassets/AgentIcons/ATTRIBUTIONS.md b/Assets.xcassets/AgentIcons/ATTRIBUTIONS.md
new file mode 100644
index 000000000000..0f5418c1b525
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/ATTRIBUTIONS.md
@@ -0,0 +1,5 @@
+# Agent icons
+
+Cursor, Gemini, Kiro, GitHub Copilot, CodeBuddy, Qoder, Kimi and Ollama SVGs come from [Lobe Icons](https://github.com/lobehub/lobe-icons/tree/a94750e3f5f8fc33757b839d85030e742284e43a/packages/static-svg/icons) under the MIT license in `LOBE-LICENSE.txt`. Original SVG paths are unchanged; currentColor is replaced with black and white for light and dark appearances.
+
+Factory uses its [published site icon](https://factory.ai/icon.svg), retrieved on 2026-09-17. The redundant outer SVG wrapper is removed for asset-catalog compatibility.
diff --git a/Assets.xcassets/AgentIcons/CodeBuddy.imageset/CodeBuddy.svg b/Assets.xcassets/AgentIcons/CodeBuddy.imageset/CodeBuddy.svg
new file mode 100644
index 000000000000..ec353e26418e
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/CodeBuddy.imageset/CodeBuddy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Assets.xcassets/AgentIcons/CodeBuddy.imageset/Contents.json b/Assets.xcassets/AgentIcons/CodeBuddy.imageset/Contents.json
new file mode 100644
index 000000000000..4700385540b8
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/CodeBuddy.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images": [
+ {
+ "filename": "CodeBuddy.svg",
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ },
+ "properties": {
+ "preserves-vector-representation": true
+ }
+}
diff --git a/Assets.xcassets/AgentIcons/Copilot.imageset/Contents.json b/Assets.xcassets/AgentIcons/Copilot.imageset/Contents.json
new file mode 100644
index 000000000000..29834aaeedf3
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Copilot.imageset/Contents.json
@@ -0,0 +1,25 @@
+{
+ "images": [
+ {
+ "filename": "Copilot.svg",
+ "idiom": "universal"
+ },
+ {
+ "filename": "Copilot-dark.svg",
+ "idiom": "universal",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ]
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ },
+ "properties": {
+ "preserves-vector-representation": true
+ }
+}
diff --git a/Assets.xcassets/AgentIcons/Copilot.imageset/Copilot-dark.svg b/Assets.xcassets/AgentIcons/Copilot.imageset/Copilot-dark.svg
new file mode 100644
index 000000000000..ef5e74f4541f
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Copilot.imageset/Copilot-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Assets.xcassets/AgentIcons/Copilot.imageset/Copilot.svg b/Assets.xcassets/AgentIcons/Copilot.imageset/Copilot.svg
new file mode 100644
index 000000000000..fd0bc9ed7aa2
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Copilot.imageset/Copilot.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Assets.xcassets/AgentIcons/Cursor.imageset/Contents.json b/Assets.xcassets/AgentIcons/Cursor.imageset/Contents.json
new file mode 100644
index 000000000000..fefa738bedee
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Cursor.imageset/Contents.json
@@ -0,0 +1,25 @@
+{
+ "images": [
+ {
+ "filename": "Cursor.svg",
+ "idiom": "universal"
+ },
+ {
+ "filename": "Cursor-dark.svg",
+ "idiom": "universal",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ]
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ },
+ "properties": {
+ "preserves-vector-representation": true
+ }
+}
diff --git a/Assets.xcassets/AgentIcons/Cursor.imageset/Cursor-dark.svg b/Assets.xcassets/AgentIcons/Cursor.imageset/Cursor-dark.svg
new file mode 100644
index 000000000000..4b6f0ed522cc
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Cursor.imageset/Cursor-dark.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Assets.xcassets/AgentIcons/Cursor.imageset/Cursor.svg b/Assets.xcassets/AgentIcons/Cursor.imageset/Cursor.svg
new file mode 100644
index 000000000000..f0cb42874a75
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Cursor.imageset/Cursor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Assets.xcassets/AgentIcons/Factory.imageset/Contents.json b/Assets.xcassets/AgentIcons/Factory.imageset/Contents.json
new file mode 100644
index 000000000000..efc8a52ab5a1
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Factory.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images": [
+ {
+ "filename": "Factory.svg",
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ },
+ "properties": {
+ "preserves-vector-representation": true
+ }
+}
diff --git a/Assets.xcassets/AgentIcons/Factory.imageset/Factory.svg b/Assets.xcassets/AgentIcons/Factory.imageset/Factory.svg
new file mode 100644
index 000000000000..38aad643a75e
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Factory.imageset/Factory.svg
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/Assets.xcassets/AgentIcons/Gemini.imageset/Contents.json b/Assets.xcassets/AgentIcons/Gemini.imageset/Contents.json
new file mode 100644
index 000000000000..e237b2d5b5fd
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Gemini.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images": [
+ {
+ "filename": "Gemini.svg",
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ },
+ "properties": {
+ "preserves-vector-representation": true
+ }
+}
diff --git a/Assets.xcassets/AgentIcons/Gemini.imageset/Gemini.svg b/Assets.xcassets/AgentIcons/Gemini.imageset/Gemini.svg
new file mode 100644
index 000000000000..e15f1f7efae4
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Gemini.imageset/Gemini.svg
@@ -0,0 +1 @@
+Gemini CLI
\ No newline at end of file
diff --git a/Assets.xcassets/AgentIcons/Kimi.imageset/Contents.json b/Assets.xcassets/AgentIcons/Kimi.imageset/Contents.json
new file mode 100644
index 000000000000..1eb219aa99fc
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Kimi.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images": [
+ {
+ "filename": "Kimi.svg",
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ },
+ "properties": {
+ "preserves-vector-representation": true
+ }
+}
diff --git a/Assets.xcassets/AgentIcons/Kimi.imageset/Kimi.svg b/Assets.xcassets/AgentIcons/Kimi.imageset/Kimi.svg
new file mode 100644
index 000000000000..83878fa28476
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Kimi.imageset/Kimi.svg
@@ -0,0 +1 @@
+Kimi
\ No newline at end of file
diff --git a/Assets.xcassets/AgentIcons/Kiro.imageset/Contents.json b/Assets.xcassets/AgentIcons/Kiro.imageset/Contents.json
new file mode 100644
index 000000000000..c5437ff6ad83
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Kiro.imageset/Contents.json
@@ -0,0 +1,15 @@
+{
+ "images": [
+ {
+ "filename": "Kiro.svg",
+ "idiom": "universal"
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ },
+ "properties": {
+ "preserves-vector-representation": true
+ }
+}
diff --git a/Assets.xcassets/AgentIcons/Kiro.imageset/Kiro.svg b/Assets.xcassets/AgentIcons/Kiro.imageset/Kiro.svg
new file mode 100644
index 000000000000..0c651b9747d0
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Kiro.imageset/Kiro.svg
@@ -0,0 +1 @@
+Kiro
\ No newline at end of file
diff --git a/Assets.xcassets/AgentIcons/LOBE-LICENSE.txt b/Assets.xcassets/AgentIcons/LOBE-LICENSE.txt
new file mode 100644
index 000000000000..1dd53d2a9d99
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/LOBE-LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 LobeHub
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/Assets.xcassets/AgentIcons/Ollama.imageset/Contents.json b/Assets.xcassets/AgentIcons/Ollama.imageset/Contents.json
new file mode 100644
index 000000000000..53532f35516e
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Ollama.imageset/Contents.json
@@ -0,0 +1,25 @@
+{
+ "images": [
+ {
+ "filename": "Ollama.svg",
+ "idiom": "universal"
+ },
+ {
+ "filename": "Ollama-dark.svg",
+ "idiom": "universal",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ]
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ },
+ "properties": {
+ "preserves-vector-representation": true
+ }
+}
diff --git a/Assets.xcassets/AgentIcons/Ollama.imageset/Ollama-dark.svg b/Assets.xcassets/AgentIcons/Ollama.imageset/Ollama-dark.svg
new file mode 100644
index 000000000000..a758e0f55587
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Ollama.imageset/Ollama-dark.svg
@@ -0,0 +1 @@
+Ollama
\ No newline at end of file
diff --git a/Assets.xcassets/AgentIcons/Ollama.imageset/Ollama.svg b/Assets.xcassets/AgentIcons/Ollama.imageset/Ollama.svg
new file mode 100644
index 000000000000..06947b8d698a
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Ollama.imageset/Ollama.svg
@@ -0,0 +1 @@
+Ollama
\ No newline at end of file
diff --git a/Assets.xcassets/AgentIcons/Qoder.imageset/Contents.json b/Assets.xcassets/AgentIcons/Qoder.imageset/Contents.json
new file mode 100644
index 000000000000..14cc6097d0ab
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Qoder.imageset/Contents.json
@@ -0,0 +1,25 @@
+{
+ "images": [
+ {
+ "filename": "Qoder.svg",
+ "idiom": "universal"
+ },
+ {
+ "filename": "Qoder-dark.svg",
+ "idiom": "universal",
+ "appearances": [
+ {
+ "appearance": "luminosity",
+ "value": "dark"
+ }
+ ]
+ }
+ ],
+ "info": {
+ "author": "xcode",
+ "version": 1
+ },
+ "properties": {
+ "preserves-vector-representation": true
+ }
+}
diff --git a/Assets.xcassets/AgentIcons/Qoder.imageset/Qoder-dark.svg b/Assets.xcassets/AgentIcons/Qoder.imageset/Qoder-dark.svg
new file mode 100644
index 000000000000..1301a9de4582
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Qoder.imageset/Qoder-dark.svg
@@ -0,0 +1 @@
+Qoder
\ No newline at end of file
diff --git a/Assets.xcassets/AgentIcons/Qoder.imageset/Qoder.svg b/Assets.xcassets/AgentIcons/Qoder.imageset/Qoder.svg
new file mode 100644
index 000000000000..36ffff45c37d
--- /dev/null
+++ b/Assets.xcassets/AgentIcons/Qoder.imageset/Qoder.svg
@@ -0,0 +1 @@
+Qoder
\ No newline at end of file
diff --git a/Sources/Cloud/CloudTreeRowContentView.swift b/Sources/Cloud/CloudTreeRowContentView.swift
index 03c4896d7127..40d73af58697 100644
--- a/Sources/Cloud/CloudTreeRowContentView.swift
+++ b/Sources/Cloud/CloudTreeRowContentView.swift
@@ -175,6 +175,7 @@ struct CloudTreeLeafRow: View {
let style: CloudTreeStyle
let icon: String
let tint: Color
+ var iconAsset: String? = nil
let title: String
var titleWeight: Font.Weight = .regular
var titleDimmed: Bool = false
@@ -189,6 +190,7 @@ struct CloudTreeLeafRow: View {
style: CloudTreeStyle,
icon: String,
tint: Color,
+ iconAsset: String? = nil,
title: String,
titleWeight: Font.Weight = .regular,
titleDimmed: Bool = false,
@@ -199,6 +201,7 @@ struct CloudTreeLeafRow: View {
self.style = style
self.icon = icon
self.tint = tint
+ self.iconAsset = iconAsset
self.title = title
self.titleWeight = titleWeight
self.titleDimmed = titleDimmed
@@ -210,7 +213,13 @@ struct CloudTreeLeafRow: View {
var body: some View {
HStack(alignment: .center, spacing: style.iconGap) {
if style.iconSlot > 0 {
- CloudTreeRowIcon(style: style, systemName: icon, tint: tint, dimmed: titleDimmed)
+ CloudTreeRowIcon(
+ style: style,
+ systemName: icon,
+ tint: tint,
+ assetName: iconAsset,
+ dimmed: titleDimmed
+ )
}
switch style.leafLayout {
case .twoLine:
@@ -276,6 +285,7 @@ extension CloudTreeLeafRow where Accessories == EmptyView {
style: CloudTreeStyle,
icon: String,
tint: Color,
+ iconAsset: String? = nil,
title: String,
titleWeight: Font.Weight = .regular,
titleDimmed: Bool = false,
@@ -286,6 +296,7 @@ extension CloudTreeLeafRow where Accessories == EmptyView {
style: style,
icon: icon,
tint: tint,
+ iconAsset: iconAsset,
title: title,
titleWeight: titleWeight,
titleDimmed: titleDimmed,
@@ -296,9 +307,7 @@ extension CloudTreeLeafRow where Accessories == EmptyView {
}
}
-/// A cmux-tui terminal row: lifecycle glyph, title (a dim sparkle prefix when an
-/// agent is running in it), dimmed cwd, an optional daemon-tab badge on pool
-/// rows, and a dim "open" mark when a local pane is already showing it.
+/// A cmux-tui terminal row with its provider mark, title, directory and optional view count.
struct CloudTreeTerminalRowContent: View {
let row: CloudTreeTerminalRow
var style: CloudTreeStyle = CloudTreeStyleStore.current
@@ -323,6 +332,7 @@ struct CloudTreeTerminalRowContent: View {
style: style,
icon: glyph,
tint: CloudTreeIconPalette.terminal,
+ iconAsset: terminal.terminalAgentIconAssetName,
title: row.displayTitle.isEmpty ? String(localized: "cloudTree.terminal.untitled", defaultValue: "terminal") : row.displayTitle,
titleDimmed: terminal.lifecycle == .exited || showsDetachedState,
detail: terminal.detail.flatMap { $0.isEmpty ? nil : Self.abbreviated($0) }
diff --git a/Sources/Cloud/CloudTreeRowIcon.swift b/Sources/Cloud/CloudTreeRowIcon.swift
index b50085bbc439..fe258350c7e4 100644
--- a/Sources/Cloud/CloudTreeRowIcon.swift
+++ b/Sources/Cloud/CloudTreeRowIcon.swift
@@ -1,4 +1,6 @@
import CmuxFoundation
+import AppKit
+import CmuxAppKitSupportUI
import SwiftUI
/// A row glyph in the shared icon slot, drawn per the style's icon treatment:
@@ -8,12 +10,30 @@ struct CloudTreeRowIcon: View {
let style: CloudTreeStyle
let systemName: String
let tint: Color
+ var assetName: String? = nil
var dimmed: Bool = false
var weight: Font.Weight = .regular
var size: CGFloat? = nil
@Environment(\.cmuxGlobalFontMagnificationPercent) private var magnification
var body: some View {
+ if let assetName {
+ CmuxResolvedIconImage(request: CmuxResolvedIconRequest(
+ source: .asset(name: assetName, bundle: .main),
+ size: NSSize(width: style.iconSize, height: style.iconSize),
+ fallbackSource: .systemSymbol(name: systemName, accessibilityDescription: nil),
+ fallbackTintColor: .secondaryLabelColor
+ ))
+ .frame(width: style.iconSlot, height: style.iconSize, alignment: .center)
+ .opacity(dimmed ? 0.45 : 1)
+ .accessibilityHidden(true)
+ } else {
+ systemIcon
+ }
+ }
+
+ @ViewBuilder
+ private var systemIcon: some View {
switch style.iconTreatment {
case .monochrome:
Image(systemName: systemName)
diff --git a/Sources/CmuxTaskManagerCodingAgentDefinition+BuiltIns.swift b/Sources/CmuxTaskManagerCodingAgentDefinition+BuiltIns.swift
index 771eb9605878..9883b3fc9cc9 100644
--- a/Sources/CmuxTaskManagerCodingAgentDefinition+BuiltIns.swift
+++ b/Sources/CmuxTaskManagerCodingAgentDefinition+BuiltIns.swift
@@ -10,7 +10,7 @@ extension CmuxTaskManagerCodingAgentDefinition {
.init(id: "codex", displayName: "Codex", assetName: "AgentIcons/Codex",
launchKinds: ["codex", "omx"], directBasenames: ["codex", "omx"],
argumentNeedles: ["codex", "@openai/codex", "oh-my-codex"]),
- .init(id: "grok", displayName: "Grok", assetName: nil,
+ .init(id: "grok", displayName: "Grok", assetName: "AgentIcons/Grok",
launchKinds: ["grok"], directBasenames: ["grok", "grok-macos-aarch64", "grok-macos-aarch"],
argumentNeedles: ["grok", "grok-build", "@xai/grok"]),
.init(id: "opencode", displayName: "OpenCode", assetName: "AgentIcons/OpenCode",
@@ -26,11 +26,11 @@ extension CmuxTaskManagerCodingAgentDefinition {
argumentNeedles: ["@mariozechner/pi-coding-agent", "pi-coding-agent"]),
.init(id: "amp", displayName: "Amp", assetName: "AgentIcons/Amp",
launchKinds: ["amp"], directBasenames: ["amp"], argumentNeedles: ["@ampcode"]),
- .init(id: "cursor", displayName: "Cursor", assetName: nil,
+ .init(id: "cursor", displayName: "Cursor", assetName: "AgentIcons/Cursor",
launchKinds: ["cursor"], directBasenames: ["cursor-agent"], argumentNeedles: ["cursor-agent"]),
- .init(id: "gemini", displayName: "Gemini", assetName: nil,
+ .init(id: "gemini", displayName: "Gemini", assetName: "AgentIcons/Gemini",
launchKinds: ["gemini"], directBasenames: ["gemini"], argumentNeedles: ["gemini"]),
- .init(id: "kiro", displayName: "Kiro", assetName: nil,
+ .init(id: "kiro", displayName: "Kiro", assetName: "AgentIcons/Kiro",
launchKinds: ["kiro"], directBasenames: ["kiro", "kiro-cli"], argumentNeedles: ["kiro", "kiro-cli"]),
.init(id: "antigravity", displayName: "Antigravity", assetName: "AgentIcons/Antigravity",
launchKinds: ["antigravity", "agy"], directBasenames: ["agy", "antigravity"],
@@ -39,18 +39,18 @@ extension CmuxTaskManagerCodingAgentDefinition {
launchKinds: ["rovodev", "rovo"], directBasenames: ["rovodev"], argumentNeedles: ["rovodev"]),
.init(id: "hermes-agent", displayName: "Hermes Agent", assetName: "AgentIcons/HermesAgent",
launchKinds: ["hermes-agent"], directBasenames: ["hermes", "hermes-agent"], argumentNeedles: ["hermes-agent"]),
- .init(id: "copilot", displayName: "Copilot", assetName: nil,
- launchKinds: ["copilot"], directBasenames: ["copilot"], argumentNeedles: ["copilot"]),
- .init(id: "codebuddy", displayName: "CodeBuddy", assetName: nil,
+ .init(id: "copilot", displayName: "Copilot", assetName: "AgentIcons/Copilot",
+ launchKinds: ["copilot"], directBasenames: ["copilot", "github-copilot"], argumentNeedles: ["copilot"]),
+ .init(id: "codebuddy", displayName: "CodeBuddy", assetName: "AgentIcons/CodeBuddy",
launchKinds: ["codebuddy"], directBasenames: ["codebuddy"], argumentNeedles: ["codebuddy"]),
- .init(id: "factory", displayName: "Factory", assetName: nil,
+ .init(id: "factory", displayName: "Factory", assetName: "AgentIcons/Factory",
launchKinds: ["factory"], directBasenames: ["droid", "factory"], argumentNeedles: ["factory"]),
- .init(id: "qoder", displayName: "Qoder", assetName: nil,
+ .init(id: "qoder", displayName: "Qoder", assetName: "AgentIcons/Qoder",
launchKinds: ["qoder"], directBasenames: ["qoder", "qodercli"], argumentNeedles: ["qoder", "qodercli"]),
.init(
id: "kimi",
displayName: String(localized: "agent.kimi.displayName", defaultValue: "Kimi Code"),
- assetName: nil,
+ assetName: "AgentIcons/Kimi",
launchKinds: ["kimi"],
// Kimi's Python entrypoint deliberately overwrites its OS process title and argv with
// "Kimi Code". This is process-status/foreground detection only; session persistence
@@ -61,7 +61,7 @@ extension CmuxTaskManagerCodingAgentDefinition {
.init(
id: "ollama",
displayName: String(localized: "agent.ollama.displayName", defaultValue: "Ollama"),
- assetName: nil,
+ assetName: "AgentIcons/Ollama",
launchKinds: ["ollama"],
directBasenames: ["ollama"],
// No argument needles: a bare "ollama" token plus the "run"
diff --git a/Sources/Surfaces/CmuxTuiSnapshotParser.swift b/Sources/Surfaces/CmuxTuiSnapshotParser.swift
index 351aaaadc71c..e38a2b45770e 100644
--- a/Sources/Surfaces/CmuxTuiSnapshotParser.swift
+++ b/Sources/Surfaces/CmuxTuiSnapshotParser.swift
@@ -191,7 +191,15 @@ struct CmuxTuiSnapshotParser: Sendable {
}
let agents = ((snapshot["agents"] as? [[String: Any]]) ?? []).compactMap { raw -> CloudVMAgentState? in
guard let terminalID = nonEmptyString(raw["terminal_id"]), let state = nonEmptyString(raw["state"]) else { return nil }
- return CloudVMAgentState(id: nonEmptyString(raw["id"]), terminalID: terminalID, state: state, source: nonEmptyString(raw["source"]))
+ return CloudVMAgentState(
+ id: nonEmptyString(raw["id"]),
+ terminalID: terminalID,
+ state: state,
+ source: nonEmptyString(raw["source"]),
+ agent: nonEmptyString(raw["agent"])
+ ?? nonEmptyString(raw["agent_type"])
+ ?? nonEmptyString(raw["provider"])
+ )
}
return CloudVMState(
@@ -536,7 +544,7 @@ struct CmuxTuiSnapshotParser: Sendable {
lifecycle: SurfaceLifecycle(rawValue: terminal.lifecycle)
?? (terminal.running == true ? .running : .exited),
agent: state.lookupIndex.agent(terminalID: terminal.id).map {
- SurfaceAgentBadge(state: $0.state, source: $0.source)
+ SurfaceAgentBadge(state: $0.state, source: $0.source, agent: $0.agent)
},
remoteWorkspace: nil,
port: nil,
@@ -1019,7 +1027,10 @@ struct CmuxTuiSnapshotParser: Sendable {
id: nonEmptyString(value["id"]),
terminalID: terminalID,
state: state,
- source: nonEmptyString(value["source"])
+ source: nonEmptyString(value["source"]),
+ agent: nonEmptyString(value["agent"])
+ ?? nonEmptyString(value["agent_type"])
+ ?? nonEmptyString(value["provider"])
)
}
@@ -1421,7 +1432,13 @@ struct CmuxTuiSnapshotParser: Sendable {
var agentByTerminal: [String: SurfaceAgentBadge] = [:]
for agent in agentsRaw {
guard let terminalID = agent["terminal_id"] as? String, let state = agent["state"] as? String else { continue }
- agentByTerminal[terminalID] = SurfaceAgentBadge(state: state, source: agent["source"] as? String)
+ agentByTerminal[terminalID] = SurfaceAgentBadge(
+ state: state,
+ source: agent["source"] as? String,
+ agent: (agent["agent"] as? String)
+ ?? (agent["agent_type"] as? String)
+ ?? (agent["provider"] as? String)
+ )
}
let workspaces = Self.workspaces(fromSnapshot: snapshot)
diff --git a/Sources/Surfaces/CmuxTuiSurfaceProvider+ManualMirror.swift b/Sources/Surfaces/CmuxTuiSurfaceProvider+ManualMirror.swift
index 512701690b9f..be3e8068c7e2 100644
--- a/Sources/Surfaces/CmuxTuiSurfaceProvider+ManualMirror.swift
+++ b/Sources/Surfaces/CmuxTuiSurfaceProvider+ManualMirror.swift
@@ -86,6 +86,10 @@ extension CmuxTuiSurfaceProvider {
) else {
throw CancellationError()
}
+ workspace.updateCloudTerminalTabIcon(
+ panelID: adopted.panelID,
+ assetName: resource.terminalAgentIconAssetName
+ )
created = adopted
reservation.inputRelay.attach(inputRouter)
// The card's grace counts from the moment the pane appeared.
@@ -94,6 +98,7 @@ extension CmuxTuiSurfaceProvider {
created = try SurfacePaneFactory.makeCloudManualMirrorPane(
at: destination,
focus: focus,
+ iconAssetName: resource.terminalAgentIconAssetName,
onInput: { input in inputRouter.send(input) },
keyNameResolver: { RemoteTmuxKeyName(inputEvent: $0)?.value },
onResize: { [weak session] sample in
diff --git a/Sources/Surfaces/SurfaceCatalog+AgentIcons.swift b/Sources/Surfaces/SurfaceCatalog+AgentIcons.swift
new file mode 100644
index 000000000000..06042e101069
--- /dev/null
+++ b/Sources/Surfaces/SurfaceCatalog+AgentIcons.swift
@@ -0,0 +1,24 @@
+import Foundation
+
+extension SurfaceCatalog {
+ /// Visits projections once per accepted catalog transaction, independent of
+ /// the number of resources in a full snapshot. Missing resources clear icons.
+ func syncCloudTerminalTabIcons(on machine: SurfaceMachineID, affected: Set? = nil) {
+ guard !machine.isLocal else { return }
+ for projection in projections where projection.resource.machine == machine {
+ if let affected, !affected.contains(projection.resource) { continue }
+ syncCloudTerminalTabIcon(projection)
+ }
+ }
+
+ func syncCloudTerminalTabIcon(_ projection: SurfaceProjection) {
+ guard !projection.resource.machine.isLocal, projection.resource.kind == .terminal,
+ let workspace = cloudWorkspaceRenameService.environment.workspace(projection.workspaceID),
+ workspace.panels[projection.panelID] is TerminalPanel,
+ let tabID = workspace.surfaceIdFromPanelId(projection.panelID),
+ let tab = workspace.bonsplitController.tab(tabID) else { return }
+ let asset = resources[projection.resource]?.terminalAgentIconAssetName
+ guard tab.iconAsset != asset else { return }
+ workspace.bonsplitController.updateTab(tabID, iconAsset: .some(asset))
+ }
+}
diff --git a/Sources/Surfaces/SurfaceCatalog.swift b/Sources/Surfaces/SurfaceCatalog.swift
index 0e4fba7e20e7..255ac517b403 100644
--- a/Sources/Surfaces/SurfaceCatalog.swift
+++ b/Sources/Surfaces/SurfaceCatalog.swift
@@ -193,6 +193,7 @@ final class SurfaceCatalog {
machines[machine] = nil
for id in resourceIDsByMachine[machine] ?? [] { resources[id] = nil }
resourceIDsByMachine[machine] = nil
+ syncCloudTerminalTabIcons(on: machine)
pendingRestoredProjections.remove(machine: machine)
cloudWorkspaceProjectionCoordinator.cancel(machine: machine)
cloudProjectionIndexDirty = true
@@ -291,6 +292,7 @@ final class SurfaceCatalog {
}
if let info { machines[machine] = machineInfoPreservingCanonicalCloudState(info) }
resolvePendingRestoredProjections(on: machine)
+ syncCloudTerminalTabIcons(on: machine)
notifyChange()
return true
}
@@ -302,6 +304,7 @@ final class SurfaceCatalog {
resources[resource.id] = resource
resourceIDsByMachine[resource.machine, default: []].insert(resource.id)
resolvePendingRestoredProjections(on: resource.machine)
+ syncCloudTerminalTabIcons(on: resource.machine, affected: [resource.id])
notifyChange()
}
@@ -313,6 +316,7 @@ final class SurfaceCatalog {
if resourceIDsByMachine[id.machine]?.isEmpty == true {
resourceIDsByMachine[id.machine] = nil
}
+ syncCloudTerminalTabIcons(on: id.machine, affected: [id])
notifyChange()
}
@@ -452,6 +456,7 @@ final class SurfaceCatalog {
cloudStateObservations[state.machine] = observation
machines[state.machine] = machineInfoPreservingCanonicalCloudState(info, state: state)
resolvePendingRestoredProjections(on: state.machine)
+ syncCloudTerminalTabIcons(on: state.machine, affected: changed)
notifyChange()
return changed
}
@@ -489,6 +494,7 @@ final class SurfaceCatalog {
cloudStateObservations[state.machine] = observation
machines[state.machine] = machineInfoPreservingCanonicalCloudState(info, state: state)
resolvePendingRestoredProjections(on: state.machine)
+ syncCloudTerminalTabIcons(on: state.machine, affected: changed)
notifyChange()
return changed
}
@@ -533,6 +539,7 @@ final class SurfaceCatalog {
rebuildResourceIndex(for: machine)
machines[machine] = machineInfoPreservingCanonicalCloudState(info)
resolvePendingRestoredProjections(on: machine)
+ syncCloudTerminalTabIcons(on: machine)
notifyChange()
}
@@ -1066,6 +1073,7 @@ final class SurfaceCatalog {
insertSupersedingLocalPlaceholder(cloudPlacementCoordinator.projectionInCurrentWorkspace(projection))
reconcileCloudWorkspaceBinding(localWorkspaceID: projection.workspaceID)
reconcileCloudProjection(projection)
+ syncCloudTerminalTabIcon(projection)
if let resource = resources[projection.resource], !resource.machine.isLocal,
resource.kind == .terminal,
let workspace = cloudWorkspaceRenameService.environment.workspace(projection.workspaceID) {
diff --git a/Sources/Surfaces/SurfaceCatalogModel.swift b/Sources/Surfaces/SurfaceCatalogModel.swift
index 28f4a563183d..b8d5e4a0d48a 100644
--- a/Sources/Surfaces/SurfaceCatalogModel.swift
+++ b/Sources/Surfaces/SurfaceCatalogModel.swift
@@ -96,6 +96,8 @@ enum SurfaceLifecycle: String, Codable, Sendable {
struct SurfaceAgentBadge: Hashable, Codable, Sendable {
var state: String
var source: String?
+ /// The adapter identity, separate from report provenance (`hook`, `socket`, or `plugin`).
+ var agent: String? = nil
}
/// The daemon's monotonic position for one complete remote session state.
@@ -289,6 +291,7 @@ struct CloudVMAgentState: Hashable, Codable, Sendable {
var terminalID: String
var state: String
var source: String?
+ var agent: String? = nil
}
/// How a remote session can be synchronized.
diff --git a/Sources/Surfaces/SurfacePaneFactory+CloudManualMirror.swift b/Sources/Surfaces/SurfacePaneFactory+CloudManualMirror.swift
index 9f7ea1e51c9b..8ce294a9c4e9 100644
--- a/Sources/Surfaces/SurfacePaneFactory+CloudManualMirror.swift
+++ b/Sources/Surfaces/SurfacePaneFactory+CloudManualMirror.swift
@@ -13,6 +13,7 @@ extension SurfacePaneFactory {
static func makeCloudManualMirrorPane(
at destination: SurfaceDestination,
focus: Bool,
+ iconAssetName: String? = nil,
onInput: @escaping @Sendable (TerminalManualInput) -> Void,
keyNameResolver: (@MainActor @Sendable (ghostty_input_key_s) -> String?)? = nil,
onResize: @escaping @MainActor @Sendable (TerminalSurfaceRawSizingSample) -> Void,
@@ -26,6 +27,7 @@ extension SurfacePaneFactory {
return try workspace.addCloudManualMirrorPane(
at: destination,
focus: focus,
+ iconAssetName: iconAssetName,
onInput: onInput,
keyNameResolver: keyNameResolver,
onResize: onResize,
diff --git a/Sources/Surfaces/SurfaceResource+AgentIcon.swift b/Sources/Surfaces/SurfaceResource+AgentIcon.swift
new file mode 100644
index 000000000000..87bb7d1a9e5d
--- /dev/null
+++ b/Sources/Surfaces/SurfaceResource+AgentIcon.swift
@@ -0,0 +1,18 @@
+import Foundation
+
+extension SurfaceResource {
+ /// One provider identity drives the tab strip and every Cloud tree placement.
+ /// Report provenance and user-controlled terminal titles are not provider IDs.
+ var terminalAgentIconAssetName: String? {
+ guard kind == .terminal, lifecycle != .exited,
+ let badge = agent, badge.state != "done",
+ let identity = (badge.agent ?? badge.source)?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(),
+ !identity.isEmpty,
+ !["hook", "socket", "detected", "plugin", "unknown"].contains(identity) else { return nil }
+ return CmuxTaskManagerCodingAgentDefinition.builtIns.first { definition in
+ definition.id == identity
+ || definition.launchKinds.contains(identity)
+ || definition.directBasenames.contains(identity)
+ }?.assetName
+ }
+}
diff --git a/Sources/Surfaces/Workspace+CloudManualMirror.swift b/Sources/Surfaces/Workspace+CloudManualMirror.swift
index 6ee8a773065b..a586aee05003 100644
--- a/Sources/Surfaces/Workspace+CloudManualMirror.swift
+++ b/Sources/Surfaces/Workspace+CloudManualMirror.swift
@@ -28,6 +28,7 @@ extension Workspace {
func addCloudManualMirrorPane(
at destination: SurfaceDestination,
focus: Bool,
+ iconAssetName: String? = nil,
onInput: @escaping @Sendable (TerminalManualInput) -> Void,
keyNameResolver: (@MainActor @Sendable (ghostty_input_key_s) -> String?)? = nil,
onResize: @escaping @MainActor @Sendable (TerminalSurfaceRawSizingSample) -> Void,
@@ -52,7 +53,9 @@ extension Workspace {
onFocus: onFocus,
attachment: attachment
)
- let panelID = try workspace.insertCloudManualMirrorPanel(panel, at: destination, focus: focus, isLoading: false)
+ let panelID = try workspace.insertCloudManualMirrorPanel(
+ panel, at: destination, focus: focus, isLoading: false, iconAssetName: iconAssetName
+ )
return (workspace.id, panelID, panel.surface)
}
@@ -81,7 +84,8 @@ extension Workspace {
_ panel: TerminalPanel,
at destination: SurfaceDestination,
focus: Bool,
- isLoading: Bool
+ isLoading: Bool,
+ iconAssetName: String? = nil
) throws -> UUID {
switch destination {
case .workspace(_, let placement):
@@ -89,20 +93,20 @@ extension Workspace {
guard let pane else { throw SurfaceCatalogError.destinationNotFound("focused pane") }
switch placement {
case .tab:
- return try insertCloudManualMirrorTab(panel, in: pane, focus: focus, isLoading: isLoading)
+ return try insertCloudManualMirrorTab(panel, in: pane, focus: focus, isLoading: isLoading, iconAssetName: iconAssetName)
case .split:
- return try splitCloudManualMirrorPane(panel, target: pane, direction: .right, focus: focus, isLoading: isLoading)
+ return try splitCloudManualMirrorPane(panel, target: pane, direction: .right, focus: focus, isLoading: isLoading, iconAssetName: iconAssetName)
}
case .tab(_, let paneID, _):
guard let pane = Self.pane(paneID, in: self) else {
throw SurfaceCatalogError.destinationNotFound("pane (paneID)")
}
- return try insertCloudManualMirrorTab(panel, in: pane, focus: focus, isLoading: isLoading)
+ return try insertCloudManualMirrorTab(panel, in: pane, focus: focus, isLoading: isLoading, iconAssetName: iconAssetName)
case .split(_, let paneID, let direction):
guard let pane = Self.pane(paneID, in: self) else {
throw SurfaceCatalogError.destinationNotFound("pane (paneID)")
}
- return try splitCloudManualMirrorPane(panel, target: pane, direction: direction, focus: focus, isLoading: isLoading)
+ return try splitCloudManualMirrorPane(panel, target: pane, direction: direction, focus: focus, isLoading: isLoading, iconAssetName: iconAssetName)
}
}
@@ -110,13 +114,15 @@ extension Workspace {
_ panel: TerminalPanel,
in pane: PaneID,
focus: Bool,
- isLoading: Bool
+ isLoading: Bool,
+ iconAssetName: String?
) throws -> UUID {
panels[panel.id] = panel
panelTitles[panel.id] = panel.displayTitle
guard let tab = bonsplitController.createTab(
title: panel.displayTitle,
icon: panel.displayIcon,
+ iconAsset: iconAssetName,
kind: SurfaceKind.terminal.rawValue,
isDirty: panel.isDirty,
isLoading: isLoading,
@@ -143,7 +149,8 @@ extension Workspace {
target: PaneID,
direction: SurfaceSplitDirection,
focus: Bool,
- isLoading: Bool
+ isLoading: Bool,
+ iconAssetName: String?
) throws -> UUID {
let previousPane = bonsplitController.focusedPaneId
let previousTab = previousPane.flatMap { bonsplitController.selectedTab(inPane: $0)?.id }
@@ -152,6 +159,7 @@ extension Workspace {
let tab = Bonsplit.Tab(
title: panel.displayTitle,
icon: panel.displayIcon,
+ iconAsset: iconAssetName,
kind: SurfaceKind.terminal.rawValue,
isDirty: panel.isDirty,
isLoading: isLoading,
@@ -192,6 +200,13 @@ extension Workspace {
bonsplitController.updateTab(tabID, isLoading: isLoading)
}
+ /// Updates a Cloud terminal tab after the daemon reports a provider identity change.
+ func updateCloudTerminalTabIcon(panelID: UUID, assetName: String?) {
+ guard let tabID = surfaceIdFromPanelId(panelID),
+ let tab = bonsplitController.tab(tabID), tab.iconAsset != assetName else { return }
+ bonsplitController.updateTab(tabID, iconAsset: .some(assetName))
+ }
+
/// The live workspace with `id` in any window, or nil once it was retired.
static func liveWorkspace(id: UUID) -> Workspace? {
AppDelegate.shared?.tabManagerFor(tabId: id)?.tabs.first { $0.id == id }
diff --git a/Sources/TerminalTabAgentIcon.swift b/Sources/TerminalTabAgentIcon.swift
new file mode 100644
index 000000000000..79b6ae262033
--- /dev/null
+++ b/Sources/TerminalTabAgentIcon.swift
@@ -0,0 +1,52 @@
+import Bonsplit
+import Foundation
+
+/// Resolves the provider mark for a local terminal tab from the same agent
+/// definitions used by process and hook detection.
+struct TerminalTabAgentIconResolver {
+ func assetName(forStatusKey statusKey: String) -> String? {
+ let normalized = statusKey.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
+ guard !normalized.isEmpty else { return nil }
+ return CmuxTaskManagerCodingAgentDefinition.builtIns.first { definition in
+ definition.id == normalized
+ || definition.launchKinds.contains(normalized)
+ || definition.directBasenames.contains(normalized)
+ }?.assetName
+ }
+
+ func titleStatusKey(from title: String) -> String? {
+ let token = title.split(whereSeparator: { $0.isWhitespace }).first.map(String.init)?.lowercased()
+ guard let token else { return nil }
+ return assetName(forStatusKey: token) == nil ? nil : token
+ }
+}
+
+extension Workspace {
+ /// Returns the current provider mark for one terminal panel, if known.
+ func terminalTabAgentIconAsset(forPanelId panelId: UUID) -> String? {
+ let resolver = TerminalTabAgentIconResolver()
+ let statusKeys = agentPIDKeysByPanelId[panelId, default: []]
+ .map(agentStatusKey(forAgentPIDKey:))
+ .sorted()
+ if let asset = statusKeys.compactMap(resolver.assetName(forStatusKey:)).first {
+ return asset
+ }
+ if let title = panels[panelId]?.displayTitle,
+ let statusKey = resolver.titleStatusKey(from: title),
+ let asset = resolver.assetName(forStatusKey: statusKey) {
+ return asset
+ }
+ guard let restored = restoredAgentSnapshotsByPanelId[panelId] else { return nil }
+ return restored.registration?.iconAssetName ?? resolver.assetName(forStatusKey: restored.kind.rawValue)
+ }
+
+ /// Reconciles a terminal tab's provider mark after agent lifecycle state changes.
+ func syncTerminalTabAgentIconAsset(forPanelId panelId: UUID) {
+ guard panels[panelId] is TerminalPanel,
+ let tabID = surfaceIdFromPanelId(panelId),
+ let tab = bonsplitController.tab(tabID) else { return }
+ let asset = terminalTabAgentIconAsset(forPanelId: panelId)
+ guard tab.iconAsset != asset else { return }
+ bonsplitController.updateTab(tabID, iconAsset: .some(asset))
+ }
+}
diff --git a/Sources/Workspace+PanelLifecycle.swift b/Sources/Workspace+PanelLifecycle.swift
index 37e1516a071c..cc439e477223 100644
--- a/Sources/Workspace+PanelLifecycle.swift
+++ b/Sources/Workspace+PanelLifecycle.swift
@@ -196,6 +196,9 @@ extension Workspace {
}
}
if refreshPorts { refreshTrackedAgentPorts() }
+ for changedPanelID in Set([previous.panelId, panelId].compactMap { $0 }) {
+ syncTerminalTabAgentIconAsset(forPanelId: changedPanelID)
+ }
return didClearOtherStructuredAgentRuntime
}
@@ -342,12 +345,16 @@ extension Workspace {
if didChange, refreshPorts {
refreshTrackedAgentPorts()
}
+ if didChange {
+ syncTerminalTabAgentIconAsset(forPanelId: ownedPanelId ?? panelId)
+ }
return didChange
}
/// Clears a panel's restored agent snapshot and resume metadata.
func clearRestoredAgentSnapshot(panelId: UUID) {
restoredAgentLifecycle.clearSessionRestore(panelId: panelId)
+ syncTerminalTabAgentIconAsset(forPanelId: panelId)
}
func refreshTrackedAgentPorts() {
diff --git a/Sources/Workspace+TitleOwnership.swift b/Sources/Workspace+TitleOwnership.swift
index 95561dfe8c49..be70b6d0525a 100644
--- a/Sources/Workspace+TitleOwnership.swift
+++ b/Sources/Workspace+TitleOwnership.swift
@@ -152,6 +152,10 @@ extension Workspace {
}
}
+ if !isRemoteTmuxMirror {
+ syncTerminalTabAgentIconAsset(forPanelId: panelId)
+ }
+
let previousWorkspaceTitle = self.title
if applyFocusedPanelTitle(panelId: panelId) {
didMutate = true
diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md
index d91a8c81f4e8..fffba8d4e13d 100644
--- a/THIRD_PARTY_LICENSES.md
+++ b/THIRD_PARTY_LICENSES.md
@@ -4,6 +4,18 @@ cmux includes the following third-party software:
---
+## Lobe Icons (selected agent marks)
+
+- **License:** MIT License
+- **Copyright:** Copyright (c) 2023 LobeHub
+- **Source:** https://github.com/lobehub/lobe-icons/tree/a94750e3f5f8fc33757b839d85030e742284e43a/packages/static-svg/icons
+
+Selected Cursor, Gemini, Kiro, GitHub Copilot, CodeBuddy, Qoder, Kimi, and
+Ollama SVG marks are bundled under `Assets.xcassets/AgentIcons`. The complete
+license text is in `Assets.xcassets/AgentIcons/LOBE-LICENSE.txt`.
+
+---
+
## Ghostty
- **License:** MIT License
diff --git a/cmux.xcodeproj/project.pbxproj b/cmux.xcodeproj/project.pbxproj
index c5376f05448e..ac450f09ef7a 100644
--- a/cmux.xcodeproj/project.pbxproj
+++ b/cmux.xcodeproj/project.pbxproj
@@ -3079,6 +3079,7 @@
C0DE70530000000000000002 /* submit-cmux-profile in Copy CLI */ = {isa = PBXBuildFile; fileRef = C0DE70530000000000000001 /* submit-cmux-profile */; };
D7AB00000000000000B041 /* SupersededPhoneDismissBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7AB00000000000000B040 /* SupersededPhoneDismissBuffer.swift */; };
C935C833B18F4CC18711BD77 /* SurfaceAgentNaming.swift in Sources */ = {isa = PBXBuildFile; fileRef = 019F9382B99C46AAAEB9C91C /* SurfaceAgentNaming.swift */; };
+ FB3A15A096C8270E929D34B6 /* SurfaceCatalog+AgentIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECA5950A12D41190CC94F10A /* SurfaceCatalog+AgentIcons.swift */; };
F11347000000000000000007 /* SurfaceCatalog+CloudBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11347000000000000000008 /* SurfaceCatalog+CloudBinding.swift */; };
F11347000000000000000001 /* SurfaceCatalog+CloudPorts.swift in Sources */ = {isa = PBXBuildFile; fileRef = F11347000000000000000002 /* SurfaceCatalog+CloudPorts.swift */; };
E71A3B8404534D5699AA2E9B /* SurfaceCatalog+CloudRenameSubmission.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A4A80857B8A4530BD5BC498 /* SurfaceCatalog+CloudRenameSubmission.swift */; };
@@ -3111,6 +3112,7 @@
7867A0010000000000000001 /* SurfaceProjectionRestoreStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7867A0020000000000000001 /* SurfaceProjectionRestoreStore.swift */; };
EE1DE3F7E67DF1366F214AC7 /* SurfaceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAED489C1C23533995AF55EA /* SurfaceProvider.swift */; };
58FE3B39F41CF68A632CA083 /* SurfaceRemotePlacement.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA467FE5247EDFC122C88129 /* SurfaceRemotePlacement.swift */; };
+ 07AFFFB52EA184A2DE379926 /* SurfaceResource+AgentIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0314DFABAF2F627A93F4AF9B /* SurfaceResource+AgentIcon.swift */; };
733F8CFB9036496EB237A178 /* SurfaceResource+CatalogOrdering.swift in Sources */ = {isa = PBXBuildFile; fileRef = FC6ACCF8253744008F186C73 /* SurfaceResource+CatalogOrdering.swift */; };
91007F48E782474596D8FB7D /* SurfaceResource+CloudTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7E2A1163D9B452EA3D55B5D /* SurfaceResource+CloudTitle.swift */; };
F96CAE8A14774122A5F8613B /* SurfaceResourceDragPayload.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48A1307BCCDB493F49179E0B /* SurfaceResourceDragPayload.swift */; };
@@ -3454,6 +3456,7 @@
D36A00090000000000000001 /* TerminalSurface+RendererRealizationSurface.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36A00090000000000000002 /* TerminalSurface+RendererRealizationSurface.swift */; };
D3052001000000000000001 /* TerminalSurfaceResizePolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3052001000000000000002 /* TerminalSurfaceResizePolicyTests.swift */; };
C750500000000000000000B1 /* TerminalSurfaceRuntimeWiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = C750500000000000000000B2 /* TerminalSurfaceRuntimeWiring.swift */; };
+ A6A6A6A60000000000000001 /* TerminalTabAgentIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6A6A6A60000000000000002 /* TerminalTabAgentIcon.swift */; };
A91C0D0F0000000000000002 /* TerminalTabIconRegressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A91C0D0F0000000000000001 /* TerminalTabIconRegressionTests.swift */; };
F83620020000000000000001 /* TerminalTTYSessionIdentity.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83620020000000000000002 /* TerminalTTYSessionIdentity.swift */; };
EC010A01 /* TerminalUploadCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC010A02 /* TerminalUploadCommand.swift */; };
@@ -6916,6 +6919,7 @@
C0DE70530000000000000001 /* submit-cmux-profile */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = "Resources/bin/submit-cmux-profile"; sourceTree = SOURCE_ROOT; };
D7AB00000000000000B040 /* SupersededPhoneDismissBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupersededPhoneDismissBuffer.swift; sourceTree = ""; };
019F9382B99C46AAAEB9C91C /* SurfaceAgentNaming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceAgentNaming.swift"; sourceTree = ""; };
+ ECA5950A12D41190CC94F10A /* SurfaceCatalog+AgentIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+AgentIcons.swift"; sourceTree = ""; };
F11347000000000000000008 /* SurfaceCatalog+CloudBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+CloudBinding.swift"; sourceTree = ""; };
F11347000000000000000002 /* SurfaceCatalog+CloudPorts.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+CloudPorts.swift"; sourceTree = ""; };
4A4A80857B8A4530BD5BC498 /* SurfaceCatalog+CloudRenameSubmission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceCatalog+CloudRenameSubmission.swift"; sourceTree = ""; };
@@ -6948,6 +6952,7 @@
7867A0020000000000000001 /* SurfaceProjectionRestoreStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SurfaceProjectionRestoreStore.swift; sourceTree = ""; };
AAED489C1C23533995AF55EA /* SurfaceProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SurfaceProvider.swift; sourceTree = ""; };
CA467FE5247EDFC122C88129 /* SurfaceRemotePlacement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceRemotePlacement.swift"; sourceTree = ""; };
+ 0314DFABAF2F627A93F4AF9B /* SurfaceResource+AgentIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceResource+AgentIcon.swift"; sourceTree = ""; };
FC6ACCF8253744008F186C73 /* SurfaceResource+CatalogOrdering.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sidebar/SurfaceResource+CatalogOrdering.swift"; sourceTree = ""; };
A7E2A1163D9B452EA3D55B5D /* SurfaceResource+CloudTitle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SurfaceResource+CloudTitle.swift"; sourceTree = ""; };
48A1307BCCDB493F49179E0B /* SurfaceResourceDragPayload.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SurfaceResourceDragPayload.swift; sourceTree = ""; };
@@ -7290,6 +7295,7 @@
D36A00090000000000000002 /* TerminalSurface+RendererRealizationSurface.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "App/TerminalSurface+RendererRealizationSurface.swift"; sourceTree = ""; };
D3052001000000000000002 /* TerminalSurfaceResizePolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSurfaceResizePolicyTests.swift; sourceTree = ""; };
C750500000000000000000B2 /* TerminalSurfaceRuntimeWiring.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalSurfaceRuntimeWiring.swift; sourceTree = ""; };
+ A6A6A6A60000000000000002 /* TerminalTabAgentIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalTabAgentIcon.swift; sourceTree = ""; };
A91C0D0F0000000000000001 /* TerminalTabIconRegressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalTabIconRegressionTests.swift; sourceTree = ""; };
F83620020000000000000002 /* TerminalTTYSessionIdentity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalTTYSessionIdentity.swift; sourceTree = ""; };
EC010A02 /* TerminalUploadCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalUploadCommand.swift; sourceTree = ""; };
@@ -8155,7 +8161,9 @@
DD9E59E46A6A91958EE22451 /* CloudEnvDelivery.swift */,
A79DE8A46550805CDAB18D09 /* SurfacePaneFactory+ProjectionLayout.swift */,
F0508401C3A0232FEDD7F97D /* CmuxTuiSurfaceProvider+ProjectionLayout.swift */,
+ 0314DFABAF2F627A93F4AF9B /* SurfaceResource+AgentIcon.swift */,
A7E2A1163D9B452EA3D55B5D /* SurfaceResource+CloudTitle.swift */,
+
1A5D0FC63C854C90B2974F39 /* CloudWorkspaceProjectionTask.swift */,
085BB53F552B4C6A8728941E /* Workspace+CloudLayoutProjection.swift */,
2C422AAD63384578846BC05E /* TerminalController+CloudTerminalCreation.swift */,
@@ -8178,7 +8186,9 @@
0DFE35D27805468DBA885560 /* CloudWorkspaceProjectionEnvironment.swift */,
A5A9286A232F4C3FAD571015 /* CloudWorkspaceProjectionCoordinator.swift */,
4A4A80857B8A4530BD5BC498 /* SurfaceCatalog+CloudRenameSubmission.swift */,
+ ECA5950A12D41190CC94F10A /* SurfaceCatalog+AgentIcons.swift */,
AB74882A29884E378E36D645 /* SurfaceCatalog+CloudWorkspaceProjection.swift */,
+
183FE43D3F71A474726D6AA6 /* CloudWorkspaceLayoutTranslator.swift */,
90C490DDBA08806E15C5CC0A /* CmuxTuiSurfaceProvider+PlacementSync.swift */,
D12439050000000000000002 /* CmuxTuiSurfaceProvider+WorkspaceLifecycle.swift */,
@@ -9756,6 +9766,7 @@
A5001099 /* TerminalNotificationScrollPosition.swift */,
7490D0107490D0107490D010 /* TerminalNotificationStore+MemoryPressure.swift */,
596100000000000000000004 /* NativeNotificationDeliveryHooks.swift */,
+ A6A6A6A60000000000000002 /* TerminalTabAgentIcon.swift */,
D7AB00000000000000B040 /* SupersededPhoneDismissBuffer.swift */,
A5001097 /* TerminalNotificationPolicy.swift */,
F1A0C0DE0000000000000001 /* FindTextFieldSupport.swift */,
@@ -12352,6 +12363,8 @@
+
+
A11CE0040000000000000001 /* AboutLicenseContent.swift in Sources */,
A5C017000000000000000001 /* AccountSignInFlow.swift in Sources */,
A5C017000000000000000003 /* AccountSignInModel.swift in Sources */,
@@ -14203,6 +14216,7 @@
D35B71010000000000000001 /* StartupBreadcrumbLog.swift in Sources */,
D7AB00000000000000B041 /* SupersededPhoneDismissBuffer.swift in Sources */,
C935C833B18F4CC18711BD77 /* SurfaceAgentNaming.swift in Sources */,
+ FB3A15A096C8270E929D34B6 /* SurfaceCatalog+AgentIcons.swift in Sources */,
F11347000000000000000007 /* SurfaceCatalog+CloudBinding.swift in Sources */,
F11347000000000000000001 /* SurfaceCatalog+CloudPorts.swift in Sources */,
E71A3B8404534D5699AA2E9B /* SurfaceCatalog+CloudRenameSubmission.swift in Sources */,
@@ -14232,6 +14246,7 @@
7867A0010000000000000001 /* SurfaceProjectionRestoreStore.swift in Sources */,
EE1DE3F7E67DF1366F214AC7 /* SurfaceProvider.swift in Sources */,
58FE3B39F41CF68A632CA083 /* SurfaceRemotePlacement.swift in Sources */,
+ 07AFFFB52EA184A2DE379926 /* SurfaceResource+AgentIcon.swift in Sources */,
733F8CFB9036496EB237A178 /* SurfaceResource+CatalogOrdering.swift in Sources */,
91007F48E782474596D8FB7D /* SurfaceResource+CloudTitle.swift in Sources */,
F96CAE8A14774122A5F8613B /* SurfaceResourceDragPayload.swift in Sources */,
@@ -14514,6 +14529,7 @@
12476000000000000000000A /* TerminalSurface+ImageTransferTarget.swift in Sources */,
D36A00090000000000000001 /* TerminalSurface+RendererRealizationSurface.swift in Sources */,
C750500000000000000000B1 /* TerminalSurfaceRuntimeWiring.swift in Sources */,
+ A6A6A6A60000000000000001 /* TerminalTabAgentIcon.swift in Sources */,
F83620020000000000000001 /* TerminalTTYSessionIdentity.swift in Sources */,
EC010A01 /* TerminalUploadCommand.swift in Sources */,
5154BEAB50364B86A9E36E4B /* TerminalViewportUITestRecorder.swift in Sources */,
diff --git a/cmuxTests/CloudSidebarConsistencyTests.swift b/cmuxTests/CloudSidebarConsistencyTests.swift
index 2ce00b774346..f1b27f14fafa 100644
--- a/cmuxTests/CloudSidebarConsistencyTests.swift
+++ b/cmuxTests/CloudSidebarConsistencyTests.swift
@@ -226,6 +226,26 @@ struct CloudSidebarConsistencyTests {
#expect(workspace.panelCustomTitleSources[panelID] == .user)
}
+ @Test("Supported Cloud providers resolve their bundled marks")
+ func supportedCloudProviderMarks() {
+ let expected = [
+ "claude": "AgentIcons/Claude", "codex": "AgentIcons/Codex",
+ "opencode": "AgentIcons/OpenCode", "pi": "AgentIcons/Pi",
+ "amp": "AgentIcons/Amp", "cursor": "AgentIcons/Cursor",
+ "gemini": "AgentIcons/Gemini", "kiro": "AgentIcons/Kiro",
+ "copilot": "AgentIcons/Copilot", "codebuddy": "AgentIcons/CodeBuddy",
+ "factory": "AgentIcons/Factory", "qoder": "AgentIcons/Qoder",
+ "kimi": "AgentIcons/Kimi", "ollama": "AgentIcons/Ollama"
+ ]
+ for (provider, asset) in expected {
+ let badge = SurfaceAgentBadge(state: "working", source: "hook", agent: provider)
+ #expect(badge.agent == provider)
+ #expect(CmuxTaskManagerCodingAgentDefinition.builtIns.first(where: { $0.id == provider })?.assetName == asset)
+ }
+ #expect(TerminalTabAgentIconResolver().assetName(forStatusKey: "codex") == "AgentIcons/Codex")
+ #expect(TerminalTabAgentIconResolver().assetName(forStatusKey: "gemini") == "AgentIcons/Gemini")
+ }
+
@Test("A bound native tab receives canonical names, process titles, and ignores delayed graph callbacks", arguments: [false, true])
func nativeNameParity(named: Bool) throws {
let manager = TabManager()
diff --git a/cmuxTests/CmuxTuiSurfaceProviderTests.swift b/cmuxTests/CmuxTuiSurfaceProviderTests.swift
index f659c49174a1..e646214281b1 100644
--- a/cmuxTests/CmuxTuiSurfaceProviderTests.swift
+++ b/cmuxTests/CmuxTuiSurfaceProviderTests.swift
@@ -134,6 +134,7 @@ import Testing
#expect(build.detail == "/root/work/app")
#expect(build.lifecycle == .running)
#expect(build.agent == SurfaceAgentBadge(state: "working", source: "claude"))
+ #expect(build.terminalAgentIconAssetName == "AgentIcons/Claude")
#expect(build.remoteWorkspace == SurfaceRemoteWorkspace(id: "ws_main", name: "main", index: 0, focused: true))
#expect(build.remoteViews?.map(\.tabID) == ["tab_1", "tab_4"])
#expect(build.remoteWorkspaces.map(\.id) == ["ws_main", "ws_api"])
@@ -156,6 +157,15 @@ import Testing
#expect(detached.lifecycle == .running)
}
+ @Test func providerAwareAgentFieldResolvesCodexMark() throws {
+ var snapshot = Self.sessionSnapshot
+ snapshot["agents"] = [["id": "agent_1", "terminal_id": "term_build", "state": "working", "source": "hook", "agent": "codex"]]
+ let resources = CmuxTuiSnapshotParser.terminals(fromSnapshot: snapshot, machine: Self.machine)
+ let terminal = try #require(resources.first { $0.id.key == "term_build" })
+ #expect(terminal.agent?.agent == "codex")
+ #expect(terminal.terminalAgentIconAssetName == "AgentIcons/Codex")
+ }
+
@Test func userTabNameStaysOnTheIndividualRemoteView() throws {
var snapshot = Self.sessionSnapshot
snapshot["tabs"] = [
From de71da70c2df8421c85a1ba07640ed588158df43 Mon Sep 17 00:00:00 2001
From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com>
Date: Thu, 17 Sep 2026 16:59:01 -0700
Subject: [PATCH 2/9] feat(tui): detect agent providers for Cloud branding
---
cmux-tui/crates/cmux-tui-core/Cargo.toml | 1 +
cmux-tui/crates/cmux-tui-core/src/lib.rs | 1 +
cmux-tui/crates/cmux-tui-core/src/mux.rs | 49 +
.../crates/cmux-tui-core/src/resource_api.rs | 9 +-
.../src/screen_detect/manifest.rs | 953 ++++++++++++++++++
.../cmux-tui-core/src/screen_detect/mod.rs | 391 +++++++
.../src/screen_detect/scanner.rs | 122 +++
cmux-tui/crates/cmux-tui-core/src/server.rs | 1 +
cmux-tui/vendor/herdr-manifests/LICENSE | 201 ++++
cmux-tui/vendor/herdr-manifests/README.md | 6 +
cmux-tui/vendor/herdr-manifests/amp.toml | 65 ++
.../vendor/herdr-manifests/antigravity.toml | 33 +
cmux-tui/vendor/herdr-manifests/claude.toml | 207 ++++
cmux-tui/vendor/herdr-manifests/cline.toml | 26 +
cmux-tui/vendor/herdr-manifests/codex.toml | 89 ++
cmux-tui/vendor/herdr-manifests/cursor.toml | 57 ++
cmux-tui/vendor/herdr-manifests/devin.toml | 86 ++
cmux-tui/vendor/herdr-manifests/droid.toml | 48 +
cmux-tui/vendor/herdr-manifests/gemini.toml | 25 +
.../herdr-manifests/github-copilot.toml | 37 +
cmux-tui/vendor/herdr-manifests/grok.toml | 164 +++
cmux-tui/vendor/herdr-manifests/hermes.toml | 102 ++
cmux-tui/vendor/herdr-manifests/kilo.toml | 24 +
cmux-tui/vendor/herdr-manifests/kimi.toml | 77 ++
cmux-tui/vendor/herdr-manifests/kiro.toml | 63 ++
cmux-tui/vendor/herdr-manifests/maki.toml | 68 ++
cmux-tui/vendor/herdr-manifests/muse.toml | 113 +++
cmux-tui/vendor/herdr-manifests/opencode.toml | 37 +
cmux-tui/vendor/herdr-manifests/pi.toml | 13 +
cmux-tui/vendor/herdr-manifests/qodercli.toml | 38 +
cmux-tui/vendor/herdr-manifests/qwen.toml | 119 +++
31 files changed, 3224 insertions(+), 1 deletion(-)
create mode 100644 cmux-tui/crates/cmux-tui-core/src/screen_detect/manifest.rs
create mode 100644 cmux-tui/crates/cmux-tui-core/src/screen_detect/mod.rs
create mode 100644 cmux-tui/crates/cmux-tui-core/src/screen_detect/scanner.rs
create mode 100644 cmux-tui/vendor/herdr-manifests/LICENSE
create mode 100644 cmux-tui/vendor/herdr-manifests/README.md
create mode 100644 cmux-tui/vendor/herdr-manifests/amp.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/antigravity.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/claude.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/cline.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/codex.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/cursor.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/devin.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/droid.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/gemini.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/github-copilot.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/grok.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/hermes.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/kilo.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/kimi.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/kiro.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/maki.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/muse.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/opencode.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/pi.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/qodercli.toml
create mode 100644 cmux-tui/vendor/herdr-manifests/qwen.toml
diff --git a/cmux-tui/crates/cmux-tui-core/Cargo.toml b/cmux-tui/crates/cmux-tui-core/Cargo.toml
index ac5bcc24fb3b..7d3588259573 100644
--- a/cmux-tui/crates/cmux-tui-core/Cargo.toml
+++ b/cmux-tui/crates/cmux-tui-core/Cargo.toml
@@ -31,6 +31,7 @@ fs4.workspace = true
sha2.workspace = true
zeroize.workspace = true
url.workspace = true
+toml.workspace = true
[target.'cfg(windows)'.dependencies]
uds_windows.workspace = true
diff --git a/cmux-tui/crates/cmux-tui-core/src/lib.rs b/cmux-tui/crates/cmux-tui-core/src/lib.rs
index aade7f08b183..4a25768e0308 100644
--- a/cmux-tui/crates/cmux-tui-core/src/lib.rs
+++ b/cmux-tui/crates/cmux-tui-core/src/lib.rs
@@ -40,6 +40,7 @@ pub mod resource_name;
mod resource_router;
mod resource_selector;
mod resource_tab;
+mod screen_detect;
mod short_id;
mod sidebar_resource;
mod surface;
diff --git a/cmux-tui/crates/cmux-tui-core/src/mux.rs b/cmux-tui/crates/cmux-tui-core/src/mux.rs
index 56e603244a77..611cb95c0375 100644
--- a/cmux-tui/crates/cmux-tui-core/src/mux.rs
+++ b/cmux-tui/crates/cmux-tui-core/src/mux.rs
@@ -1237,6 +1237,7 @@ pub struct AgentRecord {
pub state: AgentState,
pub source: AgentSource,
pub session: Option,
+ pub agent: Option,
pub updated_at_ms: u64,
}
@@ -2301,6 +2302,10 @@ pub struct Mux {
sidebar_plugin: Mutex,
machine_usage: Mutex