native macOS codings agent orchestrator
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: improve prowl list output with colored, hierarchical formatting

Reorganize the text output to group by worktree with indented tabs and
panes. Add Rainbow dependency for terminal colors with --no-color flag
support. Worktree headers show project name extracted from path, tab/pane
rows include numbered labels, and focused pane is highlighted in green.
Redundant cwd display is suppressed when it matches the worktree path.

onevcat 50bc918d 540fb8b6

+100 -12
+10 -1
Package.resolved
··· 1 1 { 2 - "originHash" : "2df18def4fd8abdae1d0f5aeeda482240785c69802373a91637b7fa48e11e3ad", 2 + "originHash" : "6a83eb824bb8cc817831017cf72468c234a2d99470f15d658e3a30b8bddccd4c", 3 3 "pins" : [ 4 + { 5 + "identity" : "rainbow", 6 + "kind" : "remoteSourceControl", 7 + "location" : "https://github.com/onevcat/Rainbow", 8 + "state" : { 9 + "revision" : "cdf146ae671b2624917648b61c908d1244b98ca1", 10 + "version" : "4.2.1" 11 + } 12 + }, 4 13 { 5 14 "identity" : "swift-argument-parser", 6 15 "kind" : "remoteSourceControl",
+2
Package.swift
··· 19 19 ], 20 20 dependencies: [ 21 21 .package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.0"), 22 + .package(url: "https://github.com/onevcat/Rainbow", from: "4.0.0"), 22 23 ], 23 24 targets: [ 24 25 .target( ··· 30 31 dependencies: [ 31 32 "ProwlCLIShared", 32 33 .product(name: "ArgumentParser", package: "swift-argument-parser"), 34 + .product(name: "Rainbow", package: "Rainbow"), 33 35 ], 34 36 path: "ProwlCLI" 35 37 ),
+3 -1
ProwlCLI/CLIExecution.swift
··· 3 3 4 4 import ArgumentParser 5 5 import ProwlCLIShared 6 + @preconcurrency import Rainbow 6 7 7 8 enum CLIExecution { 8 - static func run(command: String, output: OutputMode, _ body: () throws -> Void) throws { 9 + static func run(command: String, output: OutputMode, colorEnabled: Bool = true, _ body: () throws -> Void) throws { 10 + Rainbow.enabled = colorEnabled 9 11 do { 10 12 try body() 11 13 } catch let error as ExitError {
+7
ProwlCLI/Commands/GlobalOptions.swift
··· 8 8 @Flag(name: .long, help: "Output in JSON format matching schema contracts.") 9 9 var json = false 10 10 11 + @Flag(name: .long, help: "Disable colored output.") 12 + var noColor = false 13 + 11 14 var outputMode: OutputMode { 12 15 json ? .json : .text 16 + } 17 + 18 + var colorEnabled: Bool { 19 + !noColor && !json 13 20 } 14 21 }
+1 -1
ProwlCLI/Commands/ListCommand.swift
··· 12 12 @OptionGroup var options: GlobalOptions 13 13 14 14 mutating func run() throws { 15 - try CLIExecution.run(command: "list", output: options.outputMode) { 15 + try CLIExecution.run(command: "list", output: options.outputMode, colorEnabled: options.colorEnabled) { 16 16 let envelope = CommandEnvelope( 17 17 output: options.outputMode, 18 18 command: .list(ListInput())
+77 -9
ProwlCLI/Output/OutputRenderer.swift
··· 3 3 4 4 import Foundation 5 5 import ProwlCLIShared 6 + import Rainbow 6 7 7 8 enum OutputRenderer { 8 9 static func render(_ response: CommandResponse, mode: OutputMode) { ··· 64 65 return "No panes found." 65 66 } 66 67 68 + // Group items by worktree, preserving order of first appearance. 69 + var worktreeOrder: [String] = [] 70 + var worktreeGroups: [String: [ListCommandItem]] = [:] 71 + for item in payload.items { 72 + let key = item.worktree.id 73 + if worktreeGroups[key] == nil { 74 + worktreeOrder.append(key) 75 + } 76 + worktreeGroups[key, default: []].append(item) 77 + } 78 + 67 79 var lines: [String] = [] 68 - lines.reserveCapacity(payload.items.count * 2) 69 80 70 - for item in payload.items { 71 - let status = item.task.status?.rawValue ?? "n/a" 72 - let focused = item.pane.focused ? "focused" : "-" 81 + for (index, worktreeID) in worktreeOrder.enumerated() { 82 + guard let items = worktreeGroups[worktreeID], let first = items.first else { continue } 83 + 84 + if index > 0 { 85 + lines.append("") 86 + } 87 + 88 + // Worktree header: "ProjectName:branch (status)" 89 + let projectName = projectName(from: first.worktree.path) 90 + let statusText: String 91 + switch first.task.status { 92 + case .running: 93 + statusText = "running".green 94 + case .idle: 95 + statusText = "idle".dim 96 + case nil: 97 + statusText = "n/a".dim 98 + } 73 99 lines.append( 74 - "\(item.worktree.name) | \(item.tab.title) | \(item.pane.title) | \(status) | \(focused)" 100 + "\(projectName.cyan.bold)\(":".dim)\(first.worktree.name) (\(statusText)) \(first.worktree.id.dim)" 75 101 ) 102 + lines.append(" \("path:".dim) \(first.worktree.path)") 76 103 77 - var detail = " worktree=\(item.worktree.id) tab=\(item.tab.id) pane=\(item.pane.id)" 78 - if let cwd = item.pane.cwd { 79 - detail += " cwd=\(cwd)" 104 + // Group panes by tab within this worktree. 105 + var tabOrder: [String] = [] 106 + var tabGroups: [String: [ListCommandItem]] = [:] 107 + for item in items { 108 + let tabKey = item.tab.id 109 + if tabGroups[tabKey] == nil { 110 + tabOrder.append(tabKey) 111 + } 112 + tabGroups[tabKey, default: []].append(item) 80 113 } 81 - lines.append(detail) 114 + 115 + let worktreePath = normalizeTrailingSlash(first.worktree.path) 116 + 117 + for (tabIndex, tabID) in tabOrder.enumerated() { 118 + guard let tabItems = tabGroups[tabID], let firstTab = tabItems.first else { continue } 119 + 120 + let tabNum = "Tab \(tabIndex + 1):" 121 + let selectedMark = firstTab.tab.selected ? "*".yellow : " " 122 + let tabTitle = firstTab.tab.selected ? firstTab.tab.title.yellow : firstTab.tab.title 123 + lines.append(" [\(selectedMark)] \(tabNum.dim) \(tabTitle)") 124 + 125 + for (paneIndex, item) in tabItems.enumerated() { 126 + let focusMark = item.pane.focused ? ">".green.bold : " " 127 + let paneNum = item.pane.focused ? "Pane \(paneIndex + 1):".green : "Pane \(paneIndex + 1):".dim 128 + let paneTitle = item.pane.focused ? item.pane.title.green.bold : item.pane.title.dim 129 + 130 + var paneLine = " \(focusMark) \(paneNum) \(paneTitle)" 131 + 132 + // Only show cwd when it differs from the worktree path. 133 + if let cwd = item.pane.cwd, normalizeTrailingSlash(cwd) != worktreePath { 134 + paneLine += " \(cwd.dim)" 135 + } 136 + 137 + paneLine += " \(item.pane.id.dim)" 138 + lines.append(paneLine) 139 + } 140 + } 82 141 } 83 142 84 143 return lines.joined(separator: "\n") 144 + } 145 + 146 + private static func projectName(from path: String) -> String { 147 + let trimmed = path.hasSuffix("/") ? String(path.dropLast()) : path 148 + return trimmed.split(separator: "/").last.map(String.init) ?? path 149 + } 150 + 151 + private static func normalizeTrailingSlash(_ path: String) -> String { 152 + path.hasSuffix("/") ? String(path.dropLast()) : path 85 153 } 86 154 }