Skip to content

feat: auto-name workspaces from directory + agent tab title enhancement - #2852

Closed
d-wwei wants to merge 2 commits into
manaflow-ai:mainfrom
d-wwei:feature/auto-workspace-and-agent-naming
Closed

d-wwei wants to merge 2 commits into
manaflow-ai:mainfrom
d-wwei:feature/auto-workspace-and-agent-naming

Conversation

@d-wwei

@d-wwei d-wwei commented Apr 12, 2026 •

Copy link
Copy Markdown

Summary

Two quality-of-life improvements for users running multiple workspaces and AI agents simultaneously:

  • Workspace auto-naming: Opening a directory (cmux ~/git/project) now names the sidebar card after the directory basename ("project") instead of "Terminal N"
  • Agent tab title enhancement: When a known AI agent (claude, codex, gemini, opencode, aider, ft-claude) runs in a tab, the tab title auto-updates to show the task description extracted from process arguments (e.g. "claude: Fix auth bug")

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

File Change
Sources/TabManager.swift Derive workspace title from directory basename in addWorkspace() (+8 lines); add enhanceAgentTitle() hook in updatePanelTitle() (+25 lines)
Sources/TerminalAgentDetector.swift New file (~150 lines) — detects known agent binaries via ps, reads process args via KERN_PROCARGS2, extracts task description

Design decisions

  • Workspace naming does NOT set customTitle — directory-derived name flows through title/processTitle, so OSC terminal titles and manual renames still override naturally
  • Agent detection runs off-main-thread (per socket command threading policy) — DispatchQueue.global(qos: .utility) for process inspection, minimal DispatchQueue.main.async for UI update
  • Follows TerminalSSHSessionDetector pattern — same KERN_PROCARGS2/ps approach already proven in the codebase
  • Title priority preserved: customTitle > agent OSC title > cmux-team rename-tab > agent-detected enhanced title > default process name

Test 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"
  • Run claude "Fix auth bug" in a tab → tab title becomes "claude: Fix auth bug"
  • Run codex exec "Add tests" → tab title becomes "codex: Add tests"
  • cmux rename-tab "My Name" → custom name persists, agent detection does not override
  • Interactive claude (no prompt arg) → tab title stays "claude" (no crash, no empty enhancement)

Note: I don't have Xcode on my current machine. swiftc -parse passes for the new file. Full build verification needed.

