Skip to content

エージェント階層サイドバー + 色連携 + 独立タブ昇格 - #2742

Closed
yu-k1804 wants to merge 2 commits into
manaflow-ai:mainfrom
yu-k1804:claude/add-agent-sidebar
Closed

yu-k1804 wants to merge 2 commits into
manaflow-ai:mainfrom
yu-k1804:claude/add-agent-sidebar

Conversation

@yu-k1804

@yu-k1804 yu-k1804 commented Apr 9, 2026 •

Copy link
Copy Markdown

概要

Claude Code の agent teams で 1 ワークスペース内に複数エージェントが生成される際、左サイドバーに各 surface を子行として階層表示し、色やフォーカスジャンプ、独立タブ昇格を可能にする。

変更点

  • サイドバーに surface 子行を表示 (panels > 1 のとき自動展開)
  • Claude Code teams の select-pane -P から色を取り込み (surface.set_color socket コマンド)
  • tmux colour0-255 / named color / hex → #RRGGBB 変換ヘルパ
  • 親行の左レールに代表 panel 色を伝播
  • 子行右クリック → "Open in New Workspace" で独立タブ昇格
  • panelColors の SessionPersistence 永続化
  • cmux claude-teams-debug コマンドエイリアス追加

テスト

  • cmux claude-teams-debug で 3 体エージェント起動 → サイドバーに子行表示確認
  • 子行クリックで surface フォーカス確認
  • 子行右クリック → "Open in New Workspace" で独立タブ化確認
  • アプリ再起動後に色が復元されることを確認

Summary by cubic

Adds an agent surface hierarchy in the sidebar with per-surface colors and lets you promote a surface to its own workspace tab. Improves focus navigation and keeps colors in sync with tmux.

  • New Features
    • Sidebar shows child rows for each surface when a workspace has multiple panels. Click to focus. Colored dot reflects surface color. The first panel color is used on the workspace rail when no custom color is set.
    • Color sync from tmux via select-pane -P bg=…. Supports colour0-255, named colors, and hex, normalized to #RRGGBB. New socket command surface.set_color. Colors persist via panelColors in session snapshots.
    • Right-click a surface row → “Open in New Workspace” to detach it into a new tab. Carries over its title and color, closes the dummy terminal, and safely rolls back on failure.
    • CLI: add cmux claude-teams-debug as an alias for cmux claude-teams with updated help text.

Written for commit 09eb1e7. Summary will update on new commits.

Summary by CodeRabbit

Release Notes

  • New Features
    • Promote individual surfaces to independent workspaces via context menu
    • Customize and persist per-panel colors across sessions
    • Enhanced sidebar with improved panel organization and visual indicators
    • Workspace colors now automatically reflect representative panel colors when no custom color is set
    • Extended command support for surface color management in terminal automation

yu-k1804 added 2 commits April 9, 2026 15:49
…romotion

- Display per-surface child rows in sidebar when workspace has multiple panels
- Accept Claude Code team agent colors via select-pane -P tmux style parsing
- Add surface.set_color socket command for per-panel color assignment
- Propagate first panel color as workspace representative rail color
- Right-click surface row to promote it to a new independent workspace
- Persist panel colors across session restarts
@vercel

vercel Bot commented Apr 9, 2026

Copy link
Copy Markdown

@yu-k1804 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 9, 2026 •

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2d94682d-8e53-45a8-a261-d028a2f5eadf

📥 Commits

Reviewing files that changed from the base of the PR and between c2e97be and 09eb1e7.

📒 Files selected for processing (6)
  • CLI/cmux.swift
  • Sources/ContentView.swift
  • Sources/SessionPersistence.swift
  • Sources/TabManager.swift
  • Sources/TerminalController.swift
  • Sources/Workspace.swift

📝 Walkthrough

Walkthrough

This PR introduces per-panel color persistence and management across the application. It adds color parsing from tmux commands, new UI for displaying colored surface child rows in the sidebar, persistence of panel colors across sessions, a panel promotion feature that preserves colors when moving surfaces to new workspaces, and a new remote surface.set_color command endpoint.

Changes

