Repository navigation
Conversation
Two quality-of-life improvements for multi-agent workflows:
1. Workspace auto-naming: when opening a directory (e.g. `cmux ~/git/project`),
the sidebar card now shows the directory basename ("project") instead of
"Terminal N". Does not set customTitle, so OSC titles and manual renames
still override normally.
2. Agent tab title enhancement: when a terminal title matches a known AI agent
binary (claude, codex, gemini, opencode, aider, ft-claude), cmux reads the
process arguments to extract the task description and updates the tab title
to "agent: task" (e.g. "claude: Fix auth bug"). Respects customTitle priority
and runs detection off-main-thread per socket threading policy.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@d-wwei is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
📝 WalkthroughWalkthroughTabManager now derives workspace titles from explicit working-directory basenames at creation time and adds an async path to update panel titles when a known agent binary is detected on the panel's TTY. A new TerminalAgentDetector module locates foreground agent processes and extracts a short task description. Changes
Sequence Diagram(s)sequenceDiagram
participant TabMgr as TabManager
participant Detector as TerminalAgentDetector
participant System as System APIs
participant Panel as Panel UI
Note over TabMgr,Panel: Async panel title resolution when agent binary name seen
TabMgr->>TabMgr: Panel title change triggered
TabMgr->>TabMgr: If no custom title && has surfaceTTYName
TabMgr->>Detector: detect(forTTY: surfaceTTYName)
rect rgba(100, 150, 200, 0.5)
Detector->>System: /bin/ps (list TTY processes)
System-->>Detector: process list
Detector->>Detector: Filter foreground (pgid == tpgid)
Detector->>Detector: Match knownAgentBinaries
Detector->>System: sysctl KERN_PROCARGS2 (get argv)
System-->>Detector: argv buffer
Detector->>Detector: Parse argv, extract taskDescription
Detector-->>TabMgr: DetectedAgent{executable, task, pid}
end
rect rgba(150, 200, 100, 0.5)
TabMgr->>Panel: Update title to "executable: task"
alt Panel is focused
TabMgr->>TabMgr: Refresh window title
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR adds two UX improvements: workspace sidebar cards now derive their name from the opened directory's basename (instead of always "Terminal N"), and tab titles for known AI agent binaries (claude, codex, etc.) are asynchronously enhanced with the task description extracted via
Confidence Score: 4/5Safe to merge after addressing the repeated-detection issue in One P1 logic bug: the enhanced-title guard is missing, so any OSC "claude" repeat triggers another ps spawn and title flicker. The fix is small (one extra condition). Remaining findings are P2 (defensive guard and code dedup). Sources/TabManager.swift — the Important Files Changed
Sequence DiagramsequenceDiagram
participant Shell
participant TabManager
participant Workspace
participant BG as DispatchQueue.global
participant AgentDetector
Shell->>TabManager: OSC title "claude"
TabManager->>Workspace: updatePanelTitle("claude") → true
TabManager->>TabManager: isKnownAgentName("claude") ✓
TabManager->>BG: enhanceAgentTitle(tty)
BG->>AgentDetector: detect(forTTY:)
AgentDetector->>AgentDetector: ps -t tty (pid/pgid/tpgid/ucomm)
AgentDetector->>AgentDetector: KERN_PROCARGS2 → extractTaskDescription
AgentDetector-->>BG: DetectedAgent("claude", "Fix auth bug")
BG->>TabManager: DispatchQueue.main.async
TabManager->>Workspace: updatePanelTitle("claude: Fix auth bug")
TabManager->>TabManager: updateWindowTitle ✓
Note over Shell,TabManager: ⚠ If shell re-sends OSC "claude" title:
Shell->>TabManager: OSC title "claude" (again)
TabManager->>Workspace: updatePanelTitle("claude") → true (changed!)
TabManager->>BG: enhanceAgentTitle again → second ps spawn
Reviews (1): Last reviewed commit: "feat: auto-name workspaces from director..." | Re-trigger Greptile |
| if TerminalAgentDetector.isKnownAgentName(title), | ||
| tab.panelCustomTitles[panelId] == nil, | ||
| let ttyName = tab.surfaceTTYNames[panelId] { | ||
| enhanceAgentTitle(tabId: tabId, panelId: panelId, ttyName: ttyName) | ||
| } |
There was a problem hiding this comment.
Title flickering and repeated
ps spawns on every OSC update
enhanceAgentTitle is triggered on every call to updatePanelTitle where the title is a known agent name. The enhanced title is written back via tab.updatePanelTitle (the workspace method directly), not through the TabManager coalescer. When the next OSC update arrives with "claude" again — which happens any time the shell re-broadcasts the foreground process name, or when the agent itself refreshes its own terminal title — tab.updatePanelTitle returns true (changed from "claude: task" back to "claude"), so isKnownAgentName matches again and another background DispatchQueue.global task is spawned, running ps again. The user sees the tab title briefly revert to "claude" on each such OSC cycle.
A minimal fix is to bail out early if the title is already in enhanced form for this panel:
// If the title matches a known agent binary, try to enhance it with task info
if TerminalAgentDetector.isKnownAgentName(title),
tab.panelCustomTitles[panelId] == nil,
let ttyName = tab.surfaceTTYNames[panelId],
!(tab.panelTitles[panelId]?.hasPrefix(title + ": ") ?? false) {
enhanceAgentTitle(tabId: tabId, panelId: panelId, ttyName: ttyName)
}This stops re-detection once an enhanced title is already in place and only re-runs when the raw agent name reappears after a session change.
| } | ||
| let args = commandLineArguments(forPID: match.pid) | ||
| let task = args.flatMap { extractTaskDescription(from: $0, agent: match.executableName) } | ||
| return DetectedAgent(executableName: match.executableName, taskDescription: task, pid: match.pid) | ||
| } | ||
|
|
There was a problem hiding this comment.
Missing
pgid > 0 && tpgid > 0 foreground guards from TerminalSSHSessionDetector
TerminalSSHSessionDetector.isForegroundSSHProcess explicitly guards process.pgid > 0 && process.tpgid > 0 before comparing them — this ensures orphaned or daemon processes (where tpgid may be 0 or -1) are never falsely treated as foreground. The agent detector omits these bounds checks. While ps -t <tty> already filters by session, background processes within the same session can appear with tpgid == 0, and pgid == tpgid would then incorrectly match them.
let foreground = snapshots.filter { $0.pgid > 0 && $0.tpgid > 0 && $0.pgid == $0.tpgid }| } | ||
| guard ok else { return nil } | ||
| return parseKernProcArgs(Array(buffer.prefix(Int(size)))) | ||
| } | ||
|
|
||
| private static func parseKernProcArgs(_ bytes: [UInt8]) -> [String]? { | ||
| guard bytes.count > 4 else { return nil } | ||
| var argcRaw: Int32 = 0 | ||
| withUnsafeMutableBytes(of: &argcRaw) { $0.copyBytes(from: bytes.prefix(4)) } | ||
| let argc = Int(Int32(littleEndian: argcRaw)) | ||
| guard argc > 0 else { return nil } | ||
|
|
||
| // Skip past argc + executable path + padding nulls | ||
| var i = 4 | ||
| while i < bytes.count, bytes[i] != 0 { i += 1 } | ||
| while i < bytes.count, bytes[i] == 0 { i += 1 } | ||
|
|
||
| var args: [String] = [] | ||
| while i < bytes.count, args.count < argc { | ||
| let start = i | ||
| while i < bytes.count, bytes[i] != 0 { i += 1 } | ||
| guard let arg = String(bytes: bytes[start..<i], encoding: .utf8) else { return nil } | ||
| args.append(arg) | ||
| while i < bytes.count, bytes[i] == 0 { i += 1 } | ||
| } | ||
| return args.count == argc ? args : nil | ||
| } | ||
|
|
||
| // MARK: - Task description extraction | ||
|
|
||
| /// Extract a human-readable task description from the agent's command-line arguments. | ||
| /// Returns `nil` if no meaningful description can be determined (e.g. interactive mode). | ||
| private static func extractTaskDescription(from args: [String], agent: String) -> String? { | ||
| // args[0] is the executable path; positional args start from args[1..] | ||
| guard args.count > 1 else { return nil } | ||
| let tail = Array(args.dropFirst()) | ||
|
|
||
| // For "codex exec <prompt>" and "opencode run <prompt>", skip the subcommand | ||
| let positional: [String] | ||
| if (agent == "codex" || agent == "opencode"), tail.count > 1 { | ||
| let sub = tail[0].lowercased() | ||
| if sub == "exec" || sub == "run" { | ||
| positional = Array(tail.dropFirst()) | ||
| } else { | ||
| positional = tail | ||
| } | ||
| } else { | ||
| positional = tail | ||
| } | ||
|
|
||
| // Find first non-flag argument (doesn't start with -) | ||
| // Also skip known flag values (the argument after a flag that takes a value) | ||
| let flagsWithValue: Set<String> = [ | ||
| "--model", "-m", "--settings", "--agent", "-c", | ||
| "--append-system-prompt", "--append-system-prompt-file", | ||
| "--system-prompt", "--system-prompt-file", | ||
| "--prompt-file", | ||
| ] | ||
|
|
||
| var skipNext = false | ||
| for arg in positional { | ||
| if skipNext { skipNext = false; continue } | ||
| if arg.hasPrefix("-") { | ||
| if flagsWithValue.contains(arg) { skipNext = true } | ||
| continue | ||
| } | ||
| // Found a positional argument — this is likely the prompt/task | ||
| let trimmed = arg.trimmingCharacters(in: .whitespacesAndNewlines) | ||
| guard !trimmed.isEmpty else { continue } | ||
| // Truncate to keep tab names readable | ||
| if trimmed.count > 60 { | ||
| return String(trimmed.prefix(57)) + "..." | ||
| } | ||
| return trimmed | ||
| } | ||
| return nil | ||
| } | ||
| } |
There was a problem hiding this comment.
parseKernProcArgs and commandLineArguments duplicated from TerminalSSHSessionDetector
Both implementations are functionally identical (just variable names differ). If the kernel procargs parsing logic ever needs a fix, it would need to be applied in two places. Consider extracting a shared KernProcArgsReader helper or moving the functions into a common TerminalProcessUtils namespace that both detectors can import.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
Sources/TerminalAgentDetector.swift (1)
93-114: Consolidate KERN_PROCARGS2 parsing into one shared helper.This parser duplicates the low-level logic already present in
Sources/TerminalSSHSessionDetector.swift; extracting a shared utility will reduce drift risk for this binary format handling.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Sources/TerminalAgentDetector.swift` around lines 93 - 114, The parseKernProcArgs(_:) function duplicates KERN_PROCARGS2 parsing logic found in TerminalSSHSessionDetector; extract that low‑level byte-parsing code into a shared helper (e.g. a new internal function or extension like parseKernProcArgsBytes(_:)->[String] in a common utility file) and update both parseKernProcArgs and the parser in TerminalSSHSessionDetector to call the shared helper; ensure the helper preserves the same behavior (reading argc from first 4 bytes, skipping executable path and NUL padding, splitting NUL-terminated UTF-8 args, and validating args.count == argc) and keep function names parseKernProcArgs and the TerminalSSHSessionDetector call sites unchanged except to delegate to the new helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Sources/TabManager.swift`:
- Around line 1994-2004: The fallback literal "Terminal \(nextTabCount)" in the
derivedTitle closure must be localized: replace that bare string with a
localized lookup (e.g. obtain a localized template via String(localized:
"terminal.default", defaultValue: "Terminal %d") and format it with nextTabCount
using String(format:) or LocalizedStringKey-style formatting) inside the
derivedTitle closure; update the Resources/Localizable.xcstrings to add the
"terminal.default" key with the default "Terminal %d" entry so the formatted
title shown by TabManager/derivedTitle is localized.
- Around line 4630-4654: Pass and capture the panel's current title when calling
enhanceAgentTitle and, before applying the async update, revalidate that the tab
still exists, panelCustomTitles[panelId] is nil, surfaceTTYNames[panelId] still
equals the original ttyName, and the current panel title still equals the
original title that matched the agent name; only then call updatePanelTitle and
updateWindowTitle. Specifically, change
enhanceAgentTitle(tabId:panelId:ttyName:) to accept the originalTitle (or
capture it) and in the DispatchQueue.main.async block check
tab.surfaceTTYNames[panelId] == ttyName and tab.currentPanelTitle(for: panelId)
== originalTitle (plus the existing panelCustomTitles check) before writing the
enhanced title.
In `@Sources/TerminalAgentDetector.swift`:
- Around line 127-133: The code in TerminalAgentDetector handling agent ==
"codex" || agent == "opencode" currently only special-cases subcommands when
tail.count > 1, so invocations like "codex exec" or "opencode run" (tail.count
== 1) get misclassified as task text; update the logic in the block that
examines agent, tail and sets positional/taskDescription so that when tail.count
== 1 and the single tail element (sub) lowercased is "exec" or "run" you treat
it as a subcommand-only invocation by setting positional to an empty array and
ensuring taskDescription is nil (instead of leaving the sub as task text).
Reference variables: agent, tail, sub, positional, taskDescription in
TerminalAgentDetector.swift.
---
Nitpick comments:
In `@Sources/TerminalAgentDetector.swift`:
- Around line 93-114: The parseKernProcArgs(_:) function duplicates
KERN_PROCARGS2 parsing logic found in TerminalSSHSessionDetector; extract that
low‑level byte-parsing code into a shared helper (e.g. a new internal function
or extension like parseKernProcArgsBytes(_:)->[String] in a common utility file)
and update both parseKernProcArgs and the parser in TerminalSSHSessionDetector
to call the shared helper; ensure the helper preserves the same behavior
(reading argc from first 4 bytes, skipping executable path and NUL padding,
splitting NUL-terminated UTF-8 args, and validating args.count == argc) and keep
function names parseKernProcArgs and the TerminalSSHSessionDetector call sites
unchanged except to delegate to the new helper.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 09a18a43-2d26-42eb-99ed-d2238f75a244
📒 Files selected for processing (2)
Sources/TabManager.swiftSources/TerminalAgentDetector.swift
| // Derive workspace title: explicit title > directory basename > "Terminal N" | ||
| let derivedTitle: String = { | ||
| if let title { return title } | ||
| if let dir = explicitWorkingDirectory { | ||
| let basename = URL(fileURLWithPath: dir).lastPathComponent | ||
| if !basename.isEmpty, basename != "/" { | ||
| return basename | ||
| } | ||
| } | ||
| return "Terminal \(nextTabCount)" | ||
| }() |
There was a problem hiding this comment.
Localize the new default workspace title.
Line 2003 introduces a new bare UI string, so this path will stay English in localized builds. Please route the fallback through String(localized:defaultValue:) and add the key to Resources/Localizable.xcstrings.
🌐 Suggested fix
- return "Terminal \(nextTabCount)"
+ return String(
+ localized: "workspace.defaultTitle",
+ defaultValue: "Terminal \(nextTabCount)"
+ )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/TabManager.swift` around lines 1994 - 2004, The fallback literal
"Terminal \(nextTabCount)" in the derivedTitle closure must be localized:
replace that bare string with a localized lookup (e.g. obtain a localized
template via String(localized: "terminal.default", defaultValue: "Terminal %d")
and format it with nextTabCount using String(format:) or
LocalizedStringKey-style formatting) inside the derivedTitle closure; update the
Resources/Localizable.xcstrings to add the "terminal.default" key with the
default "Terminal %d" entry so the formatted title shown by
TabManager/derivedTitle is localized.
| // If the title matches a known agent binary, try to enhance it with task info | ||
| if TerminalAgentDetector.isKnownAgentName(title), | ||
| tab.panelCustomTitles[panelId] == nil, | ||
| let ttyName = tab.surfaceTTYNames[panelId] { | ||
| enhanceAgentTitle(tabId: tabId, panelId: panelId, ttyName: ttyName) | ||
| } | ||
| } | ||
|
|
||
| /// Asynchronously detect the agent's task from process arguments and update the panel title. | ||
| private func enhanceAgentTitle(tabId: UUID, panelId: UUID, ttyName: String) { | ||
| DispatchQueue.global(qos: .utility).async { [weak self] in | ||
| guard let agent = TerminalAgentDetector.detect(forTTY: ttyName), | ||
| let task = agent.taskDescription, !task.isEmpty else { return } | ||
| let enhanced = "\(agent.executableName): \(task)" | ||
| DispatchQueue.main.async { [weak self] in | ||
| guard let self, | ||
| let tab = self.tabs.first(where: { $0.id == tabId }), | ||
| tab.panelCustomTitles[panelId] == nil else { return } | ||
| _ = tab.updatePanelTitle(panelId: panelId, title: enhanced) | ||
| if self.selectedTabId == tabId && tab.focusedPanelId == panelId { | ||
| self.updateWindowTitle(for: tab) | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Revalidate the title/TTY before applying the async enhancement.
Lines 4644-4648 only re-check panelCustomTitles. If a newer OSC/process title lands, or surfaceTTYNames[panelId] changes while detection is running, the late callback will still write a stale agent: task title back and break the intended precedence.
🧵 Suggested fix
- enhanceAgentTitle(tabId: tabId, panelId: panelId, ttyName: ttyName)
+ enhanceAgentTitle(
+ tabId: tabId,
+ panelId: panelId,
+ ttyName: ttyName,
+ expectedBaseTitle: title
+ )
}
}
/// Asynchronously detect the agent's task from process arguments and update the panel title.
- private func enhanceAgentTitle(tabId: UUID, panelId: UUID, ttyName: String) {
+ private func enhanceAgentTitle(
+ tabId: UUID,
+ panelId: UUID,
+ ttyName: String,
+ expectedBaseTitle: String
+ ) {
DispatchQueue.global(qos: .utility).async { [weak self] in
guard let agent = TerminalAgentDetector.detect(forTTY: ttyName),
let task = agent.taskDescription, !task.isEmpty else { return }
let enhanced = "\(agent.executableName): \(task)"
DispatchQueue.main.async { [weak self] in
guard let self,
let tab = self.tabs.first(where: { $0.id == tabId }),
- tab.panelCustomTitles[panelId] == nil else { return }
+ tab.panelCustomTitles[panelId] == nil,
+ tab.surfaceTTYNames[panelId] == ttyName,
+ tab.panelTitles[panelId] == expectedBaseTitle else { return }
_ = tab.updatePanelTitle(panelId: panelId, title: enhanced)
if self.selectedTabId == tabId && tab.focusedPanelId == panelId {
self.updateWindowTitle(for: tab)
}
}
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/TabManager.swift` around lines 4630 - 4654, Pass and capture the
panel's current title when calling enhanceAgentTitle and, before applying the
async update, revalidate that the tab still exists, panelCustomTitles[panelId]
is nil, surfaceTTYNames[panelId] still equals the original ttyName, and the
current panel title still equals the original title that matched the agent name;
only then call updatePanelTitle and updateWindowTitle. Specifically, change
enhanceAgentTitle(tabId:panelId:ttyName:) to accept the originalTitle (or
capture it) and in the DispatchQueue.main.async block check
tab.surfaceTTYNames[panelId] == ttyName and tab.currentPanelTitle(for: panelId)
== originalTitle (plus the existing panelCustomTitles check) before writing the
enhanced title.
| if (agent == "codex" || agent == "opencode"), tail.count > 1 { | ||
| let sub = tail[0].lowercased() | ||
| if sub == "exec" || sub == "run" { | ||
| positional = Array(tail.dropFirst()) | ||
| } else { | ||
| positional = tail | ||
| } |
There was a problem hiding this comment.
Subcommand-only agent invocations are misclassified as task text.
For codex exec / opencode run without a prompt argument, this path currently returns "exec"/"run" as taskDescription instead of nil.
Proposed fix
- if (agent == "codex" || agent == "opencode"), tail.count > 1 {
- let sub = tail[0].lowercased()
- if sub == "exec" || sub == "run" {
- positional = Array(tail.dropFirst())
- } else {
- positional = tail
- }
- } else {
- positional = tail
- }
+ if (agent == "codex" || agent == "opencode"), let sub = tail.first?.lowercased() {
+ if sub == "exec" || sub == "run" {
+ positional = Array(tail.dropFirst())
+ } else {
+ positional = tail
+ }
+ } else {
+ positional = tail
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@Sources/TerminalAgentDetector.swift` around lines 127 - 133, The code in
TerminalAgentDetector handling agent == "codex" || agent == "opencode" currently
only special-cases subcommands when tail.count > 1, so invocations like "codex
exec" or "opencode run" (tail.count == 1) get misclassified as task text; update
the logic in the block that examines agent, tail and sets
positional/taskDescription so that when tail.count == 1 and the single tail
element (sub) lowercased is "exec" or "run" you treat it as a subcommand-only
invocation by setting positional to an empty array and ensuring taskDescription
is nil (instead of leaving the sub as task text). Reference variables: agent,
tail, sub, positional, taskDescription in TerminalAgentDetector.swift.
There was a problem hiding this comment.
3 issues found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="Sources/TabManager.swift">
<violation number="1" location="Sources/TabManager.swift:4631">
P1: This triggers repeated `ps` spawns and visible title flickering. When the shell re-broadcasts the raw agent name (e.g. `"claude"`), `updatePanelTitle` overwrites the enhanced title `"claude: Fix auth bug"` back to `"claude"`, which re-matches `isKnownAgentName`, spawning another background detection. The user sees the title briefly revert on each OSC cycle. Add a guard to bail out if the current panel title already has an enhanced form (e.g. starts with `title + ": "`).</violation>
<violation number="2" location="Sources/TabManager.swift:4648">
P2: Async agent title enhancement can apply stale results and overwrite newer panel title state because completion lacks title/TTY consistency checks.</violation>
</file>
<file name="Sources/TerminalAgentDetector.swift">
<violation number="1" location="Sources/TerminalAgentDetector.swift:29">
P2: Missing positive-value guards before the foreground check. When `pgid` and `tpgid` are both `0` (background/daemon processes within the same TTY session), `pgid == tpgid` evaluates to `true`, falsely treating them as foreground. The existing `TerminalSSHSessionDetector` guards against this with `pgid > 0 && tpgid > 0`.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| if TerminalAgentDetector.isKnownAgentName(title), | ||
| tab.panelCustomTitles[panelId] == nil, | ||
| let ttyName = tab.surfaceTTYNames[panelId] { |
There was a problem hiding this comment.
P1: This triggers repeated ps spawns and visible title flickering. When the shell re-broadcasts the raw agent name (e.g. "claude"), updatePanelTitle overwrites the enhanced title "claude: Fix auth bug" back to "claude", which re-matches isKnownAgentName, spawning another background detection. The user sees the title briefly revert on each OSC cycle. Add a guard to bail out if the current panel title already has an enhanced form (e.g. starts with title + ": ").
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/TabManager.swift, line 4631:
<comment>This triggers repeated `ps` spawns and visible title flickering. When the shell re-broadcasts the raw agent name (e.g. `"claude"`), `updatePanelTitle` overwrites the enhanced title `"claude: Fix auth bug"` back to `"claude"`, which re-matches `isKnownAgentName`, spawning another background detection. The user sees the title briefly revert on each OSC cycle. Add a guard to bail out if the current panel title already has an enhanced form (e.g. starts with `title + ": "`).</comment>
<file context>
@@ -4615,6 +4626,31 @@ class TabManager: ObservableObject {
}
+
+ // If the title matches a known agent binary, try to enhance it with task info
+ if TerminalAgentDetector.isKnownAgentName(title),
+ tab.panelCustomTitles[panelId] == nil,
+ let ttyName = tab.surfaceTTYNames[panelId] {
</file context>
| if TerminalAgentDetector.isKnownAgentName(title), | |
| tab.panelCustomTitles[panelId] == nil, | |
| let ttyName = tab.surfaceTTYNames[panelId] { | |
| if TerminalAgentDetector.isKnownAgentName(title), | |
| tab.panelCustomTitles[panelId] == nil, | |
| let ttyName = tab.surfaceTTYNames[panelId], | |
| !(tab.panelTitles[panelId]?.hasPrefix(title + ": ") ?? false) { |
| guard let self, | ||
| let tab = self.tabs.first(where: { $0.id == tabId }), | ||
| tab.panelCustomTitles[panelId] == nil else { return } | ||
| _ = tab.updatePanelTitle(panelId: panelId, title: enhanced) |
There was a problem hiding this comment.
P2: Async agent title enhancement can apply stale results and overwrite newer panel title state because completion lacks title/TTY consistency checks.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/TabManager.swift, line 4648:
<comment>Async agent title enhancement can apply stale results and overwrite newer panel title state because completion lacks title/TTY consistency checks.</comment>
<file context>
@@ -4615,6 +4626,31 @@ class TabManager: ObservableObject {
+ guard let self,
+ let tab = self.tabs.first(where: { $0.id == tabId }),
+ tab.panelCustomTitles[panelId] == nil else { return }
+ _ = tab.updatePanelTitle(panelId: panelId, title: enhanced)
+ if self.selectedTabId == tabId && tab.focusedPanelId == panelId {
+ self.updateWindowTitle(for: tab)
</file context>
| static func detect(forTTY ttyName: String) -> DetectedAgent? { | ||
| let snapshots = processSnapshots(forTTY: ttyName) | ||
| // Find foreground processes (pgid == tpgid) that are known agents | ||
| let foreground = snapshots.filter { $0.pgid == $0.tpgid } |
There was a problem hiding this comment.
P2: Missing positive-value guards before the foreground check. When pgid and tpgid are both 0 (background/daemon processes within the same TTY session), pgid == tpgid evaluates to true, falsely treating them as foreground. The existing TerminalSSHSessionDetector guards against this with pgid > 0 && tpgid > 0.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/TerminalAgentDetector.swift, line 29:
<comment>Missing positive-value guards before the foreground check. When `pgid` and `tpgid` are both `0` (background/daemon processes within the same TTY session), `pgid == tpgid` evaluates to `true`, falsely treating them as foreground. The existing `TerminalSSHSessionDetector` guards against this with `pgid > 0 && tpgid > 0`.</comment>
<file context>
@@ -0,0 +1,165 @@
+ static func detect(forTTY ttyName: String) -> DetectedAgent? {
+ let snapshots = processSnapshots(forTTY: ttyName)
+ // Find foreground processes (pgid == tpgid) that are known agents
+ let foreground = snapshots.filter { $0.pgid == $0.tpgid }
+ guard let match = foreground.first(where: { knownAgentBinaries.contains($0.executableName) }) else {
+ return nil
</file context>
| let foreground = snapshots.filter { $0.pgid == $0.tpgid } | |
| let foreground = snapshots.filter { $0.pgid > 0 && $0.tpgid > 0 && $0.pgid == $0.tpgid } |
Register the new file in project.pbxproj so xcodebuild can find it. Build verified locally with Xcode 26 + zig 0.15. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Workspace auto-naming and agent tab naming are shipped on current main. Thank you for this! :) |
Summary
Two quality-of-life improvements for users running multiple workspaces and AI agents simultaneously:
cmux ~/git/project) now names the sidebar card after the directory basename ("project") instead of "Terminal N"Motivation
When running 10+ workspaces and multiple AI agents per workspace, the default "Terminal N" workspace names and generic "Claude Code" tab names make it impossible to identify what's running where. These changes make the sidebar and tab bar immediately informative without any manual renaming.
Changes
Sources/TabManager.swiftaddWorkspace()(+8 lines); addenhanceAgentTitle()hook inupdatePanelTitle()(+25 lines)Sources/TerminalAgentDetector.swiftps, reads process args viaKERN_PROCARGS2, extracts task descriptionDesign decisions
customTitle— directory-derived name flows throughtitle/processTitle, so OSC terminal titles and manual renames still override naturallyDispatchQueue.global(qos: .utility)for process inspection, minimalDispatchQueue.main.asyncfor UI updateTerminalSSHSessionDetectorpattern — sameKERN_PROCARGS2/psapproach already proven in the codebaseTest plan
cmux ~/git/some-project→ sidebar shows "some-project" (not "Terminal N")cmux /→ still shows "Terminal N" (root "/" excluded)cmux(no directory) → still shows "Terminal N"claude "Fix auth bug"in a tab → tab title becomes "claude: Fix auth bug"codex exec "Add tests"→ tab title becomes "codex: Add tests"cmux rename-tab "My Name"→ custom name persists, agent detection does not overrideclaude(no prompt arg) → tab title stays "claude" (no crash, no empty enhancement)🤖 Generated with Claude Code
Summary by cubic
Auto-name workspaces from the opened directory and auto-label agent tabs with their task. This makes the sidebar and tab bar clear when running many workspaces and agents.
New Features
/or no directory still uses the default. Manual and OSC titles still win.claude,codex,gemini,opencode,aider,ft-claude) show "agent: task" from process args (e.g. "claude: Fix auth bug"). Honors custom titles and runs off the main thread.Bug Fixes
TerminalAgentDetector.swiftto the Xcode project so builds include it.Written for commit 7e5c784. Summary will update on new commits.
Summary by CodeRabbit