← back to Ticket System
auto-data-snapshot: 2026-09-11T14:39:38 (2 data files) — desktop-bar/TicketBar.swift.bak-20260911-143435 run-ticket.sh.bak-20260911-142309
16c35905946e244f9823864e641754b358380e9a · 2026-09-11 14:39:40 -0700 · auto-commit-fleet
Files touched
A desktop-bar/TicketBar.swift.bak-20260911-143435A run-ticket.sh.bak-20260911-142309
Diff
commit 16c35905946e244f9823864e641754b358380e9a
Author: auto-commit-fleet <steve@designerwallcoverings.com>
Date: Fri Sep 11 14:39:40 2026 -0700
auto-data-snapshot: 2026-09-11T14:39:38 (2 data files) — desktop-bar/TicketBar.swift.bak-20260911-143435 run-ticket.sh.bak-20260911-142309
---
desktop-bar/TicketBar.swift.bak-20260911-143435 | 967 ++++++++++++++++++++++++
run-ticket.sh.bak-20260911-142309 | 82 ++
2 files changed, 1049 insertions(+)
diff --git a/desktop-bar/TicketBar.swift.bak-20260911-143435 b/desktop-bar/TicketBar.swift.bak-20260911-143435
new file mode 100644
index 00000000..385c488d
--- /dev/null
+++ b/desktop-bar/TicketBar.swift.bak-20260911-143435
@@ -0,0 +1,967 @@
+import AppKit
+import WebKit
+
+private let ledgerURL = FileManager.default.homeDirectoryForCurrentUser
+ .appendingPathComponent(".claude/tickets/events.jsonl")
+private let viewerURL = URL(string: "http://127.0.0.1:9794/?section=all&layout=grid")!
+
+private struct TicketState {
+ var id = ""
+ var title = ""
+ var status = "open"
+ var updated = ""
+ var created = ""
+}
+
+private final class BarTile: NSButton {
+ let key: String
+ let label: NSTextField
+ var menuProvider: (() -> NSMenu)?
+ var clickHandler: (() -> Void)?
+ var dragHandler: ((String, CGFloat) -> Void)?
+ var shouldSuppressClick: (() -> Bool)?
+ var widthConstraint: NSLayoutConstraint!
+
+ init(key: String, label: NSTextField, width: CGFloat) {
+ self.key = key
+ self.label = label
+ super.init(frame: .zero)
+ title = ""
+ isBordered = false
+ target = self
+ action = #selector(showDropdown)
+ setAccessibilityLabel("\(key.capitalized) ticket tile")
+ wantsLayer = true
+ layer?.cornerRadius = 6
+ layer?.backgroundColor = NSColor(calibratedWhite: 0.11, alpha: 0.9).cgColor
+ label.translatesAutoresizingMaskIntoConstraints = false
+ addSubview(label)
+ NSLayoutConstraint.activate([
+ label.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 9),
+ label.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -9),
+ label.centerYAnchor.constraint(equalTo: centerYAnchor)
+ ])
+ widthConstraint = widthAnchor.constraint(equalToConstant: width)
+ widthConstraint.isActive = true
+ heightAnchor.constraint(equalToConstant: 28).isActive = true
+ toolTip = "Click for details and layout controls"
+ }
+
+ required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
+ override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
+ @objc private func showDropdown() {
+ // A drag (vertical bar-slide or horizontal tile-reorder) sets this true so the
+ // trailing mouse-up click doesn't also pop the menu.
+ if shouldSuppressClick?() == true { return }
+ NSLog("TicketBar tile pressed: %@", key)
+ guard let menu = menuProvider?() else { return }
+ NSApp.activate(ignoringOtherApps: true)
+ menu.popUp(positioning: nil, at: NSPoint(x: 4, y: bounds.height - 2), in: self)
+ }
+
+ override func rightMouseDown(with event: NSEvent) {
+ guard let menu = menuProvider?() else { return }
+ NSApp.activate(ignoringOtherApps: true)
+ menu.popUp(positioning: nil, at: NSPoint(x: 4, y: bounds.height - 2), in: self)
+ }
+}
+
+private final class BarController: NSObject, NSApplicationDelegate, NSWindowDelegate, WKNavigationDelegate {
+ private var panel: NSPanel!
+ private let openLabel = NSTextField(labelWithString: "OPEN …")
+ private let blockedLabel = NSTextField(labelWithString: "BLOCKED …")
+ private let doingLabel = NSTextField(labelWithString: "DOING …")
+ private let working = NSTextField(labelWithString: "Working · —")
+ private let latest = NSTextField(labelWithString: "Latest · —")
+ private let cpuLabel = NSTextField(labelWithString: "CPU · —")
+ private var stack: NSStackView!
+ private var tiles: [BarTile] = []
+ private var openButton: NSButton!
+ private var themeButton: NSButton!
+ private var posButton: NSButton!
+ private var dotsButton: NSButton!
+ private var sniperButton: NSButton!
+ private var hamburger: NSButton!
+ private var isNight = true
+ private var barPosition = "top" // top | left | right | off
+ private var horizWidths: [String: CGFloat] = [:] // tile widths captured for horizontal (top) mode
+ private let vbarWidth: CGFloat = 300
+ private var contentContainer: NSView!
+ private var stackConstraints: [NSLayoutConstraint] = []
+ private var alignCenterY: NSLayoutConstraint! // vertical centering (Top mode)
+ private var alignTop: NSLayoutConstraint! // top-pinned (Left/Right docks)
+ private var statusDetails: [String: [String]] = [:]
+ private var workingDetails: [String] = []
+ private var latestDetail = "No activity yet"
+ private var ticketPopup: NSWindow?
+ private var timer: Timer?
+ private var dragMonitor: Any?
+ private var draggingKey: String?
+ private var dragStartX: CGFloat?
+ private var dragStartMouseY: CGFloat?
+ private var dragStartPanelY: CGFloat?
+ private var barSlid = false
+ private var didDrag = false
+ private var resizing = false
+ private var resizeStartMouse = CGPoint.zero
+ private var resizeStartSize: CGFloat = 0
+ private var gripView: NSView!
+
+ func applicationDidFinishLaunching(_ notification: Notification) {
+ NSApp.setActivationPolicy(.accessory)
+ buildPanel()
+ refresh()
+ refreshSystem()
+ timer = Timer.scheduledTimer(withTimeInterval: 4, repeats: true) { [weak self] _ in self?.refresh() }
+ NotificationCenter.default.addObserver(self, selector: #selector(reposition), name: NSApplication.didChangeScreenParametersNotification, object: nil)
+ }
+
+ private func buildPanel() {
+ panel = NSPanel(contentRect: .zero, styleMask: [.borderless, .nonactivatingPanel], backing: .buffered, defer: false)
+ panel.level = .statusBar
+ panel.collectionBehavior = [.canJoinAllSpaces, .stationary, .fullScreenAuxiliary]
+ panel.isOpaque = false
+ panel.backgroundColor = NSColor(calibratedWhite: 0.055, alpha: 0.97)
+ panel.hasShadow = true
+ panel.hidesOnDeactivate = false
+
+ isNight = UserDefaults.standard.object(forKey: "ticketBarNight") as? Bool ?? true
+ barPosition = UserDefaults.standard.string(forKey: "ticketBarPosition") ?? "top"
+ let countLabels = [openLabel, blockedLabel, doingLabel]
+ for label in countLabels + [working, latest] {
+ label.font = .systemFont(ofSize: 12, weight: countLabels.contains(label) ? .bold : .medium)
+ label.lineBreakMode = .byTruncatingTail
+ label.maximumNumberOfLines = 1
+ }
+ // Text/tile/panel colors are theme-driven — see applyTheme().
+
+ let savedWidths = UserDefaults.standard.dictionary(forKey: "ticketBarWidths") as? [String: CGFloat] ?? [:]
+ // OPEN / BLOCKED / DOING are now THREE separate tiles — each opens its own
+ // dropdown listing that status's tickets. Working + Latest unchanged.
+ let definitions: [(String, NSTextField, CGFloat)] = [
+ ("open", openLabel, 150), ("blocked", blockedLabel, 175), ("doing", doingLabel, 150),
+ ("working", working, 560), ("latest", latest, 520)
+ ]
+ let savedOrder = UserDefaults.standard.stringArray(forKey: "ticketBarOrder") ?? definitions.map(\.0)
+ let byKey = Dictionary(uniqueKeysWithValues: definitions.map { ($0.0, $0) })
+ let ordered = savedOrder.compactMap { byKey[$0] } + definitions.filter { !savedOrder.contains($0.0) }
+ tiles = ordered.map { key, label, defaultWidth in
+ let tile = BarTile(key: key, label: label, width: savedWidths[key] ?? defaultWidth)
+ tile.dragHandler = { [weak self] key, x in self?.moveTile(key, toward: x) }
+ tile.menuProvider = { [weak self] in self?.menu(for: key) ?? NSMenu() }
+ tile.shouldSuppressClick = { [weak self] in self?.didDrag ?? false }
+ // No clickHandler: a clean left-click opens the tile's DROPDOWN menu (details
+ // + layout controls). A vertical drag slides the whole bar; a horizontal drag
+ // on a tile reorders it. The web control window is reachable from the menu's
+ // "Open Control Window" item and from the "Open Fleet ↗" button.
+ return tile
+ }
+ horizWidths = Dictionary(uniqueKeysWithValues: tiles.map { ($0.key, $0.widthConstraint.constant) })
+ let open = NSButton(title: "Open Fleet ↗", target: self, action: #selector(openViewer))
+ open.bezelStyle = .inline
+ openButton = open
+ themeButton = NSButton(title: isNight ? "🌙" : "☀️", target: self, action: #selector(toggleTheme))
+ themeButton.bezelStyle = .inline
+ themeButton.isBordered = false
+ themeButton.toolTip = "Toggle night / day"
+ posButton = NSButton(title: "▔", target: self, action: #selector(cyclePosition))
+ posButton.bezelStyle = .inline
+ posButton.isBordered = false
+ posButton.toolTip = "Dock position: top → left → right → off"
+ // ◉ Terminal-dots dropdown — built FRESH on each open by shelling out to the
+ // /allcolordots scanner (see showDotsMenu). Styled like the theme/pos buttons.
+ dotsButton = NSButton(title: "◉", target: self, action: #selector(showDotsMenu))
+ dotsButton.bezelStyle = .inline
+ dotsButton.isBordered = false
+ dotsButton.toolTip = "Terminal dots"
+ cpuLabel.font = .monospacedDigitSystemFont(ofSize: 12, weight: .medium)
+ cpuLabel.lineBreakMode = .byTruncatingTail
+ cpuLabel.alignment = .center
+ cpuLabel.widthAnchor.constraint(equalToConstant: 112).isActive = true
+ sniperButton = NSButton(title: "RAM Sniper · ON", target: self, action: #selector(toggleRamSniper))
+ sniperButton.bezelStyle = .inline
+ sniperButton.wantsLayer = true
+ sniperButton.layer?.cornerRadius = 6
+ sniperButton.layer?.borderWidth = 1
+ sniperButton.toolTip = "Toggle the background CPU/RAM priority sniper"
+ sniperButton.setContentHuggingPriority(.required, for: .horizontal)
+ stack = NSStackView(views: tiles + [cpuLabel, sniperButton, open, themeButton, posButton, dotsButton])
+ stack.orientation = .horizontal
+ stack.spacing = 6
+ stack.alignment = .centerY
+ stack.distribution = .fill
+ for tile in tiles { tile.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) }
+ open.setContentHuggingPriority(.required, for: .horizontal)
+ themeButton.setContentHuggingPriority(.required, for: .horizontal)
+ posButton.setContentHuggingPriority(.required, for: .horizontal)
+ dotsButton.setContentHuggingPriority(.required, for: .horizontal)
+
+ // Center the tile group in the bar. Collapsed hamburger (off-mode) lives in the
+ // top-left corner and is shown only when barPosition == "off". In off mode the
+ // stack is REMOVED from the view so its width can't force the panel wide.
+ contentContainer = NSView()
+ let container = contentContainer!
+ stack.translatesAutoresizingMaskIntoConstraints = false
+ container.clipsToBounds = true
+ container.addSubview(stack)
+ alignCenterY = stack.centerYAnchor.constraint(equalTo: container.centerYAnchor)
+ alignTop = stack.topAnchor.constraint(equalTo: container.topAnchor, constant: 10)
+ stackConstraints = [
+ stack.centerXAnchor.constraint(equalTo: container.centerXAnchor),
+ stack.leadingAnchor.constraint(greaterThanOrEqualTo: container.leadingAnchor, constant: 14),
+ stack.trailingAnchor.constraint(lessThanOrEqualTo: container.trailingAnchor, constant: -10)
+ ]
+ NSLayoutConstraint.activate(stackConstraints + [alignCenterY])
+ hamburger = NSButton(title: "☰", target: self, action: #selector(openFromHamburger))
+ hamburger.isBordered = false
+ hamburger.font = .systemFont(ofSize: 18, weight: .bold)
+ hamburger.contentTintColor = .white
+ hamburger.toolTip = "Show ticket bar"
+ hamburger.translatesAutoresizingMaskIntoConstraints = false
+ hamburger.isHidden = true
+ container.addSubview(hamburger)
+ NSLayoutConstraint.activate([
+ hamburger.centerXAnchor.constraint(equalTo: container.centerXAnchor),
+ hamburger.centerYAnchor.constraint(equalTo: container.centerYAnchor)
+ ])
+ // Visible resize grip at the draggable edge (positioned per-dock in applyPosition).
+ gripView = NSView(frame: .zero)
+ gripView.wantsLayer = true
+ gripView.layer?.backgroundColor = NSColor(calibratedRed: 0.30, green: 0.60, blue: 1.0, alpha: 0.9).cgColor
+ gripView.layer?.cornerRadius = 2
+ gripView.toolTip = "Drag to resize the bar"
+ container.addSubview(gripView)
+ panel.contentView = container
+ installDragMonitor()
+ applyTheme()
+ applyPosition()
+ panel.orderFrontRegardless()
+ }
+
+ // The draggable resize edge for the current dock, in window (bottom-left) coords.
+ private func inResizeZone(_ loc: CGPoint) -> Bool {
+ let f = panel.frame
+ switch barPosition {
+ case "left": return loc.x >= f.width - 12
+ case "right": return loc.x <= 12
+ case "top": return loc.y <= 9
+ default: return false
+ }
+ }
+ private func positionGrip() {
+ guard gripView != nil else { return }
+ let f = panel.frame
+ switch barPosition {
+ case "left": gripView.isHidden = false; gripView.frame = NSRect(x: f.width - 6, y: f.height/2 - 60, width: 5, height: 120)
+ case "right": gripView.isHidden = false; gripView.frame = NSRect(x: 1, y: f.height/2 - 60, width: 5, height: 120)
+ case "top": gripView.isHidden = false; gripView.frame = NSRect(x: f.width/2 - 60, y: 1, width: 120, height: 4)
+ default: gripView.isHidden = true
+ }
+ }
+
+ private static let positionOrder = ["top", "left", "right", "off"]
+ @objc private func cyclePosition() {
+ let i = (BarController.positionOrder.firstIndex(of: barPosition) ?? 0) + 1
+ setPosition(BarController.positionOrder[i % BarController.positionOrder.count])
+ }
+ @objc private func openFromHamburger() { setPosition("top") }
+ private func setPosition(_ p: String) {
+ barPosition = p
+ UserDefaults.standard.set(p, forKey: "ticketBarPosition")
+ applyPosition()
+ }
+
+ // Persisted per-mode bar size: width for the vertical docks, height for the top bar.
+ private func vBarWidth() -> CGFloat { CGFloat((UserDefaults.standard.object(forKey: "ticketBarVWidth") as? Double) ?? 300) }
+ private func topBarHeight() -> CGFloat { CGFloat((UserDefaults.standard.object(forKey: "ticketBarHeight") as? Double) ?? 38) }
+ @objc private func barBigger(_ s: NSMenuItem) { adjustBarSize(by: 1) }
+ @objc private func barSmaller(_ s: NSMenuItem) { adjustBarSize(by: -1) }
+ @objc private func cyclePositionMenu(_ s: NSMenuItem) { cyclePosition() }
+ // Expand/shrink the WHOLE bar: cross-axis width (left/right) or height (top).
+ private func adjustBarSize(by dir: CGFloat) {
+ if barPosition == "left" || barPosition == "right" {
+ UserDefaults.standard.set(Double(min(700, max(180, vBarWidth() + dir * 40))), forKey: "ticketBarVWidth")
+ } else if barPosition == "top" {
+ UserDefaults.standard.set(Double(min(300, max(28, topBarHeight() + dir * 16))), forKey: "ticketBarHeight")
+ }
+ applyPosition()
+ }
+
+ // Dock the bar top (horizontal), left/right (vertical), or collapse to a ☰ hamburger.
+ private func applyPosition() {
+ guard let screen = panel.screen ?? NSScreen.main ?? NSScreen.screens.first else { return }
+ let v = screen.visibleFrame
+ if barPosition == "off" {
+ if stack.superview != nil {
+ NSLayoutConstraint.deactivate(stackConstraints)
+ stack.removeFromSuperview()
+ }
+ hamburger.isHidden = false
+ gripView?.isHidden = true
+ let w: CGFloat = 46, h: CGFloat = 32
+ panel.setFrame(NSRect(x: v.minX + 8, y: v.maxY - h, width: w, height: h), display: true)
+ panel.orderFrontRegardless()
+ return
+ }
+ if stack.superview == nil {
+ contentContainer.addSubview(stack)
+ NSLayoutConstraint.activate(stackConstraints)
+ }
+ stack.isHidden = false
+ hamburger.isHidden = true
+ let vertical = (barPosition == "left" || barPosition == "right")
+ stack.orientation = vertical ? .vertical : .horizontal
+ stack.alignment = vertical ? .leading : .centerY
+ // Top-pin the tile group in the vertical docks; center it in the Top bar.
+ alignCenterY.isActive = !vertical
+ alignTop.isActive = vertical
+ let vW = self.vBarWidth()
+ // Tile widths: uniform in a vertical bar; restore the horizontal widths on top.
+ for tile in tiles {
+ tile.widthConstraint.constant = vertical ? (vW - 44) : (horizWidths[tile.key] ?? tile.widthConstraint.constant)
+ }
+ switch barPosition {
+ case "left":
+ panel.setFrame(NSRect(x: v.minX, y: v.minY, width: vW, height: v.height), display: true)
+ case "right":
+ panel.setFrame(NSRect(x: v.maxX - vW, y: v.minY, width: vW, height: v.height), display: true)
+ default: // top
+ let barHeight = self.topBarHeight()
+ let requested = CGFloat(UserDefaults.standard.double(forKey: "ticketBarYOffset"))
+ let offset = min(max(0, requested), max(0, v.height - barHeight))
+ panel.setFrame(NSRect(x: v.minX, y: v.maxY - barHeight - offset, width: v.width, height: barHeight), display: true)
+ }
+ posButton.title = ["top": "▔", "left": "▏", "right": "▕"][barPosition] ?? "▔"
+ positionGrip()
+ panel.orderFrontRegardless()
+ }
+
+ @objc private func toggleTheme() {
+ isNight.toggle()
+ UserDefaults.standard.set(isNight, forKey: "ticketBarNight")
+ applyTheme()
+ }
+
+ // Night = dark bar (default), Day = light bar. Toggled by the 🌙/☀️ button.
+ private func applyTheme() {
+ let night = isNight
+ panel.backgroundColor = night ? NSColor(calibratedWhite: 0.055, alpha: 0.97)
+ : NSColor(calibratedWhite: 0.95, alpha: 0.98)
+ let tileBG = (night ? NSColor(calibratedWhite: 0.11, alpha: 0.90)
+ : NSColor(calibratedWhite: 0.86, alpha: 0.95)).cgColor
+ for tile in tiles { tile.layer?.backgroundColor = tileBG }
+ openLabel.textColor = night ? NSColor(calibratedRed: 0.31, green: 0.63, blue: 1.00, alpha: 1)
+ : NSColor(calibratedRed: 0.13, green: 0.38, blue: 0.82, alpha: 1)
+ blockedLabel.textColor = night ? NSColor(calibratedRed: 0.88, green: 0.42, blue: 0.46, alpha: 1)
+ : NSColor(calibratedRed: 0.74, green: 0.18, blue: 0.22, alpha: 1)
+ doingLabel.textColor = night ? NSColor(calibratedRed: 0.43, green: 0.70, blue: 0.95, alpha: 1)
+ : NSColor(calibratedRed: 0.16, green: 0.44, blue: 0.72, alpha: 1)
+ working.textColor = night ? NSColor(calibratedRed: 0.45, green: 0.78, blue: 1.00, alpha: 1)
+ : NSColor(calibratedRed: 0.10, green: 0.45, blue: 0.82, alpha: 1)
+ latest.textColor = night ? NSColor(calibratedWhite: 0.72, alpha: 1)
+ : NSColor(calibratedWhite: 0.30, alpha: 1)
+ cpuLabel.textColor = night ? NSColor(calibratedRed: 0.45, green: 0.90, blue: 0.65, alpha: 1)
+ : NSColor(calibratedRed: 0.08, green: 0.42, blue: 0.24, alpha: 1)
+ openButton?.contentTintColor = night ? .white : NSColor(calibratedWhite: 0.15, alpha: 1)
+ themeButton?.title = night ? "🌙" : "☀️"
+ styleSniperButton(active: sniperButton?.title.contains("ON") == true)
+ panel.invalidateShadow()
+ panel.contentView?.needsDisplay = true
+ }
+
+ private func installDragMonitor() {
+ guard dragMonitor == nil else { return }
+ dragMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown, .leftMouseDragged, .leftMouseUp]) { [weak self] event in
+ guard let self, event.window === self.panel else { return event }
+ switch event.type {
+ case .leftMouseDown:
+ self.didDrag = false
+ self.barSlid = false
+ // Grab the resize edge? (right edge of left dock, left edge of right dock, bottom of top bar)
+ self.resizing = self.inResizeZone(event.locationInWindow)
+ if self.resizing {
+ self.resizeStartMouse = NSEvent.mouseLocation
+ self.resizeStartSize = (self.barPosition == "top") ? self.topBarHeight() : self.vBarWidth()
+ }
+ self.draggingKey = self.tiles.first(where: {
+ $0.bounds.contains($0.convert(event.locationInWindow, from: nil))
+ })?.key
+ self.dragStartX = event.locationInWindow.x
+ self.dragStartMouseY = NSEvent.mouseLocation.y // global screen Y
+ self.dragStartPanelY = self.panel.frame.origin.y
+ case .leftMouseDragged:
+ // Edge-drag RESIZE takes priority over slide/reorder.
+ if self.resizing {
+ self.didDrag = true
+ let m = NSEvent.mouseLocation
+ switch self.barPosition {
+ case "left":
+ UserDefaults.standard.set(Double(min(700, max(180, self.resizeStartSize + (m.x - self.resizeStartMouse.x)))), forKey: "ticketBarVWidth")
+ case "right":
+ UserDefaults.standard.set(Double(min(700, max(180, self.resizeStartSize + (self.resizeStartMouse.x - m.x)))), forKey: "ticketBarVWidth")
+ case "top":
+ UserDefaults.standard.set(Double(min(300, max(28, self.resizeStartSize + (self.resizeStartMouse.y - m.y)))), forKey: "ticketBarHeight")
+ default: break
+ }
+ self.applyPosition()
+ return event
+ }
+ let dy = NSEvent.mouseLocation.y - (self.dragStartMouseY ?? NSEvent.mouseLocation.y)
+ let dx = event.locationInWindow.x - (self.dragStartX ?? event.locationInWindow.x)
+ // Vertical drag → SLIDE the whole bar up/down (Top mode only). Horizontal drag on a tile → reorder.
+ if self.barPosition == "top", self.barSlid || (abs(dy) > 4 && abs(dy) >= abs(dx)) {
+ self.barSlid = true
+ self.didDrag = true
+ self.slideBar(toPanelY: (self.dragStartPanelY ?? self.panel.frame.origin.y) + dy)
+ } else if let key = self.draggingKey, abs(dx) > 3 {
+ self.didDrag = true
+ self.moveTile(key, toward: event.locationInWindow.x)
+ }
+ case .leftMouseUp:
+ if self.barSlid { self.persistBarOffset() }
+ self.resizing = false
+ self.draggingKey = nil
+ self.dragStartX = nil
+ self.dragStartMouseY = nil
+ self.dragStartPanelY = nil
+ // Keep didDrag true through the click that this mouse-up triggers (so the
+ // menu is suppressed after a drag), then clear it for the next gesture.
+ let dragged = self.didDrag
+ DispatchQueue.main.async { if dragged { self.didDrag = false } }
+ default: break
+ }
+ return event
+ }
+ }
+
+ // Move the whole bar to a new vertical position, clamped on-screen.
+ private func slideBar(toPanelY y: CGFloat) {
+ guard let screen = panel.screen ?? NSScreen.main ?? NSScreen.screens.first else { return }
+ let v = screen.visibleFrame
+ let barHeight: CGFloat = 38
+ let clampedY = min(max(v.minY, y), v.maxY - barHeight)
+ panel.setFrame(NSRect(x: v.minX, y: clampedY, width: v.width, height: barHeight), display: true)
+ }
+
+ // Persist the bar's current vertical position as an offset below the top.
+ private func persistBarOffset() {
+ guard let screen = panel.screen ?? NSScreen.main ?? NSScreen.screens.first else { return }
+ let v = screen.visibleFrame
+ let offset = max(0, (v.maxY - 38) - panel.frame.origin.y)
+ UserDefaults.standard.set(Double(offset), forKey: "ticketBarYOffset")
+ }
+
+ private func moveTile(_ key: String, toward windowX: CGFloat) {
+ guard let source = tiles.firstIndex(where: { $0.key == key }) else { return }
+ var target = source
+ for (index, tile) in tiles.enumerated() where tile.key != key {
+ let mid = tile.convert(tile.bounds, to: nil).midX
+ if windowX > mid { target = index }
+ }
+ if windowX < tiles.first?.convert(tiles.first!.bounds, to: nil).midX ?? 0 { target = 0 }
+ guard target != source else { return }
+ let tile = tiles.remove(at: source)
+ target = min(target, tiles.count)
+ tiles.insert(tile, at: target)
+ stack.removeArrangedSubview(tile)
+ tile.removeFromSuperview()
+ stack.insertArrangedSubview(tile, at: target)
+ UserDefaults.standard.set(tiles.map(\.key), forKey: "ticketBarOrder")
+ }
+
+ private func menu(for key: String) -> NSMenu {
+ let menu = NSMenu(title: key)
+ let openItem = NSMenuItem(title: "Open Control Window ↗", action: #selector(openControlFromMenu), keyEquivalent: "")
+ openItem.target = self
+ menu.addItem(openItem)
+ menu.addItem(.separator())
+ // autoenable OFF so the detail rows render in normal readable text instead of
+ // greyed-out/disabled — and each row is clickable (jumps to that ticket).
+ menu.autoenablesItems = false
+ let details: [String]
+ if key == "open" || key == "blocked" || key == "doing" {
+ let list = statusDetails[key] ?? []
+ details = list.isEmpty ? ["No \(key) tickets"] : list
+ } else if key == "working" {
+ details = workingDetails.isEmpty ? ["No tickets currently marked doing"] : workingDetails
+ } else {
+ details = [latestDetail]
+ }
+ for detail in details.prefix(12) {
+ let item = NSMenuItem(title: detail, action: #selector(openDetailTicket(_:)), keyEquivalent: "")
+ item.target = self
+ item.representedObject = detail
+ item.isEnabled = true
+ menu.addItem(item)
+ }
+ menu.addItem(.separator())
+ for (title, action) in [
+ ("Move Left", #selector(moveLeft(_:))), ("Move Right", #selector(moveRight(_:))),
+ ("Move Bar Up", #selector(moveBarUp(_:))), ("Move Bar Down", #selector(moveBarDown(_:))),
+ ("Tile Wider", #selector(wider(_:))), ("Tile Narrower", #selector(narrower(_:))),
+ ("Bar Bigger (expand)", #selector(barBigger(_:))), ("Bar Smaller", #selector(barSmaller(_:))),
+ ("Change Dock ▸ top/left/right/off", #selector(cyclePositionMenu(_:)))
+ ] {
+ let item = NSMenuItem(title: title, action: action, keyEquivalent: "")
+ item.target = self
+ item.representedObject = key
+ menu.addItem(item)
+ }
+ return menu
+ }
+
+ @objc private func openControlFromMenu() { openTicketPopup() }
+
+ // Click a ticket detail row → open the control window filtered to that ticket.
+ @objc private func openDetailTicket(_ sender: NSMenuItem) {
+ let text = sender.representedObject as? String ?? ""
+ guard let range = text.range(of: "TK-\\d+", options: .regularExpression) else {
+ openTicketPopup(); return
+ }
+ let id = String(text[range])
+ openTicketPopup()
+ // Retry a few times so it lands whether the web view was just created or already loaded.
+ for delay in [0.2, 0.6, 1.2] {
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
+ guard let web = self?.ticketPopup?.contentView as? WKWebView else { return }
+ web.evaluateJavaScript("window.setQ ? (setQ('\(id)'), true) : (location.hash='q=\(id)')", completionHandler: nil)
+ }
+ }
+ }
+
+ // Vertical bar placement — "ticketBarYOffset" is pixels BELOW the top of the
+ // visible frame (0 = pinned under the menu bar, the original behavior).
+ @objc private func moveBarUp(_ sender: NSMenuItem) { nudgeBar(by: -40) }
+ @objc private func moveBarDown(_ sender: NSMenuItem) { nudgeBar(by: 40) }
+ private func nudgeBar(by delta: CGFloat) {
+ let current = CGFloat(UserDefaults.standard.double(forKey: "ticketBarYOffset"))
+ UserDefaults.standard.set(Double(current + delta), forKey: "ticketBarYOffset")
+ reposition()
+ }
+
+ @objc private func moveLeft(_ sender: NSMenuItem) { shift(sender.representedObject as? String, by: -1) }
+ @objc private func moveRight(_ sender: NSMenuItem) { shift(sender.representedObject as? String, by: 1) }
+ private func shift(_ key: String?, by delta: Int) {
+ guard let key, let index = tiles.firstIndex(where: { $0.key == key }) else { return }
+ let target = max(0, min(tiles.count - 1, index + delta))
+ guard target != index else { return }
+ let tile = tiles.remove(at: index)
+ tiles.insert(tile, at: target)
+ stack.removeArrangedSubview(tile); tile.removeFromSuperview(); stack.insertArrangedSubview(tile, at: target)
+ UserDefaults.standard.set(tiles.map(\.key), forKey: "ticketBarOrder")
+ }
+ @objc private func wider(_ sender: NSMenuItem) { resize(sender.representedObject as? String, by: 80) }
+ @objc private func narrower(_ sender: NSMenuItem) { resize(sender.representedObject as? String, by: -80) }
+ private func resize(_ key: String?, by delta: CGFloat) {
+ guard let tile = tiles.first(where: { $0.key == key }) else { return }
+ tile.widthConstraint.constant = max(180, min(900, tile.widthConstraint.constant + delta))
+ UserDefaults.standard.set(Dictionary(uniqueKeysWithValues: tiles.map { ($0.key, $0.widthConstraint.constant) }), forKey: "ticketBarWidths")
+ }
+
+ private func divider() -> NSView {
+ let view = NSView(frame: NSRect(x: 0, y: 0, width: 1, height: 18))
+ view.wantsLayer = true
+ view.layer?.backgroundColor = NSColor(calibratedWhite: 0.28, alpha: 1).cgColor
+ view.widthAnchor.constraint(equalToConstant: 1).isActive = true
+ return view
+ }
+
+ @objc private func reposition() { applyPosition() }
+
+ @objc private func openViewer() { NSWorkspace.shared.open(viewerURL) }
+
+ // ── live CPU + RAM Sniper control ─────────────────────────────────────────
+ private func runCommand(_ executable: String, _ args: [String]) -> String {
+ let task = Process(); task.executableURL = URL(fileURLWithPath: executable); task.arguments = args
+ let pipe = Pipe(); task.standardOutput = pipe; task.standardError = Pipe()
+ do { try task.run(); task.waitUntilExit() } catch { return "" }
+ return String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? ""
+ }
+
+ private func refreshSystem() {
+ DispatchQueue.global(qos: .utility).async { [weak self] in
+ guard let self else { return }
+ let top = self.runCommand("/usr/bin/top", ["-l", "1", "-n", "0", "-s", "0"])
+ let cpuUsed: Double
+ let cpuText: String
+ if let line = top.split(separator: "\n").first(where: { $0.contains("CPU usage") }),
+ let user = line.split(separator: ",").first(where: { $0.contains("user") }),
+ let sys = line.split(separator: ",").first(where: { $0.contains("sys") }) {
+ let number: (Substring) -> Double = { part in Double(part.split(separator: "%").first?.split(separator: " ").last ?? "0") ?? 0 }
+ cpuUsed = number(user) + number(sys)
+ cpuText = String(format: "CPU · %.0f%%", cpuUsed)
+ } else { cpuUsed = 0; cpuText = "CPU · —" }
+ var sniper = !self.runCommand("/usr/bin/pgrep", ["-f", "/Users/macstudio3/bin/ram-sniper.sh"]).trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+ if !sniper && cpuUsed >= 75 {
+ self.startRamSniper()
+ DispatchQueue.main.async { self.flashSniperButton() }
+ sniper = true
+ }
+ DispatchQueue.main.async {
+ self.cpuLabel.stringValue = cpuText
+ self.sniperButton.title = sniper ? "RAM Sniper · ON" : "RAM Sniper · OFF"
+ self.styleSniperButton(active: sniper)
+ }
+ }
+ }
+
+ private func styleSniperButton(active: Bool) {
+ guard let button = sniperButton else { return }
+ let green = NSColor(calibratedRed: 0.38, green: 0.92, blue: 0.58, alpha: 0.30)
+ let tint = active ? NSColor(calibratedRed: 0.62, green: 1.0, blue: 0.75, alpha: 1) : NSColor(calibratedWhite: 0.58, alpha: 0.72)
+ button.layer?.backgroundColor = active ? green.cgColor : NSColor.clear.cgColor
+ button.layer?.borderColor = active ? tint.cgColor : NSColor.clear.cgColor
+ button.contentTintColor = tint
+ }
+
+ @objc private func toggleRamSniper() {
+ let running = !runCommand("/usr/bin/pgrep", ["-f", "/Users/macstudio3/bin/ram-sniper.sh"]).trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
+ if running {
+ _ = runCommand("/usr/bin/pkill", ["-TERM", "-f", "/Users/macstudio3/bin/ram-sniper.sh"])
+ } else {
+ startRamSniper()
+ }
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in self?.refreshSystem() }
+ }
+
+ private func startRamSniper() {
+ let task = Process(); task.executableURL = URL(fileURLWithPath: "/bin/zsh")
+ task.arguments = ["-lc", "nohup /Users/macstudio3/bin/ram-sniper.sh >/tmp/ram-sniper.stdout 2>&1 &"]
+ try? task.run()
+ }
+
+ private func flashSniperButton() {
+ guard let button = sniperButton else { return }
+ button.layer?.backgroundColor = NSColor(calibratedRed: 0.20, green: 1.0, blue: 0.35, alpha: 0.95).cgColor
+ button.layer?.borderColor = NSColor.white.cgColor
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) { [weak self] in
+ self?.styleSniperButton(active: true)
+ }
+ }
+
+ // ── ◉ Terminal-dots dropdown ────────────────────────────────────────────────
+ // SELECTION MODEL: click-to-jump (the robust fallback). AppKit does not support
+ // keeping an NSMenu open across checkbox toggles without private API / custom
+ // item views, so — per the task's fallback — clicking a terminal row focuses
+ // that one window immediately, and a "▶ Focus all that want me" item focuses
+ // every orange+purple+yellow terminal at once. The menu is rebuilt FRESH on
+ // every open by re-running the /allcolordots scanner.
+ private struct DotEntry { let tty: String; let color: String; let label: String }
+ private static let dotOrder = ["orange", "purple", "yellow", "green", "pink", "none"]
+ private static let dotEmoji: [String: String] = [
+ "orange": "🟠", "purple": "🟣", "yellow": "🟡", "green": "🟢", "pink": "🩷", "none": "⚪️"
+ ]
+ private static let wantColors: Set<String> = ["orange", "purple", "yellow"]
+
+ // Shell out to the scanner and parse its JSON array (already priority-sorted).
+ private func scanDots() -> [DotEntry] {
+ let task = Process()
+ task.executableURL = URL(fileURLWithPath: "/bin/bash")
+ task.arguments = ["-lc", "bash /Users/macstudio3/.claude/skills/allcolordots/allcolordots.sh --json"]
+ let out = Pipe()
+ task.standardOutput = out
+ task.standardError = Pipe()
+ do { try task.run() } catch { return [] }
+ let data = out.fileHandleForReading.readDataToEndOfFile()
+ task.waitUntilExit()
+ guard let arr = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { return [] }
+ return arr.compactMap { row in
+ guard let tty = row["tty"] as? String else { return nil }
+ return DotEntry(tty: tty, color: row["color"] as? String ?? "none", label: row["label"] as? String ?? tty)
+ }
+ }
+
+ @objc private func showDotsMenu() {
+ let menu = buildDotsMenu()
+ NSApp.activate(ignoringOtherApps: true)
+ guard let dotsButton else { return }
+ menu.popUp(positioning: nil, at: NSPoint(x: 0, y: dotsButton.bounds.height + 2), in: dotsButton)
+ }
+
+ private func buildDotsMenu() -> NSMenu {
+ let menu = NSMenu(title: "dots")
+ menu.autoenablesItems = false
+ let entries = scanDots()
+ if entries.isEmpty {
+ let none = NSMenuItem(title: "No dots (scanner returned nothing)", action: nil, keyEquivalent: "")
+ none.isEnabled = false
+ menu.addItem(none)
+ menu.addItem(.separator())
+ let refresh = NSMenuItem(title: "🔄 Refresh", action: #selector(showDotsMenu), keyEquivalent: "")
+ refresh.target = self
+ menu.addItem(refresh)
+ return menu
+ }
+ let wantCount = entries.filter { BarController.wantColors.contains($0.color) }.count
+ let header = NSMenuItem(title: "Terminals — \(wantCount) want you", action: nil, keyEquivalent: "")
+ header.isEnabled = false
+ menu.addItem(header)
+ if wantCount > 0 {
+ let focusAll = NSMenuItem(title: "▶ Focus all that want me (\(wantCount))", action: #selector(focusAllWanting), keyEquivalent: "")
+ focusAll.target = self
+ menu.addItem(focusAll)
+ }
+ // Path-A "gather all my yellows in one place" — yellow-only quick jump
+ // (needs-direction tabs), distinct from the 3-color "want me" action above.
+ let yellowCount = entries.filter { $0.color == "yellow" }.count
+ if yellowCount > 0 {
+ let focusYellow = NSMenuItem(title: "🟡 Gather my yellows (\(yellowCount))", action: #selector(focusAllYellow), keyEquivalent: "")
+ focusYellow.target = self
+ menu.addItem(focusYellow)
+ }
+ menu.addItem(.separator())
+ for color in BarController.dotOrder {
+ let group = entries.filter { $0.color == color }
+ guard !group.isEmpty else { continue }
+ let emoji = BarController.dotEmoji[color] ?? "•"
+ let section = NSMenuItem(title: "\(emoji) \(color.capitalized) (\(group.count))", action: nil, keyEquivalent: "")
+ section.isEnabled = false
+ menu.addItem(section)
+ for entry in group {
+ let item = NSMenuItem(title: entry.label, action: #selector(focusDotTerminal(_:)), keyEquivalent: "")
+ item.target = self
+ item.representedObject = entry.tty
+ item.isEnabled = true
+ menu.addItem(item)
+ }
+ }
+ menu.addItem(.separator())
+ let refresh = NSMenuItem(title: "🔄 Refresh", action: #selector(showDotsMenu), keyEquivalent: "")
+ refresh.target = self
+ menu.addItem(refresh)
+ let board = NSMenuItem(title: "Open board ↗", action: #selector(openViewer), keyEquivalent: "")
+ board.target = self
+ menu.addItem(board)
+ return menu
+ }
+
+ @objc private func focusDotTerminal(_ sender: NSMenuItem) {
+ guard let tty = sender.representedObject as? String else { return }
+ focusTerminal(tty: tty)
+ }
+
+ @objc private func focusAllWanting() {
+ for entry in scanDots() where BarController.wantColors.contains(entry.color) {
+ focusTerminal(tty: entry.tty)
+ }
+ }
+
+ // Path-A navigator: bring every 🟡 needs-direction terminal forward in one click.
+ @objc private func focusAllYellow() {
+ for entry in scanDots() where entry.color == "yellow" {
+ focusTerminal(tty: entry.tty)
+ }
+ }
+
+ // Bring the iTerm2 window/tab/session whose tty matches to the front. The
+ // scanner reports "ttys011"; iTerm reports "/dev/ttys011". No-ops silently if
+ // iTerm2 isn't the terminal (that's fine).
+ private func focusTerminal(tty: String) {
+ let dev = tty.hasPrefix("/dev/") ? tty : "/dev/\(tty)"
+ let script = """
+ tell application "iTerm2"
+ repeat with w in windows
+ repeat with t in tabs of w
+ repeat with s in sessions of t
+ if tty of s is "\(dev)" then
+ select w
+ select t
+ select s
+ activate
+ return
+ end if
+ end repeat
+ end repeat
+ end repeat
+ end tell
+ tell application "iTerm2" to activate
+ """
+ let task = Process()
+ task.executableURL = URL(fileURLWithPath: "/usr/bin/osascript")
+ task.arguments = ["-e", script]
+ try? task.run()
+ }
+
+ private func openTicketPopup() {
+ NSLog("TicketBar opening control window")
+ if let ticketPopup {
+ centerOnBarScreen(ticketPopup)
+ ticketPopup.makeKeyAndOrderFront(nil)
+ ticketPopup.orderFrontRegardless()
+ NSApp.activate(ignoringOtherApps: true)
+ return
+ }
+ let size = NSSize(width: 1480, height: 860)
+ let popup = NSPanel(contentRect: NSRect(origin: .zero, size: size), styleMask: [.titled, .closable, .resizable, .miniaturizable, .nonactivatingPanel], backing: .buffered, defer: false)
+ popup.title = "Fleet Ticket Control · All / Open / Blocked / Doing"
+ popup.level = .statusBar
+ popup.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
+ popup.minSize = NSSize(width: 900, height: 560)
+ popup.isReleasedWhenClosed = false
+ popup.hidesOnDeactivate = false
+ popup.isFloatingPanel = true
+ popup.becomesKeyOnlyIfNeeded = false
+ popup.delegate = self
+ let web = WKWebView(frame: popup.contentView?.bounds ?? .zero)
+ web.navigationDelegate = self
+ web.autoresizingMask = [.width, .height]
+ popup.contentView = web
+ // Let WebKit's HTTP Basic challenge establish a session-wide credential;
+ // a one-off Authorization header would authenticate the HTML navigation but
+ // leave the board's same-origin fetch() calls unauthenticated and blank.
+ web.load(URLRequest(url: viewerURL))
+ centerOnBarScreen(popup)
+ ticketPopup = popup
+ panel.addChildWindow(popup, ordered: .above)
+ popup.makeKeyAndOrderFront(nil)
+ popup.orderFrontRegardless()
+ NSApp.activate(ignoringOtherApps: true)
+ }
+
+ func windowWillClose(_ notification: Notification) {
+ guard let closing = notification.object as? NSWindow, closing === ticketPopup else { return }
+ panel.removeChildWindow(closing)
+ ticketPopup = nil
+ }
+
+ private func centerOnBarScreen(_ window: NSWindow) {
+ let screen = panel.screen ?? NSScreen.main ?? NSScreen.screens.first
+ guard let visible = screen?.visibleFrame else { window.center(); return }
+ let width = min(window.frame.width, visible.width - 40)
+ let height = min(window.frame.height, visible.height - 40)
+ window.setFrame(NSRect(
+ x: visible.midX - width / 2,
+ y: visible.midY - height / 2,
+ width: width,
+ height: height
+ ), display: true)
+ NSLog("TicketBar control frame %@ on bar screen %@", NSStringFromRect(window.frame), NSStringFromRect(visible))
+ }
+
+ func webView(_ webView: WKWebView, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
+ if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic {
+ completionHandler(.useCredential, URLCredential(user: "admin", password: "DW2024!", persistence: .forSession))
+ } else { completionHandler(.performDefaultHandling, nil) }
+ }
+
+ // ── priority + created-time enrichment ──────────────────────────────────────
+ private let isoParser: ISO8601DateFormatter = {
+ let f = ISO8601DateFormatter(); f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]; return f
+ }()
+ private let isoParserPlain = ISO8601DateFormatter()
+ private let stampFmt: DateFormatter = {
+ let f = DateFormatter(); f.dateFormat = "MMM d, h:mm a"; return f
+ }()
+
+ private func stamp(_ iso: String) -> String {
+ guard !iso.isEmpty else { return "" }
+ guard let d = isoParser.date(from: iso) ?? isoParserPlain.date(from: iso) else { return "" }
+ return "🕓 " + stampFmt.string(from: d)
+ }
+
+ // Best-effort pull of server-computed rank/priority from the viewer API.
+ // Empty (no priority shown) if the viewer is unreachable — counts still work.
+ private func fetchPriorities() -> [String: (rank: Int?, priority: Double)] {
+ guard let url = URL(string: "http://127.0.0.1:9794/api/tickets") else { return [:] }
+ var req = URLRequest(url: url, timeoutInterval: 2.5)
+ req.setValue("Basic " + Data("admin:DW2024!".utf8).base64EncodedString(), forHTTPHeaderField: "Authorization")
+ var result: [String: (Int?, Double)] = [:]
+ let sem = DispatchSemaphore(value: 0)
+ URLSession.shared.dataTask(with: req) { data, _, _ in
+ defer { sem.signal() }
+ guard let data, let arr = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { return }
+ for t in arr {
+ guard let id = t["id"] as? String else { continue }
+ result[id] = (t["rank"] as? Int, (t["priority"] as? NSNumber)?.doubleValue ?? 0)
+ }
+ }.resume()
+ _ = sem.wait(timeout: .now() + 3)
+ return result
+ }
+
+ private func refresh() {
+ refreshSystem()
+ DispatchQueue.global(qos: .utility).async { [weak self] in
+ guard let self else { return }
+ do {
+ let text = try String(contentsOf: ledgerURL, encoding: .utf8)
+ var states: [String: TicketState] = [:]
+ var newest: [String: Any]?
+ for line in text.split(separator: "\n") {
+ guard let data = line.data(using: .utf8),
+ let event = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
+ let id = event["id"] as? String, id.hasPrefix("TK-") else { continue }
+ let type = event["type"] as? String ?? ""
+ let timestamp = event["ts"] as? String ?? ""
+ if type == "create" {
+ states[id] = TicketState(id: id, title: event["title"] as? String ?? id, status: "open", updated: timestamp, created: timestamp)
+ } else if var ticket = states[id] {
+ if type == "status", let status = event["status"] as? String { ticket.status = status }
+ ticket.updated = timestamp
+ states[id] = ticket
+ }
+ if type == "action" || type == "comment" || type == "status" { newest = event }
+ }
+ let active = states.values.filter { $0.status != "done" && $0.status != "stopped" }
+ let prios = self.fetchPriorities() // best-effort; empty if viewer down
+ // Highest priority first (lowest rank #); un-ranked fall to the bottom, newest first.
+ let byPrio: (TicketState, TicketState) -> Bool = { a, b in
+ let ra = prios[a.id]?.rank ?? Int.max, rb = prios[b.id]?.rank ?? Int.max
+ if ra != rb { return ra < rb }
+ return a.updated > b.updated
+ }
+ let openList = active.filter { $0.status == "open" }.sorted(by: byPrio)
+ let blockedList = active.filter { $0.status == "blocked" }.sorted(by: byPrio)
+ let doing = active.filter { $0.status == "doing" }.sorted(by: byPrio)
+ let workText = doing.prefix(3).map { "\(self.shortID($0.id)) \(self.fiveWords($0.title))" }.joined(separator: " • ")
+ let latestText = self.describe(newest)
+ // Row: TK-xxxx #rank p<priority> · title 🕓 created date/time
+ let row: (TicketState) -> String = { t in
+ var prefix = self.shortID(t.id)
+ if let p = prios[t.id] {
+ if let r = p.rank { prefix += " #\(r)" }
+ if p.priority > 0 { prefix += String(format: " p%.1f", p.priority) }
+ }
+ let when = self.stamp(t.created)
+ return when.isEmpty ? "\(prefix) · \(t.title)" : "\(prefix) · \(t.title) \(when)"
+ }
+ DispatchQueue.main.async {
+ self.openLabel.stringValue = "OPEN \(openList.count)"
+ self.blockedLabel.stringValue = "BLOCKED \(blockedList.count)"
+ self.doingLabel.stringValue = "DOING \(doing.count)"
+ self.statusDetails = ["open": openList.map(row), "blocked": blockedList.map(row), "doing": doing.map(row)]
+ self.working.stringValue = "Working · \(workText.isEmpty ? "none" : workText)"
+ self.latest.stringValue = "Latest · \(latestText)"
+ self.workingDetails = doing.map(row)
+ self.latestDetail = latestText
+ self.panel.orderFrontRegardless()
+ if let popup = self.ticketPopup, !popup.isMiniaturized { popup.orderFrontRegardless() }
+ }
+ } catch {
+ DispatchQueue.main.async { self.latest.stringValue = "Latest · ledger unavailable" }
+ }
+ }
+ }
+
+ private func shortID(_ id: String) -> String {
+ let parts = id.split(separator: "-")
+ return parts.count > 1 ? "TK-\(parts[1])" : id
+ }
+
+ private func fiveWords(_ text: String) -> String {
+ text.split(whereSeparator: { $0.isWhitespace }).prefix(5).joined(separator: " ")
+ }
+
+ private func describe(_ event: [String: Any]?) -> String {
+ guard let event else { return "no activity" }
+ let id = shortID(event["id"] as? String ?? "")
+ let type = event["type"] as? String ?? "event"
+ let detail = (event["text"] as? String) ?? (event["status"] as? String) ?? "updated"
+ return "\(id) \(type) · \(fiveWords(detail))"
+ }
+}
+
+let app = NSApplication.shared
+private let delegate = BarController()
+app.delegate = delegate
+app.run()
diff --git a/run-ticket.sh.bak-20260911-142309 b/run-ticket.sh.bak-20260911-142309
new file mode 100755
index 00000000..6a401f04
--- /dev/null
+++ b/run-ticket.sh.bak-20260911-142309
@@ -0,0 +1,82 @@
+#!/usr/bin/env bash
+# run-ticket.sh — open ONE new iTerm2 window with `claude` pre-loaded to drive a
+# ticket to done (the "Run Now" action from the board; mirrors ~/.zsh/task.sh but
+# hardened: it takes only a VALIDATED ticket id + an optional cwd, so no agent-
+# authored ticket title ever reaches a shell). Steve, TK-10527 (2026-08-13):
+# "run ticket like the approvals opens a new iterm2 terminal with claude loaded first."
+#
+# Usage: run-ticket.sh <TK-id> [start-dir] [claude-sonnet|claude-opus|claude-haiku|claude-opus-5|claude-sonnet-5|claude-fable|codex|codex-gpt6|codex-gpt52|local-qwen-27b|local-qwen-14b]
+set -euo pipefail
+
+ID="${1:?usage: run-ticket.sh <TK-id> [start-dir]}"
+# Hard gate: id must be TK-<digits> optionally + a lowercase slug. Reject anything else.
+if [[ ! "$ID" =~ ^TK-[0-9]+(-[a-z0-9-]+)?$ ]]; then
+ echo "run-ticket: refusing unsafe id '$ID'" >&2; exit 2
+fi
+IDNUM="${ID#TK-}"; IDNUM="${IDNUM%%-*}"
+
+CWD="${2:-$HOME}"
+# only allow an existing dir under $HOME; else fall back home (never an arbitrary path)
+case "$CWD" in
+ "$HOME"|"$HOME"/*) [[ -d "$CWD" ]] || CWD="$HOME" ;;
+ *) CWD="$HOME" ;;
+esac
+
+PROFILE="${3:-claude-sonnet}"
+case "$PROFILE" in
+ claude-sonnet) RUNNER=(claude --model sonnet); AGENT_PREFIX=claude-run ;;
+ claude-opus) RUNNER=(claude --model opus); AGENT_PREFIX=claude-run ;;
+ claude-haiku) RUNNER=(claude --model haiku); AGENT_PREFIX=claude-run ;;
+ claude-opus-5) RUNNER=(claude --model claude-opus-5); AGENT_PREFIX=claude-run ;;
+ claude-sonnet-5) RUNNER=(claude --model claude-sonnet-5); AGENT_PREFIX=claude-run ;;
+ claude-fable) RUNNER=(claude --model claude-fable-5-1); AGENT_PREFIX=claude-run ;;
+ codex) RUNNER=(/Users/macstudio3/.local/bin/codex); AGENT_PREFIX=codex-run ;;
+ codex-gpt6) RUNNER=(/Users/macstudio3/.local/bin/codex --model gpt-6-astra); AGENT_PREFIX=codex-run ;;
+ codex-gpt52) RUNNER=(/Users/macstudio3/.local/bin/codex --model gpt-5.2); AGENT_PREFIX=codex-run ;;
+ local-qwen-27b) RUNNER=(/Users/macstudio3/.local/bin/codex --oss --local-provider ollama --model qwen3.8-27b-heretic:latest); AGENT_PREFIX=local-qwen-27b-run ;;
+ local-qwen-14b) RUNNER=(/Users/macstudio3/.local/bin/codex --oss --local-provider ollama --model qwen3:14b); AGENT_PREFIX=local-qwen-14b-run ;;
+ *) echo "run-ticket: refusing unknown profile '$PROFILE'" >&2; exit 2 ;;
+esac
+
+# The prompt references ONLY the safe id — the session pulls full context itself
+# via `tk show`. Encodes Steve's standing operating loop (tickets-first, copious
+# notes, gated actions draft to pending-approval).
+read -r -d '' PROMPT <<PROMPT_EOF || true
+export TK_AGENT=${AGENT_PREFIX}-${IDNUM}. You are driving ticket ${ID} to completion. First run: tk inbox (act on any DMs), then tk show ${ID} for full context. You now OWN this ticket: tk take ${ID}. Work it to done per AGENTS.md/CLAUDE.md's operating loop — tk log EVERY action, tk comment reasoning, dm peers with tk dm if you need help. Any customer-facing / destructive / spend / DNS / publish / send-to-list / remote-push / canonical dw_unified or Shopify write is GATED: draft it to ~/.claude/yolo-queue/pending-approval/ and STOP — never auto-fire. When the work is genuinely complete AND verified: tk done ${ID}.
+PROMPT_EOF
+
+# Build the inner shell payload, then escape for an AppleScript double-quoted literal
+# (same technique as task.sh): backslash first, then double-quote.
+# Title the window with the SHORT ticket number so iTerm's profile appends the running
+# job -> "TK-<n> (node)" = ticket + job ("both", per Steve TK-10573). A backgrounded
+# re-assert fires ~5s in (after zsh's shell-integration precmd hook has run, which would
+# otherwise overwrite the launch-time name) and again on a short loop for the first minute.
+retitle="( for i in 1 2 3 4; do sleep 5; \"$HOME/bin/tk-iterm-title\" TK-${IDNUM}; done ) >/dev/null 2>&1 &"
+printf -v runner_cmd '%q ' "${RUNNER[@]}"
+payload="cd $(printf %q "$CWD") && ${retitle} ${runner_cmd}$(printf %q "$PROMPT")"
+osa_title="TK-${IDNUM}"
+esc() { local s="$1"; s="${s//\\/\\\\}"; s="${s//\"/\\\"}"; printf '%s' "$s"; }
+osa_payload="$(esc "$payload")"
+osa_title="$(esc "$osa_title")"
+
+if [[ "${RUN_TICKET_DRY_RUN:-0}" == "1" ]]; then
+ printf 'profile=%s\nagent=%s-%s\npayload=%s\n' "$PROFILE" "$AGENT_PREFIX" "$IDNUM" "$payload"
+ exit 0
+fi
+
+/usr/bin/osascript <<OSA
+tell application "iTerm"
+ set newWin to (create window with default profile)
+ set newPane to current session of newWin
+ tell newPane
+ set name to "$osa_title"
+ write text "$osa_payload"
+ end tell
+ activate
+ select newWin
+end tell
+OSA
+# Belt-and-suspenders: LaunchServices foregrounding works from a pm2 background daemon
+# where a bare Apple-event `activate` may not. Harmless if iTerm is already front.
+/usr/bin/open -a iTerm 2>/dev/null || /usr/bin/open -a iTerm2 2>/dev/null || true
+echo "run-ticket: launched iTerm2 window for $ID (cwd $CWD)"
← 516f09bb run-ticket: add local-qwen-14b-mac1 runner to offload qwen t
·
back to Ticket System
·
ticket bar: show swap / qwen-27b sessions / ollama reachabil 73d9bb51 →