Cohort / File(s) Summary
Core Color Data Model
Sources/Workspace.swift, Sources/SessionPersistence.swift
Added panelColors dictionary to persist per-panel color mappings; new setPanelColor(_:hex:) API with validation; session snapshot/restore integration for color persistence.
UI Surface Display
Sources/ContentView.swift
Added "surface child rows" section in sidebar showing panels as tappable items with context menu for promotion; new SurfaceChildRow view with color-based styling; workspace rail color now falls back to first available panel color.
Panel Promotion & Management
Sources/TabManager.swift
New promoteSurfaceToNewWorkspace(sourceWorkspace:panelId:) method that detaches a panel from its workspace, creates new workspace, reattaches panel, and applies captured color; includes rollback paths for failures.
CLI & Remote Commands
CLI/cmux.swift, Sources/TerminalController.swift
Added claude-teams-debug command alias; enhanced select-pane handling to parse tmux background color specs (hex, xterm-256 palette, named colors) and dispatch surface.set_color API; new V2 command endpoint surface.set_color for remote color updates.

Sequence Diagram(s)

sequenceDiagram
    actor Client as Client/Tmux<br/>(CLI)
    participant CMux as cmux.swift<br/>(CLI Handler)
    participant Term as TerminalController<br/>(API Router)
    participant WS as Workspace<br/>(State)
    participant UI as UI Layer<br/>(Observers)

    Client->>CMux: select-pane -P with bg=color
    CMux->>CMux: Parse tmux bg value<br/>(hex/colorN/named)
    CMux->>Term: surface.set_color command<br/>(surface_id, hex color)
    Term->>WS: setPanelColor(panelId, hex)
    WS->>WS: Normalize & validate hex<br/>Update panelColors dict
    WS->>UI: Emit panelColors change
    UI->>UI: Re-render surface row<br/>with new color
Loading
sequenceDiagram
    actor User as User
    participant UI as ContentView.swift<br/>(UI)
    participant TM as TabManager<br/>(Manager)
    participant SRC as Source Workspace<br/>(State)
    participant NEW as New Workspace<br/>(State)

    User->>UI: Tap "Promote" in context menu<br/>for surface child row
    UI->>TM: promoteSurfaceToNewWorkspace<br/>(sourceWorkspace, panelId)
    TM->>SRC: Capture panel color & title
    TM->>TM: addWorkspace(title, select)
    TM->>NEW: Find target pane
    TM->>SRC: Detach surface from<br/>source workspace
    TM->>NEW: Attach surface to<br/>new workspace pane
    TM->>TM: Close dummy panels
    TM->>NEW: setCustomColor<br/>(captured color)
    NEW->>UI: Emit workspace change
    UI->>UI: Display new workspace<br/>with inherited color
    Note over TM,NEW: On failure: reattach to source
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • #664: Both PRs enhance Workspace and UI color handling—this PR adds per-panel color storage and APIs, while that PR uses color resolution and UI display for workspace focus states.

Poem

🐰 Panels now painted in colors so bright,
Each surface with hues, a magnificent sight!
From CLI to UI, the colors do flow,
Promoting and persisting, watch workspaces glow! ✨

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

@yu-k1804 yu-k1804 closed this Apr 9, 2026
@yu-k1804
yu-k1804 deleted the claude/add-agent-sidebar branch April 9, 2026 07:14
@greptile-apps

greptile-apps Bot commented Apr 9, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces agent-hierarchy sidebar rendering for Claude Code agent teams: surface child rows with color dots in the sidebar, a surface.set_color socket command fed from tmux select-pane -P, session-persistent panelColors, representative rail color propagation to the parent workspace, and a "Open in New Workspace" promotion flow.

  • P1 — panelColors missing from pruneSurfaceMetadata: every other panel-keyed dict (panelTitles, panelDirectories, panelGitBranches, etc.) is filtered there; panelColors is not, so stale entries grow in memory and are re-serialized to disk across restarts (Sources/Workspace.swift).
  • P1 — missing Localizable.xcstrings entries: \"sidebar.surfaceChild.defaultTitle\" and \"sidebar.surfaceChild.openInNewWorkspace\" are not registered in Resources/Localizable.xcstrings; Japanese users see English fallback text, violating the project localization policy.

Confidence Score: 4/5

Two P1 issues need to be resolved before merging: missing panelColors pruning and missing Localizable.xcstrings entries.

