Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

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

slab/menubar: per-edge session colors, inline session list, mute toggle

Polygon icon now strokes each edge with its own session's state color
(cyan working / pulsing red awaiting / blinking gray stale) instead of
homogenizing into one aggregate hue, so a hexagon of 3-working/3-paused
threads reads as 3 cyan + 3 throbbing red edges. Dropdown gets the same
treatment: sessions are inlined directly into the main menu (no more
nested submenu) and each row's status dot + label is tinted to match
the icon. Adds a "Mute ambient sonification" checkbox backed by
~/.local/share/slab/state/muted; claude-stop.sh respects it by skipping
chimes/TTS/beeps and force-stopping ambient while the flag is present.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+128 -74
+10 -1
slab/bin/claude-stop.sh
··· 22 22 ACTIVE_DIR="$SLAB_HOME/state/active-prompts" 23 23 AWAITING_DIR="$SLAB_HOME/state/awaiting-prompts" 24 24 SUBAGENT_DIR="$SLAB_HOME/state/active-subagents" 25 + MUTE_FLAG="$SLAB_HOME/state/muted" 25 26 mkdir -p "$(dirname "$LOG")" "$ACTIVE_DIR" "$AWAITING_DIR" "$SUBAGENT_DIR" 26 27 27 28 pkill -f claude-ping-repeat.sh 2>/dev/null ··· 102 103 fi 103 104 } 104 105 105 - if (( others == 0 )); then 106 + if [[ -e "$MUTE_FLAG" ]]; then 107 + # User asked for silence — keep the lid-aware sleep handoff but skip 108 + # every chime / TTS / pentatonic beep. Ambient is force-stopped so a 109 + # daemon-restarted pad doesn't sneak back on while muted. 110 + stop_ambient 111 + if (( others == 0 )) && [[ "$lid" == "Yes" ]]; then 112 + "$SLAB_BIN/claude-sleep" now 113 + fi 114 + elif (( others == 0 )); then 106 115 if [[ "$lid" == "Yes" ]]; then 107 116 stop_ambient 108 117 tired_stinger
+17
slab/menubar-swift/Sources/SlabMenubar/AppDelegate.swift
··· 154 154 ShellRunner.runAsync(Paths.claudeSleep, args: ["now"]) 155 155 } 156 156 157 + @objc func toggleMute() { 158 + let path = Paths.muteFlag 159 + let fm = FileManager.default 160 + if fm.fileExists(atPath: path) { 161 + try? fm.removeItem(atPath: path) 162 + } else { 163 + let dir = (path as NSString).deletingLastPathComponent 164 + try? fm.createDirectory(atPath: dir, withIntermediateDirectories: true) 165 + fm.createFile(atPath: path, contents: nil) 166 + // Stop any in-flight ambient pad / chime so toggling on shuts up 167 + // the speakers immediately instead of waiting for the next Stop. 168 + ShellRunner.runAsync("/usr/bin/pkill", args: ["-TERM", "-f", "lid-reactive.py"]) 169 + ShellRunner.runAsync("/usr/bin/pkill", args: ["-x", "afplay"]) 170 + } 171 + refresh() 172 + } 173 + 157 174 @objc func syncBoth() { syncMail(account: nil) } 158 175 @objc func syncAcMail() { syncMail(account: "ac-mail") } 159 176 @objc func syncJasMail() { syncMail(account: "jas-mail") }
+40 -41
slab/menubar-swift/Sources/SlabMenubar/IconRenderer.swift
··· 2 2 3 3 enum IconRenderer { 4 4 /// Render the menubar icon. When there are any active Claude sessions we 5 - /// draw a regular polygon — one edge per session, colored by its state 6 - /// (rainbow-pulsing for awaiting, steady cyan for working, dim gray for 7 - /// stale). N=1 is a single line, N=2 is two parallel bars, N≥3 is the 8 - /// matching n-gon (triangle, square, pentagon, …). The whole shape 9 - /// rotates slowly while any session is non-stale, faster with more 10 - /// awaiting work. Falls back to SF Symbols for idle / ambient / 11 - /// lid-closed states. 5 + /// draw a regular polygon — one edge per session, colored by THAT 6 + /// session's state: steady cyan for working, pulsing red for awaiting, 7 + /// slow-blinking gray for stale. N=1 is a single line, N=2 is two 8 + /// parallel bars, N≥3 is the matching n-gon (triangle, square, 9 + /// pentagon, …). The whole shape rotates slowly while any session is 10 + /// non-stale, faster with more awaiting work. Falls back to SF Symbols 11 + /// for idle / ambient / lid-closed states. 12 12 static func image(for state: StateSnapshot, phase: CGFloat = 0, rotation: CGFloat = 0) -> NSImage { 13 13 if !state.claudeSessions.isEmpty { 14 14 return polygonImage(state: state, phase: phase, rotation: rotation) ··· 95 95 let radius: CGFloat = 9.5 96 96 let lineWidth: CGFloat = 1.6 97 97 98 - // Color is homogenized across every edge — geometry shows count, 99 - // color shows aggregate health. 100 - let color = aggregateColor(state: state, phase: phase) 101 - 98 + // Per-edge color: each edge is one session, colored by that 99 + // session's own state. Geometry shows count; color shows which 100 + // threads are working vs paused vs stale at a glance. 102 101 if n == 1 { 103 102 // Single horizontal line, rotated. 104 103 drawSegment( 105 104 center: NSPoint(x: cx, y: cy), 106 105 length: 2 * radius, 107 106 angle: rotation, 108 - color: color, 107 + color: sessionColor(visible[0].state, phase: phase), 109 108 lineWidth: lineWidth 110 109 ) 111 110 } else if n == 2 { ··· 124 123 center: center, 125 124 length: length, 126 125 angle: rotation, 127 - color: color, 126 + color: sessionColor(visible[i].state, phase: phase), 128 127 lineWidth: lineWidth 129 128 ) 130 129 } ··· 140 139 + rotation 141 140 vertices.append(NSPoint(x: cx + radius * cos(theta), y: cy + radius * sin(theta))) 142 141 } 143 - color.setStroke() 144 - let path = NSBezierPath() 145 - path.lineWidth = lineWidth 146 - path.lineJoinStyle = .round 147 - path.move(to: vertices[0]) 148 - for k in 1..<n { 149 - path.line(to: vertices[k]) 142 + // Each edge stroked separately so its color reflects its own 143 + // session. Round caps make adjacent edges meet without visible 144 + // seams at the vertices. 145 + for k in 0..<n { 146 + let a = vertices[k] 147 + let b = vertices[(k + 1) % n] 148 + let path = NSBezierPath() 149 + path.move(to: a) 150 + path.line(to: b) 151 + path.lineWidth = lineWidth 152 + path.lineCapStyle = .round 153 + sessionColor(visible[k].state, phase: phase).setStroke() 154 + path.stroke() 150 155 } 151 - path.close() 152 - path.stroke() 153 156 } 154 157 155 158 if overflow > 0 { ··· 177 180 path.stroke() 178 181 } 179 182 180 - /// One color for the whole polygon, derived from aggregate state. Hue 181 - /// slides green → red as the awaiting ratio climbs (overall health), 182 - /// brightness pulses harder the more urgent it gets, and an all-stale 183 - /// shape blinks gray to signal "done, sessions still on disk." 184 - private static func aggregateColor(state: StateSnapshot, phase: CGFloat) -> NSColor { 185 - let sessions = state.claudeSessions 186 - let allStale = !sessions.isEmpty && sessions.allSatisfy { $0.state == .stale } 187 - if allStale { 183 + /// Per-session edge color. Working = steady cyan-teal. Awaiting = 184 + /// pulsing warm red, the loud "look at me" state. Stale = slow gray 185 + /// blink, "thread is dead but the marker's still on disk." 186 + private static func sessionColor(_ state: ClaudeSession.State, phase: CGFloat) -> NSColor { 187 + switch state { 188 + case .working: 189 + return NSColor(deviceHue: 0.50, saturation: 0.60, brightness: 0.82, alpha: 1.0) 190 + case .awaiting: 191 + // Pulse brightness at 2 Hz so paused threads visibly throb 192 + // among the steady cyan working ones. 193 + let pulse = 0.5 + 0.5 * cos(phase * .pi * 4) 194 + let brightness = 0.70 + 0.30 * pulse 195 + return NSColor(deviceHue: 0.0, saturation: 0.92, brightness: brightness, alpha: 1.0) 196 + case .stale: 188 197 let blink = 0.5 + 0.5 * cos(phase * .pi * 2) 189 - return NSColor(deviceWhite: 0.30 + 0.45 * blink, alpha: 1.0) 198 + return NSColor(deviceWhite: 0.28 + 0.32 * blink, alpha: 1.0) 190 199 } 191 - let total = max(1, sessions.count) 192 - let urgency = CGFloat(state.awaitingCount) / CGFloat(total) 193 - // Health gradient: 0.35 (green) when calm, 0.0 (red) when fully awaiting. 194 - let hue = 0.35 * (1 - urgency) 195 - let saturation = 0.55 + 0.40 * urgency 196 - // Brightness pulses with depth proportional to urgency. At urgency=0 197 - // the shape is steady; at urgency=1 it pulses fully. 198 - let pulse = 0.5 + 0.5 * cos(phase * .pi * 4) 199 - let brightness = 0.85 - 0.35 * urgency * (1 - pulse) 200 - return NSColor(deviceHue: hue, saturation: saturation, brightness: brightness, alpha: 1.0) 201 200 } 202 201 203 202 private static func fallbackImage() -> NSImage {
+55 -32
slab/menubar-swift/Sources/SlabMenubar/MenuBuilder.swift
··· 8 8 menu.addItem(info("Status: \(state.statusLine)")) 9 9 menu.addItem(.separator()) 10 10 11 - menu.addItem(buildClaude(state: state, target: target)) 11 + appendClaude(to: menu, state: state, target: target) 12 12 menu.addItem(info("Subagents in flight: \(state.activeSubagents)")) 13 13 menu.addItem(.separator()) 14 14 ··· 20 20 stayAwake.state = state.sleepDisabled ? .on : .off 21 21 menu.addItem(stayAwake) 22 22 menu.addItem(item("Sleep now", selector: #selector(AppDelegate.sleepNow), target: target)) 23 + 24 + let mute = item("Mute ambient sonification", selector: #selector(AppDelegate.toggleMute), target: target) 25 + mute.state = state.muted ? .on : .off 26 + menu.addItem(mute) 23 27 menu.addItem(.separator()) 24 28 25 29 menu.addItem(item("Open daemon log", selector: #selector(AppDelegate.openDaemonLog), target: target)) ··· 77 81 return parent 78 82 } 79 83 80 - private static func buildClaude(state: StateSnapshot, target: AppDelegate) -> NSMenuItem { 84 + /// Inline the Claude session list straight into the main menu so the 85 + /// user can see (and click into) every thread without drilling through 86 + /// a submenu. Each row is colored with the same palette as its icon 87 + /// edge — cyan for working, red for awaiting, gray for stale — so the 88 + /// menubar polygon and the dropdown read as the same picture. 89 + private static func appendClaude(to menu: NSMenu, state: StateSnapshot, target: AppDelegate) { 81 90 let sessions = state.claudeSessions 82 - let label: String 91 + let header: String 83 92 if sessions.isEmpty { 84 - label = "Claude: idle" 93 + header = "Claude: idle" 85 94 } else if state.anyAwaiting { 86 - label = "Claude: \(state.awaitingCount) awaiting · \(sessions.count) active" 95 + header = "Claude: \(state.awaitingCount) awaiting · \(sessions.count) active" 87 96 } else { 88 - label = "Claude: \(sessions.count) active" 97 + header = "Claude: \(sessions.count) active" 89 98 } 90 - let parent = NSMenuItem(title: label, action: nil, keyEquivalent: "") 91 - let sub = NSMenu() 99 + menu.addItem(info(header)) 92 100 93 - if sessions.isEmpty { 94 - sub.addItem(info("(no active prompts)")) 95 - } else { 96 - for s in sessions { 97 - let dot: String 98 - switch s.state { 99 - case .awaiting: dot = "◉" 100 - case .working: dot = "●" 101 - case .stale: dot = "○" 102 - } 103 - let tail = s.cwdLabel.isEmpty ? "" : " · \(s.cwdLabel)" 104 - let title = "\(dot) \(s.shortSubject)\(tail)" 105 - let entry = NSMenuItem( 106 - title: title, 107 - action: #selector(AppDelegate.focusClaudeSession(_:)), 108 - keyEquivalent: "" 109 - ) 110 - entry.target = target 111 - entry.representedObject = s.tty 112 - entry.toolTip = sessionTooltip(s) 113 - entry.isEnabled = !s.tty.isEmpty 114 - sub.addItem(entry) 101 + if sessions.isEmpty { return } 102 + 103 + for s in sessions { 104 + let dot: String 105 + switch s.state { 106 + case .awaiting: dot = "◉" 107 + case .working: dot = "●" 108 + case .stale: dot = "○" 115 109 } 110 + let tail = s.cwdLabel.isEmpty ? "" : " · \(s.cwdLabel)" 111 + let entry = NSMenuItem( 112 + title: "\(dot) \(s.shortSubject)\(tail)", 113 + action: #selector(AppDelegate.focusClaudeSession(_:)), 114 + keyEquivalent: "" 115 + ) 116 + entry.target = target 117 + entry.representedObject = s.tty 118 + entry.toolTip = sessionTooltip(s) 119 + entry.isEnabled = !s.tty.isEmpty 120 + entry.attributedTitle = coloredTitle(for: s, dot: dot, tail: tail) 121 + menu.addItem(entry) 116 122 } 123 + } 117 124 118 - parent.submenu = sub 119 - return parent 125 + private static func coloredTitle(for s: ClaudeSession, dot: String, tail: String) -> NSAttributedString { 126 + let full = "\(dot) \(s.shortSubject)\(tail)" 127 + let attr = NSMutableAttributedString(string: full) 128 + let dotColor: NSColor 129 + switch s.state { 130 + case .working: dotColor = NSColor(deviceHue: 0.50, saturation: 0.60, brightness: 0.82, alpha: 1.0) 131 + case .awaiting: dotColor = NSColor(deviceHue: 0.0, saturation: 0.92, brightness: 0.92, alpha: 1.0) 132 + case .stale: dotColor = NSColor(deviceWhite: 0.55, alpha: 1.0) 133 + } 134 + // Color the status dot strong, the rest of the line in the dot's 135 + // hue at lower intensity so the row is glanceable but the text is 136 + // still readable in both light and dark menubar themes. 137 + let dotRange = NSRange(location: 0, length: (dot as NSString).length) 138 + attr.addAttribute(.foregroundColor, value: dotColor, range: dotRange) 139 + let textRange = NSRange(location: dotRange.length, length: (full as NSString).length - dotRange.length) 140 + let textColor = dotColor.blended(withFraction: 0.55, of: NSColor.labelColor) ?? NSColor.labelColor 141 + attr.addAttribute(.foregroundColor, value: textColor, range: textRange) 142 + return attr 120 143 } 121 144 122 145 private static func sessionTooltip(_ s: ClaudeSession) -> String {
+4
slab/menubar-swift/Sources/SlabMenubar/Paths.swift
··· 29 29 static var passphraseSocket: String { "\(home)/.ac-daemon.sock" } 30 30 31 31 static var ambientFlag: String { "/tmp/slab-ambient-active" } 32 + /// Persistent mute flag. When this file exists, claude-stop.sh skips 33 + /// chimes and stops ambient instead of starting it. Toggled from the 34 + /// menubar's "Mute ambient sonification" item. 35 + static var muteFlag: String { "\(slabHome)/state/muted" } 32 36 } 33 37 34 38 enum Tools {
+2
slab/menubar-swift/Sources/SlabMenubar/StateSnapshot.swift
··· 6 6 var activePrompts: Int = 0 7 7 var activeSubagents: Int = 0 8 8 var ambientActive: Bool = false 9 + var muted: Bool = false 9 10 var tailnetPeers: [TailnetPeer] = [] 10 11 var claudeSessions: [ClaudeSession] = [] 11 12 ··· 32 33 s.activePrompts = countFiles(in: Paths.activePromptsDir) 33 34 s.activeSubagents = countFiles(in: Paths.activeSubagentsDir) 34 35 s.ambientActive = FileManager.default.fileExists(atPath: Paths.ambientFlag) 36 + s.muted = FileManager.default.fileExists(atPath: Paths.muteFlag) 35 37 s.tailnetPeers = TailnetPeer.query() 36 38 s.claudeSessions = ClaudeSessionReader.active() 37 39 return s