BroV look: swamp-zombie unibrow character, new app and menu bar icons, BroV naming in UI

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
maksarsanjeev
2026-10-06 19:53:47 +03:00
parent 0f77ec8704
commit 7d76986bc0
22 changed files with 422 additions and 54 deletions
+4 -4
View File
@@ -28,13 +28,13 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private func setupMenuBarItem() {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
guard let button = statusItem?.button else { return }
button.image = NSImage(named: "MenuBarIcon") ?? NSImage(systemSymbolName: "circle.fill", accessibilityDescription: "Coucou")
button.image = NSImage(named: "MenuBarIcon") ?? NSImage(systemSymbolName: "circle.fill", accessibilityDescription: "BroV")
button.image?.size = NSSize(width: 24, height: 18)
button.image?.accessibilityDescription = "Coucou"
button.image?.accessibilityDescription = "BroV"
button.image?.isTemplate = true
let menu = NSMenu()
menu.addItem(withTitle: "Open Coucou", action: #selector(openIsland), keyEquivalent: "")
menu.addItem(withTitle: "Open BroV", action: #selector(openIsland), keyEquivalent: "")
menu.addItem(.separator())
menu.addItem(withTitle: "Settings…", action: #selector(openSettings), keyEquivalent: ",")
menu.addItem(.separator())
@@ -69,7 +69,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
let win = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 720, height: 560),
styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered, defer: false)
win.title = "Settings — Coucou"
win.title = "Settings — BroV"
let host = NSHostingView(rootView: SettingsView())
host.sizingOptions = [.minSize]
win.contentView = host
+2 -2
View File
@@ -198,9 +198,9 @@ final class ClaudeService {
/// neutral otherwise — same wording as the Windows build.
private nonisolated static func makeSystemPrompt() -> String {
let opening = if let firstName = resolveUserFirstName() {
"You are Mochi, \(firstName)'s personal AI assistant embedded in the notch of their Mac."
"You are BroV, \(firstName)'s personal AI assistant embedded in the notch of their Mac."
} else {
"You are Mochi, a personal AI assistant embedded in the notch of the user's Mac."
"You are BroV, a personal AI assistant embedded in the notch of the user's Mac."
}
return """
\(opening) \
+28 -28
View File
@@ -19,7 +19,7 @@ final class HookServer: @unchecked Sendable {
static var socketPath: String {
#if APPSTORE
// Container home root keeps path ≤ 103 bytes (sun_path limit on macOS is 104 incl. NUL)
// /Users/louis/Library/Containers/fr.louisraille.Coucou/Data/nb.sock = 66 bytes ✓
// /Users/louis/Library/Containers/fr.louisraille.BroV/Data/nb.sock = 66 bytes ✓
return NSHomeDirectory() + "/nb.sock"
#else
return supportDir.appendingPathComponent("nb.sock").path
@@ -1093,7 +1093,7 @@ final class HookServer: @unchecked Sendable {
// MARK: - Outdated hook detection
/// Returns true if settings.json has a Coucou hook that needs updating:
/// Returns true if settings.json has a BroV hook that needs updating:
/// either a PermissionRequest hook with timeout < 120s, or the AskUserQuestion
/// PreToolUse matcher is missing (requires Claude Code 2.1.85+).
static func hooksNeedUpdate() -> Bool {
@@ -1104,8 +1104,8 @@ final class HookServer: @unchecked Sendable {
let hooks = settings["hooks"] as? [String: Any] else {
return false
}
// Track whether any Coucou hook is installed at all
var hasCoucouHooks = false
// Track whether any BroV hook is installed at all
var hasBroVHooks = false
if let permReqHooks = hooks["PermissionRequest"] as? [[String: Any]] {
for matcher in permReqHooks {
@@ -1113,7 +1113,7 @@ final class HookServer: @unchecked Sendable {
for hook in hookList {
if let cmd = hook["command"] as? String,
cmd.contains("NotchBuddy") || cmd.contains("coucou") {
hasCoucouHooks = true
hasBroVHooks = true
if let timeout = hook["timeout"] as? Int, timeout < 120 { return true }
}
}
@@ -1122,7 +1122,7 @@ final class HookServer: @unchecked Sendable {
}
// Check that the AskUserQuestion PreToolUse entry exists
if hasCoucouHooks {
if hasBroVHooks {
let preToolHooks = hooks["PreToolUse"] as? [[String: Any]] ?? []
let hasAskEntry = preToolHooks.contains { m in
(m["matcher"] as? String) == "AskUserQuestion"
@@ -1540,7 +1540,7 @@ final class HookServer: @unchecked Sendable {
label: "~/.gemini/settings.json")
if let raw = settings["hooks"], !(raw is [String: Any]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\" has an unexpected type — Coucou has not touched it."
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\" has an unexpected type — BroV has not touched it."
])
}
let base = hookBase()
@@ -1557,7 +1557,7 @@ final class HookServer: @unchecked Sendable {
for (geminiEvent, normalizedEvent, timeout) in events {
if let raw = hooks[geminiEvent], !(raw is [[String: Any]]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\"[\"\(geminiEvent)\"] has an unexpected type — Coucou has not touched it."
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\"[\"\(geminiEvent)\"] has an unexpected type — BroV has not touched it."
])
}
var groups = hooks[geminiEvent] as? [[String: Any]] ?? []
@@ -1581,7 +1581,7 @@ final class HookServer: @unchecked Sendable {
label: "~/.gemini/settings.json")
if let raw = settings["hooks"], !(raw is [String: Any]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\" has an unexpected type — Coucou has not touched it."
NSLocalizedDescriptionKey: "~/.gemini/settings.json: \"hooks\" has an unexpected type — BroV has not touched it."
])
}
if var hooks = settings["hooks"] as? [String: Any] {
@@ -1679,12 +1679,12 @@ final class HookServer: @unchecked Sendable {
do { data = try Data(contentsOf: url) }
catch {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "\(label) cannot be read — Coucou has not touched it."
NSLocalizedDescriptionKey: "\(label) cannot be read — BroV has not touched it."
])
}
guard let obj = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "\(label) is not valid JSON — Coucou has not touched it."
NSLocalizedDescriptionKey: "\(label) is not valid JSON — BroV has not touched it."
])
}
return obj
@@ -1735,7 +1735,7 @@ final class HookServer: @unchecked Sendable {
FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".codex/hooks.json")
}
/// True when ~/.codex/hooks.json already routes Codex events to Coucou's nb-hook.
/// True when ~/.codex/hooks.json already routes Codex events to BroV's nb-hook.
static func codexHooksInstalled() -> Bool {
guard let data = try? Data(contentsOf: codexHooksURL),
let root = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
@@ -1790,7 +1790,7 @@ final class HookServer: @unchecked Sendable {
var root = try Self.strictReadJSONObject(at: Self.codexHooksURL, label: "~/.codex/hooks.json")
if let raw = root["hooks"], !(raw is [String: Any]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\" has an unexpected type — Coucou has not touched it."
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\" has an unexpected type — BroV has not touched it."
])
}
let base = hookBase()
@@ -1800,7 +1800,7 @@ final class HookServer: @unchecked Sendable {
("SessionStart", 10, nil),
("UserPromptSubmit", 10, nil),
("PreToolUse", 10, nil),
("PermissionRequest", 120, "Waiting for your answer in the notch (Coucou)"),
("PermissionRequest", 120, "Waiting for your answer in the notch (BroV)"),
("PostToolUse", 10, nil),
("Stop", 10, nil),
("SubagentStart", 10, nil),
@@ -1812,11 +1812,11 @@ final class HookServer: @unchecked Sendable {
for (event, timeout, statusMsg) in events {
if let raw = hooks[event], !(raw is [[String: Any]]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\"[\"\(event)\"] has an unexpected type — Coucou has not touched it."
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\"[\"\(event)\"] has an unexpected type — BroV has not touched it."
])
}
var groups = hooks[event] as? [[String: Any]] ?? []
// Remove existing Coucou entries
// Remove existing BroV entries
groups = removeNbHookEntries(from: groups)
var hookEntry: [String: Any] = [
"type": "command",
@@ -1836,7 +1836,7 @@ final class HookServer: @unchecked Sendable {
var root = try Self.strictReadJSONObject(at: Self.codexHooksURL, label: "~/.codex/hooks.json")
if let raw = root["hooks"], !(raw is [String: Any]) {
throw NSError(domain: "Coucou", code: 2, userInfo: [
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\" has an unexpected type — Coucou has not touched it."
NSLocalizedDescriptionKey: "~/.codex/hooks.json: \"hooks\" has an unexpected type — BroV has not touched it."
])
}
if var hooks = root["hooks"] as? [String: Any] {
@@ -1874,7 +1874,7 @@ extension Notification.Name {
private let nbHookShellWrapper = """
#!/bin/sh
# Coucou hook relay — always exits 0, never blocks Claude Code
# BroV hook relay — always exits 0, never blocks Claude Code
HOOK_DIR="$(dirname "$0")"
if xcode-select -p >/dev/null 2>&1; then
out=$(/usr/bin/python3 "$HOOK_DIR/nb-hook.py" "$@" 2>/dev/null)
@@ -1890,8 +1890,8 @@ exit 0
private let nbHookPythonGitHub = """
#!/usr/bin/env python3
# nb-hook.py — Coucou hook relay for Claude Code and third-party agents (GitHub version)
# Reads JSON from stdin, forwards to Coucou via Unix socket, translates response.
# nb-hook.py — BroV hook relay for Claude Code and third-party agents (GitHub version)
# Reads JSON from stdin, forwards to BroV via Unix socket, translates response.
import sys, json, os, socket
def normalize_event(name):
@@ -1948,7 +1948,7 @@ def main():
'~/Library/Application Support/NotchBuddy/nb.sock'
)
# --statusline mode: relay rate_limits to Coucou, then delegate to saved previous
# --statusline mode: relay rate_limits to BroV, then delegate to saved previous
if '--statusline' in sys.argv[1:]:
relay = {
'coucou_kind': 'statusline',
@@ -2074,7 +2074,7 @@ def main():
# socket_path is already defined above
if event == 'PermissionRequest':
# Block and wait for Coucou's decision (Claude Code allows up to 120s)
# Block and wait for BroV's decision (Claude Code allows up to 120s)
try:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(118)
@@ -2115,7 +2115,7 @@ def main():
sys.stdout.flush()
sys.exit(0)
elif decision == 'deny':
out = {'hookSpecificOutput': {'hookEventName': 'PermissionRequest', 'decision': {'behavior': 'deny', 'message': 'Denied from Coucou'}}}
out = {'hookSpecificOutput': {'hookEventName': 'PermissionRequest', 'decision': {'behavior': 'deny', 'message': 'Denied from BroV'}}}
sys.stdout.write(json.dumps(out) + '\\n')
sys.stdout.flush()
sys.exit(0)
@@ -2157,7 +2157,7 @@ sys.exit(0)
private let nbHookPythonAppStore = """
#!/usr/bin/env python3
# nb-hook.py — Coucou (App Store) hook relay for Claude Code and third-party agents
# nb-hook.py — BroV (App Store) hook relay for Claude Code and third-party agents
# Socket lives inside the sandboxed container; script runs outside the sandbox.
import sys, json, os, socket
@@ -2212,10 +2212,10 @@ def main():
return
socket_path = os.path.expanduser(
'~/Library/Containers/fr.louisraille.Coucou/Data/nb.sock'
'~/Library/Containers/fr.louisraille.BroV/Data/nb.sock'
)
# --statusline mode: relay rate_limits to Coucou, then delegate to saved previous
# --statusline mode: relay rate_limits to BroV, then delegate to saved previous
if '--statusline' in sys.argv[1:]:
relay = {
'coucou_kind': 'statusline',
@@ -2340,7 +2340,7 @@ def main():
# socket_path is already defined above
if event == 'PermissionRequest':
# Block and wait for Coucou's decision (Claude Code allows up to 120s)
# Block and wait for BroV's decision (Claude Code allows up to 120s)
try:
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
s.settimeout(118)
@@ -2381,7 +2381,7 @@ def main():
sys.stdout.flush()
sys.exit(0)
elif decision == 'deny':
out = {'hookSpecificOutput': {'hookEventName': 'PermissionRequest', 'decision': {'behavior': 'deny', 'message': 'Denied from Coucou'}}}
out = {'hookSpecificOutput': {'hookEventName': 'PermissionRequest', 'decision': {'behavior': 'deny', 'message': 'Denied from BroV'}}}
sys.stdout.write(json.dumps(out) + '\\n')
sys.stdout.flush()
sys.exit(0)
@@ -3891,7 +3891,7 @@ struct MusicCardView: View {
.padding(.leading, 108)
.padding(.trailing, 36)
Text("Allow Coucou to control Music")
Text("Allow BroV to control Music")
.font(.system(size: 11))
.foregroundColor(Color(hex: "#8E939C"))
.padding(.leading, 108)
+9 -9
View File
@@ -119,7 +119,7 @@ struct SettingsView: View {
.resizable()
.frame(width: 32, height: 32)
VStack(alignment: .leading, spacing: 1) {
Text("Coucou")
Text("BroV")
.font(.system(size: 13, weight: .semibold))
Text(appVersion)
.font(.system(size: 11))
@@ -301,14 +301,14 @@ struct SettingsView: View {
VStack(alignment: .leading, spacing: 6) {
Toggle("Show my agent sessions on my iPhone", isOn: $iPhoneSyncEnabled)
.onChange(of: iPhoneSyncEnabled) { _, on in CloudProbe.shared.setEnabled(on) }
Text("Sends your sessions to your private iCloud for the Coucou iPhone app. Project names, commands and questions are encrypted with your iCloud keys. Turning it off deletes them from iCloud.")
Text("Sends your sessions to your private iCloud for the BroV iPhone app. Project names, commands and questions are encrypted with your iCloud keys. Turning it off deletes them from iCloud.")
.font(.system(size: 11))
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
Toggle("Move Mochi to my iPhone's Dynamic Island when my Mac is locked", isOn: $iPhoneLiveActivityEnabled)
.disabled(!iPhoneSyncEnabled)
.onChange(of: iPhoneLiveActivityEnabled) { _, on in LiveActivityRelay.shared.setEnabled(on) }
Text("Goes through the Coucou relay to Apple's push service. Only the agent's name and state are sent: no project name, command or path.")
Text("Goes through the BroV relay to Apple's push service. Only the agent's name and state are sent: no project name, command or path.")
.font(.system(size: 11))
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
@@ -332,7 +332,7 @@ struct SettingsView: View {
@ViewBuilder private var activePillsSection: some View {
GroupBox {
VStack(alignment: .leading, spacing: 10) {
Text("Choose the tools you use. Coucou only shows what you declare here.")
Text("Choose the tools you use. BroV only shows what you declare here.")
.font(.system(size: 11))
.foregroundColor(.secondary)
@@ -534,7 +534,7 @@ struct SettingsView: View {
GroupBox("Plan usage") {
VStack(alignment: .leading, spacing: 10) {
Text("Shows your Claude plan usage (5-hour and weekly limits) in the notch header. Coucou adds a status line relay to ~/.claude/settings.json. If you already have a status line, it keeps working as before. Pro and Max plans only.")
Text("Shows your Claude plan usage (5-hour and weekly limits) in the notch header. BroV adds a status line relay to ~/.claude/settings.json. If you already have a status line, it keeps working as before. Pro and Max plans only.")
.font(.system(size: 11))
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
@@ -936,7 +936,7 @@ struct SettingsView: View {
private func installHooksAppStore() {
guard let claudeURL = pickClaudeFolder(prompt: "Select") else { return }
let alert = NSAlert()
alert.messageText = "Install Coucou hooks in ~/.claude?"
alert.messageText = "Install BroV hooks in ~/.claude?"
alert.informativeText = "Will write:\n• ~/.claude/coucou/nb-hook\n• ~/.claude/settings.json (backup created first)"
alert.addButton(withTitle: "Install")
alert.addButton(withTitle: "Cancel")
@@ -1036,7 +1036,7 @@ struct SettingsView: View {
pendingGeminiJSON = try HookServer.shared.previewGeminiHooks(install: install)
showGeminiDiff = true
statusMessage = "Review the JSON below before confirming."
} catch let e as NSError where e.domain == "CoucouNoop" {
} catch let e as NSError where e.domain == "BroVNoop" {
statusMessage = e.localizedDescription
} catch {
statusMessage = "❌ \(error.localizedDescription)"
@@ -1063,7 +1063,7 @@ struct SettingsView: View {
pendingAgyJSON = try HookServer.shared.previewAgyHooks(install: install)
showAgyDiff = true
statusMessage = "Review the JSON below before confirming."
} catch let e as NSError where e.domain == "CoucouNoop" {
} catch let e as NSError where e.domain == "BroVNoop" {
statusMessage = e.localizedDescription
} catch {
statusMessage = "❌ \(error.localizedDescription)"
@@ -1090,7 +1090,7 @@ struct SettingsView: View {
pendingCodexJSON = try HookServer.shared.previewCodexHooks(install: install)
showCodexDiff = true
statusMessage = "Review the JSON below before confirming."
} catch let e as NSError where e.domain == "CoucouNoop" {
} catch let e as NSError where e.domain == "BroVNoop" {
statusMessage = e.localizedDescription
} catch {
statusMessage = "❌ \(error.localizedDescription)"
+3 -3
View File
@@ -2,7 +2,7 @@ import Foundation
// MARK: - Shortcut Action
/// Every keyboard shortcut Coucou knows about.
/// Every keyboard shortcut BroV knows about.
/// The first 10 cases are *global* (registered with Carbon and effective from any app).
/// Island-local shortcuts are documented in `ShortcutLogic.islandShortcuts` but are
/// not managed here — they are handled by a local NSEvent monitor in IslandWindowController.
@@ -46,7 +46,7 @@ enum ShortcutAction: String, CaseIterable, Sendable {
case .nextPill: return "Next pill"
case .prevPill: return "Previous pill"
case .muteToggle: return "Mute / unmute sounds"
case .desktopToggle: return "Mochi on / off desktop"
case .desktopToggle: return "BroV on / off desktop"
case .wardrobeToggle: return "Open / close wardrobe"
}
}
@@ -159,7 +159,7 @@ enum ShortcutLogic {
return c
}
// MARK: - Conflict detection (within Coucou's own shortcuts)
// MARK: - Conflict detection (within BroV's own shortcuts)
/// Returns the set of actions whose (keyCode, nsFlags) pair is shared with another action.
static func duplicates(in specs: [ShortcutAction: ShortcutSpec]) -> Set<ShortcutAction> {