The feature logic and TabItemView observation wiring are sound. Two P1 gaps — pruneSurfaceMetadata not covering panelColors (causing stale persistence) and the two new localization keys being absent from Localizable.xcstrings (required by project policy, breaks Japanese UI) — prevent a 5.

Sources/Workspace.swift (pruning gap) and Resources/Localizable.xcstrings (missing keys).

Vulnerabilities

No security concerns identified. The surface.set_color socket command validates surface_id against ws.panels before accepting a color, and color values are passed through WorkspaceTabColorSettings.normalizedHex before being stored or returned. The tmux style parser is pure string transformation with no eval or shell execution.

Important Files Changed

Filename Overview
Sources/Workspace.swift Adds panelColors: [UUID: String] published property and setPanelColor helper, wires both $panelColors and $panelTitles into sidebarObservationSignal, and persists colors in SessionWorkspaceSnapshot. Critical gap: panelColors is not added to pruneSurfaceMetadata, unlike every other panel-keyed dictionary, so stale entries accumulate and are re-serialized across restarts.
Sources/ContentView.swift Adds SurfaceChildRow component and injects child-row rendering into TabItemView body when tab.panels.count > 1; adds representativePanelColorHex for the parent rail. The .equatable() + sidebarObservationSignal contract is preserved correctly, but two new localization keys are missing from Localizable.xcstrings.
Sources/TabManager.swift Adds promoteSurfaceToNewWorkspace: detaches a surface from its source workspace, creates a new workspace, attaches the surface there, closes the auto-created dummy terminal, and applies the panel color as the new workspace custom color. Rollback paths correctly re-attach to source on failure.
Sources/TerminalController.swift Adds v2SurfaceSetColor socket handler dispatched via v2MainSync; routes surface.set_color in the command switch. Uses redundant double-normalization, but otherwise correct.
CLI/cmux.swift Adds claude-teams-debug alias, tmux style color parsing helpers, and wires select-pane -P to send surface.set_color. Color index math and named-color mappings are correct.
Sources/SessionPersistence.swift Adds optional panelColors: [String: String]? to SessionWorkspaceSnapshot; backward-compatible. Straightforward addition with no issues.

Sequence Diagram

sequenceDiagram
    participant CC as Claude Code (tmux)
    participant CLI as cmux CLI
    participant TC as TerminalController
    participant WS as Workspace
    participant SB as Sidebar (SwiftUI)

    CC->>CLI: select-pane -P bg=colour160 -t %42
    CLI->>CLI: parseTmuxStyleBackgroundHex() returns #CD0000
    CLI->>TC: surface.set_color {workspace_id, surface_id, color}
    TC->>TC: v2MainSync
    TC->>WS: setPanelColor(surfaceId, hex)
    WS->>WS: panelColors[surfaceId] = hex
    WS-->>SB: sidebarObservationSignal triggers re-render
    SB->>SB: SurfaceChildRow dot color updated

    Note over CC,SB: User right-clicks child row
    SB->>TC: promoteSurfaceToNewWorkspace(sourceWS, panelId)
    TC->>WS: detachSurface(panelId)
    TC->>TC: addWorkspace(title)
    TC->>WS: attachDetachedSurface(transfer, inPane)
    TC->>WS: closePanel(dummyId, force: true)
    TC->>WS: setCustomColor(panelColor)
Loading