🤖 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

    • Workspace auto-naming: opening a directory names the workspace by basename (e.g. "project") instead of "Terminal N". Root / or no directory still uses the default. Manual and OSC titles still win.
    • Agent tab titles: known agents (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

    • Added TerminalAgentDetector.swift to the Xcode project so builds include it.

Written for commit 7e5c784. Summary will update on new commits.

Summary by CodeRabbit

  • New Features
    • Detects AI coding agents running in terminals and updates panel titles to show the agent name and a short task description.
    • Automatically derives workspace titles from directory names when no explicit title is provided.

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>
@vercel

vercel Bot commented Apr 12, 2026

Copy link
Copy Markdown

@d-wwei is attempting to deploy a commit to the Manaflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Apr 12, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 93c29963-738d-4748-a233-fa434bd5af57

📥 Commits

Reviewing files that changed from the base of the PR and between 7d7f03a and 7e5c784.

📒 Files selected for processing (1)
  • GhosttyTabs.xcodeproj/project.pbxproj
✅ Files skipped from review due to trivial changes (1)
  • GhosttyTabs.xcodeproj/project.pbxproj

📝 Walkthrough

Walkthrough

TabManager 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

Cohort / File(s) Summary
Tab manager changes
Sources/TabManager.swift
Compute derivedTitle from explicitWorkingDirectory basename for workspace creation; augment panel-title update flow to detect known agent binaries for a panel's TTY, asynchronously resolve agent/task, and set title to "<executable>: <task>" (refresh window title if focused).
Agent detection module
Sources/TerminalAgentDetector.swift
New file introducing TerminalAgentDetector with detect(forTTY:) returning DetectedAgent (executableName, taskDescription, pid); enumerates processes via /bin/ps, filters foreground processes, matches allowlisted binaries, and extracts argv via sysctl to build a human-readable task string.
Xcode project
GhosttyTabs.xcodeproj/project.pbxproj
Added TerminalAgentDetector.swift to the project and the app target's sources build phase.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I peeked inside the TTY glow,
Found clever agents stealing the show,
I named their tasks from argv crumbs,
Now panels sing where work was hums — hop! 🎩✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: workspace auto-naming from directory and agent tab title enhancement, matching the primary objectives.
Description check ✅ Passed The description covers all major template sections including summary, motivation, changes, design decisions, and test plan; testing checklist is incomplete but core content is comprehensive.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented Apr 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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 KERN_PROCARGS2.

  • Title flickering / repeated ps spawns: enhanceAgentTitle is triggered on every updatePanelTitle call with a known agent name. Because the enhanced title is written back via the workspace method directly (bypassing TabManager.updatePanelTitle), any subsequent OSC update that resets the title to "claude" registers as a change and fires another background detection — causing visible flickering and unbounded ps invocations for shells that re-broadcast the foreground process name.

Confidence Score: 4/5

Safe to merge after addressing the repeated-detection issue in updatePanelTitle; everything else is defensive or cleanup.

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 isKnownAgentName check in updatePanelTitle needs an "already enhanced" bail-out condition.

Important Files Changed

Filename Overview
Sources/TabManager.swift Adds workspace auto-naming from directory basename and enhanceAgentTitle hook in updatePanelTitle; the hook has no de-duplication guard so it fires (and spawns ps) on every subsequent OSC "claude" update, causing title flickering.
Sources/TerminalAgentDetector.swift New file following TerminalSSHSessionDetector pattern; missing pgid > 0 && tpgid > 0 foreground guards and duplicates parseKernProcArgs/commandLineArguments verbatim.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (1): Last reviewed commit: "feat: auto-name workspaces from director..." | Re-trigger Greptile

Comment thread Sources/TabManager.swift
Comment on lines +4631 to +4635
if TerminalAgentDetector.isKnownAgentName(title),
tab.panelCustomTitles[panelId] == nil,
let ttyName = tab.surfaceTTYNames[panelId] {
enhanceAgentTitle(tabId: tabId, panelId: panelId, ttyName: ttyName)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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.

Comment on lines +32 to +37
}
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)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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 }

Comment on lines +88 to +165
}
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
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between c42ecd7 and 7d7f03a.

📒 Files selected for processing (2)
  • Sources/TabManager.swift
  • Sources/TerminalAgentDetector.swift

Comment thread Sources/TabManager.swift
Comment on lines +1994 to +2004
// 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)"
}()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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)"
+                )
As per coding guidelines, "All user-facing strings must be localized. Use `String(localized: "key.name", defaultValue: "English text")` for every string shown in the UI."
🤖 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.

Comment thread Sources/TabManager.swift
Comment on lines +4630 to 4654
// 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)
}
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +127 to +133
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
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread Sources/TabManager.swift
Comment on lines +4631 to +4633
if TerminalAgentDetector.isKnownAgentName(title),
tab.panelCustomTitles[panelId] == nil,
let ttyName = tab.surfaceTTYNames[panelId] {

@cubic-dev-ai cubic-dev-ai Bot Apr 12, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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) {
Fix with Cubic

Comment thread Sources/TabManager.swift
guard let self,
let tab = self.tabs.first(where: { $0.id == tabId }),
tab.panelCustomTitles[panelId] == nil else { return }
_ = tab.updatePanelTitle(panelId: panelId, title: enhanced)

@cubic-dev-ai cubic-dev-ai Bot Apr 12, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with Cubic

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 }

@cubic-dev-ai cubic-dev-ai Bot Apr 12, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
let foreground = snapshots.filter { $0.pgid == $0.tpgid }
let foreground = snapshots.filter { $0.pgid > 0 && $0.tpgid > 0 && $0.pgid == $0.tpgid }
Fix with Cubic

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>
@teamleaderleo

Copy link
Copy Markdown
Collaborator

Workspace auto-naming and agent tab naming are shipped on current main. Thank you for this! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants