[object Object]

← back to Ticket System

desktop-bar: fix PASTE beacon showing every dot twice + double 'PASTE:' prefix

703dc9948ddc2e2d847ec9283ae1a958c733505d · 2026-09-16 16:27:23 -0700 · Steve Abrams

Steve hit this on screen: 3 real pastes rendered as 6 lines, two of them
identical pairs, one reading the absurd 'PASTE: PASTE waiting', and real
entries pushed past the 5-line cap into '...and 1 more'.

Two independent bugs, both in the beacon path:

1. scanOrangeDots() walks BOTH ~/.claude/tab-dots and ~/.codex/tab-dots, and
   the dot writer writes the same tty into BOTH (measured: 67/67 pairs
   byte-identical, separate inodes, zero divergence). Nothing keyed by tty, so
   every dot was appended twice -- deterministic, not a race. Now deduped by
   tty, first-wins over a fixed dir order.

2. The line was built as "PASTE: \(label)" while the label already ends with
   the engine's 'PASTE waiting' (terminal_status.py maps orange -> that
   string), printing PASTE twice. The emoji already says which colour it is.

Ships with a negative test (TicketBar.beacon-test.swift) that replays the
exact screenshot state and asserts 3 lines not 6, no 'PASTE:' prefix, and --
the case dedupe could plausibly break -- that 7 DISTINCT ttys still render
5 + 'and 2 more' rather than being swallowed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Files touched

Diff

commit 703dc9948ddc2e2d847ec9283ae1a958c733505d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Wed Sep 16 16:27:23 2026 -0700

    desktop-bar: fix PASTE beacon showing every dot twice + double 'PASTE:' prefix
    
    Steve hit this on screen: 3 real pastes rendered as 6 lines, two of them
    identical pairs, one reading the absurd 'PASTE: PASTE waiting', and real
    entries pushed past the 5-line cap into '...and 1 more'.
    
    Two independent bugs, both in the beacon path:
    
    1. scanOrangeDots() walks BOTH ~/.claude/tab-dots and ~/.codex/tab-dots, and
       the dot writer writes the same tty into BOTH (measured: 67/67 pairs
       byte-identical, separate inodes, zero divergence). Nothing keyed by tty, so
       every dot was appended twice -- deterministic, not a race. Now deduped by
       tty, first-wins over a fixed dir order.
    
    2. The line was built as "PASTE: \(label)" while the label already ends with
       the engine's 'PASTE waiting' (terminal_status.py maps orange -> that
       string), printing PASTE twice. The emoji already says which colour it is.
    
    Ships with a negative test (TicketBar.beacon-test.swift) that replays the
    exact screenshot state and asserts 3 lines not 6, no 'PASTE:' prefix, and --
    the case dedupe could plausibly break -- that 7 DISTINCT ttys still render
    5 + 'and 2 more' rather than being swallowed.
    
    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---
 desktop-bar/TicketBar.beacon-test.swift | 38 +++++++++++++++++++++++++++++++++
 desktop-bar/TicketBar.swift             | 17 +++++++++++++--
 2 files changed, 53 insertions(+), 2 deletions(-)