Comments Outside Diff (1)

  1. Sources/Workspace.swift, line 7803-7812 (link)

    P1 panelColors missing from pruneSurfaceMetadata

    pruneSurfaceMetadata is the single authoritative cleanup point for every panel-keyed dictionary, yet panelColors was not added to it. Every other similar dict (panelTitles, panelDirectories, panelCustomTitles, panelGitBranches, etc.) is filtered there, so when panels are closed or pruned, panelColors will silently accumulate stale entries. These stale entries are then serialized via SessionWorkspaceSnapshot.panelColors and re-applied on the next app launch via applySnapshot.

    func pruneSurfaceMetadata(validSurfaceIds: Set<UUID>) {
        panelDirectories = panelDirectories.filter { validSurfaceIds.contains($0.key) }
        panelTitles = panelTitles.filter { validSurfaceIds.contains($0.key) }
        panelCustomTitles = panelCustomTitles.filter { validSurfaceIds.contains($0.key) }
        pinnedPanelIds = pinnedPanelIds.filter { validSurfaceIds.contains($0) }
        manualUnreadPanelIds = manualUnreadPanelIds.filter { validSurfaceIds.contains($0) }
        panelGitBranches = panelGitBranches.filter { validSurfaceIds.contains($0.key) }
        manualUnreadMarkedAt = manualUnreadMarkedAt.filter { validSurfaceIds.contains($0.key) }
        surfaceListeningPorts = surfaceListeningPorts.filter { validSurfaceIds.contains($0.key) }
        surfaceTTYNames = surfaceTTYNames.filter { validSurfaceIds.contains($0.key) }
        remoteDetectedSurfaceIds = remoteDetectedSurfaceIds.filter { validSurfaceIds.contains($0) }
        panelShellActivityStates = panelShellActivityStates.filter { validSurfaceIds.contains($0.key) }
        panelPullRequests = panelPullRequests.filter { validSurfaceIds.contains($0.key) }
    +   panelColors = panelColors.filter { validSurfaceIds.contains($0.key) }
        syncRemotePortScanTTYs()
        recomputeListeningPorts()
    }

Reviews (1): Last reviewed commit: "Add claude-teams-debug as alias for clau..." | Re-trigger Greptile

Comment thread Sources/ContentView.swift
Comment on lines +13017 to +13023
Button(String(localized: "sidebar.surfaceChild.openInNewWorkspace", defaultValue: "Open in New Workspace")) {
tabManager.promoteSurfaceToNewWorkspace(
sourceWorkspace: tab,
panelId: panelId
)
}
}

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 New localization keys missing from Localizable.xcstrings

"sidebar.surfaceChild.defaultTitle" and "sidebar.surfaceChild.openInNewWorkspace" are correctly using String(localized:defaultValue:), but neither key has been added to Resources/Localizable.xcstrings. Per the project policy in CLAUDE.md, every key must be registered with translations for all supported languages (English and Japanese). Without the entries, Japanese users will see English fallback text for these strings.

Comment thread CLI/cmux.swift
"'" + value.replacingOccurrences(of: "'", with: "'\"'\"'") + "'"
}

/// Parse a tmux pane style string (e.g. "bg=colour160,fg=white" or "bg=#C0392B")

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 Instance method with no self reference — consider static

parseTmuxStyleBackgroundHex closes over nothing from self; it only calls Self.xterm256ColorHex and Self.tmuxNamedColorHex (both already static). Marking it private static func would make the intent explicit and mirror the pattern of the two helpers it calls.

Suggested change
/// Parse a tmux pane style string (e.g. "bg=colour160,fg=white" or "bg=#C0392B")
private static func parseTmuxStyleBackgroundHex(_ style: String) -> String? {

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +4929 to +4935
}
guard let surfaceId = v2UUID(params, "surface_id"), ws.panels[surfaceId] != nil else {
result = .err(code: "not_found", message: "Surface not found", data: nil)
return
}

let colorRaw = v2String(params, "color")?.trimmingCharacters(in: .whitespacesAndNewlines)

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 Redundant double normalization

v2SurfaceSetColor normalizes colorRaw via WorkspaceTabColorSettings.normalizedHex(colorRaw), then passes the already-normalized string to ws.setPanelColor, which calls normalizedHex a second time. setPanelColor can be called directly with the raw input and the normalization happens exactly once inside it.

Suggested change
}
guard let surfaceId = v2UUID(params, "surface_id"), ws.panels[surfaceId] != nil else {
result = .err(code: "not_found", message: "Surface not found", data: nil)
return
}
let colorRaw = v2String(params, "color")?.trimmingCharacters(in: .whitespacesAndNewlines)
let colorRaw = v2String(params, "color")?.trimmingCharacters(in: .whitespacesAndNewlines)
if let colorRaw, !colorRaw.isEmpty {
ws.setPanelColor(surfaceId, hex: colorRaw)
if let stored = ws.panelColors[surfaceId] {
result = .ok(["surface_id": surfaceId.uuidString, "color": stored])
} else {
result = .err(code: "invalid_params", message: "Invalid color hex", data: nil)
}

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.

1 participant