diff --git a/desktop-bar/TicketBar.beacon-test.swift b/desktop-bar/TicketBar.beacon-test.swift
new file mode 100644
index 00000000..f8d1e24a
--- /dev/null
+++ b/desktop-bar/TicketBar.beacon-test.swift
@@ -0,0 +1,38 @@
+import Foundation
+struct OrangeDot { let tty: String; let label: String }
+// --- verbatim copy of the FIXED logic under test ---
+func render(_ found: [OrangeDot]) -> [String] {
+  var seen = Set<String>()
+  let unique = found.filter { seen.insert($0.tty).inserted }
+  let dots = unique.sorted { $0.tty < $1.tty }
+  let cap = 5
+  var lines = dots.prefix(cap).map { "🟠 \($0.label)  [\($0.tty)]" }
+  if dots.count > cap { lines.append("…and \(dots.count - cap) more") }
+  return lines
+}
+// --- the EXACT state from Steve's screenshot: 3 real pastes, each mirrored into both dirs ---
+let screenshot = [
+  OrangeDot(tty:"ttys016", label:"TK-11715 · PASTE waiting"),   // .claude
+  OrangeDot(tty:"ttys016", label:"TK-11715 · PASTE waiting"),   // .codex  (the duplicate)
+  OrangeDot(tty:"ttys030", label:"TK-11812 · PASTE waiting"),
+  OrangeDot(tty:"ttys030", label:"TK-11812 · PASTE waiting"),
+  OrangeDot(tty:"ttys034", label:"PASTE waiting"),
+  OrangeDot(tty:"ttys034", label:"PASTE waiting"),
+]
+let out = render(screenshot)
+print("--- rendered ---"); out.forEach { print($0) }
+var fail = 0
+func check(_ name: String, _ cond: Bool) { print((cond ? "PASS  " : "FAIL  ") + name); if !cond { fail += 1 } }
+check("3 lines, not 6", out.count == 3)
+check("no '…and N more' (real entries no longer pushed past cap)", !out.contains { $0.contains("more") })
+check("ttys016 appears exactly once", out.filter { $0.contains("ttys016") }.count == 1)
+check("no double PASTE ('PASTE: PASTE')", !out.contains { $0.contains("PASTE: PASTE") })
+check("no 'PASTE:' prefix at all", !out.contains { $0.contains("PASTE:") })
+check("ticket id still shown", out.contains { $0.contains("TK-11715") })
+// NEGATIVE TEST: the detector must still SHOW distinct ttys — dedupe must not swallow real dots.
+let distinct = (1...7).map { OrangeDot(tty:"ttys10\($0)", label:"TK-\($0) · PASTE waiting") }
+let dout = render(distinct)
+check("NEGATIVE: 7 distinct ttys still render 5 + 'and 2 more'", dout.count == 6 && dout.last!.contains("2 more"))
+check("NEGATIVE: an empty set renders nothing", render([]).isEmpty)
+print(fail == 0 ? "\nALL PASS" : "\n\(fail) FAILED")
+exit(fail == 0 ? 0 : 1)
diff --git a/desktop-bar/TicketBar.swift b/desktop-bar/TicketBar.swift
index adc05613..52e1dd79 100644
--- a/desktop-bar/TicketBar.swift
+++ b/desktop-bar/TicketBar.swift
@@ -619,7 +619,16 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
         found.append(OrangeDot(tty: tty, label: label.isEmpty ? tty : label))
       }
     }
-    return found.sorted { $0.tty < $1.tty }
+    // DEDUPE BY TTY (TK-11864). `dirs` holds BOTH ~/.claude/tab-dots and ~/.codex/tab-dots,
+    // and the dot writer writes the SAME tty into BOTH (measured: 67/67 pairs byte-identical,
+    // separate inodes, zero divergence). So every orange dot was appended twice and the beacon
+    // rendered each paste on two lines -- 3 real pastes shown as 6, which also pushed real
+    // entries past the 5-line cap into "...and N more". One tty is ONE paste, so key by tty and
+    // keep the first hit; `dirs` order is fixed ([.claude, .codex]) so first-wins is
+    // deterministic, and on the (currently non-existent) divergent case .claude wins.
+    var seen = Set<String>()
+    let unique = found.filter { seen.insert($0.tty).inserted }
+    return unique.sorted { $0.tty < $1.tty }
   }
 
   private func buildBeacon() {
@@ -666,7 +675,11 @@ private final class BarController: NSObject, NSApplicationDelegate, NSWindowDele
       return
     }
     let cap = 5
-    var lines = dots.prefix(cap).map { "🟠 PASTE: \($0.label)  [\($0.tty)]" }
+    // The dot label already ENDS with the engine's "PASTE waiting" string (terminal_status.py
+    // maps orange -> "PASTE waiting"), so prefixing "PASTE: " printed it twice:
+    // "PASTE: TK-11715 - PASTE waiting", and for an unlabelled dot the absurd "PASTE: PASTE
+    // waiting". The emoji already says which colour this is; the label carries the ticket.
+    var lines = dots.prefix(cap).map { "🟠 \($0.label)  [\($0.tty)]" }
     if dots.count > cap { lines.append("…and \(dots.count - cap) more") }
     beaconLabel.stringValue = lines.joined(separator: "\n")
     positionBeacon(lineCount: lines.count)

← e53b3918 Record ordered monitoring cycle with peer ownership changes  ·  back to Ticket System  ·  Record bounded ordered ticket monitoring cycle yf2320 65af0230 →