From 5d39e5d2cd2bee8b298739a8958ab1a4756d53ee Mon Sep 17 00:00:00 2001 From: scgopi Date: Tue, 8 Sep 2026 00:38:20 -0700 Subject: [PATCH 1/3] Start mailroom watch glyph branch Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01M8s9Ranjzra1VAKrP27gN7 From 290b00800a502a9528e6c8a8b348f6616bed250e Mon Sep 17 00:00:00 2001 From: scgopi Date: Tue, 8 Sep 2026 00:41:02 -0700 Subject: [PATCH 2/3] Mark a loop watching the Mailroom with an envelope glyph A quiet outline envelope on the sidebar row and the canvas card's meta row whenever the node carries a mailroomWatch and is not resolved, with a tooltip naming the topic when the watch has one. No count, no unread dot. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01M8s9Ranjzra1VAKrP27gN7 --- .../Sources/Features/App/AppSidebarView.swift | 6 +++ .../Features/Canvas/LoopCardView.swift | 6 +++ .../Canvas/MailroomWatchPresentation.swift | 24 +++++++++ .../MailroomWatchPresentationTests.swift | 49 +++++++++++++++++++ 4 files changed, 85 insertions(+) create mode 100644 graphcode/Sources/Features/Canvas/MailroomWatchPresentation.swift create mode 100644 graphcode/Tests/MailroomWatchPresentationTests.swift diff --git a/graphcode/Sources/Features/App/AppSidebarView.swift b/graphcode/Sources/Features/App/AppSidebarView.swift index b2739ae3..1f4c584e 100644 --- a/graphcode/Sources/Features/App/AppSidebarView.swift +++ b/graphcode/Sources/Features/App/AppSidebarView.swift @@ -282,6 +282,12 @@ struct AppSidebarView: View { .frame(width: 3, height: 14) Text(node.title).font(.system(size: 12.5)).lineLimit(1) Spacer(minLength: 4) + if let watching = MailroomWatchPresentation.tooltip(for: node) { + Image(systemName: MailroomWatchPresentation.symbolName) + .font(.system(size: 10)) + .foregroundStyle(.white.opacity(0.45)) + .help(watching) + } Text(LoopCardPresentation.duration(sidebarNow.timeIntervalSince(node.createdAt))) .font(.system(size: 10.5, design: .monospaced)) .foregroundStyle(.white.opacity(0.5)) diff --git a/graphcode/Sources/Features/Canvas/LoopCardView.swift b/graphcode/Sources/Features/Canvas/LoopCardView.swift index 36b3d515..c5ea21d0 100644 --- a/graphcode/Sources/Features/Canvas/LoopCardView.swift +++ b/graphcode/Sources/Features/Canvas/LoopCardView.swift @@ -241,6 +241,12 @@ struct LoopCardView: View { if isRemote { Image(systemName: "network").font(.system(size: 9)).foregroundStyle(.white.opacity(0.4)) } + if let watching = MailroomWatchPresentation.tooltip(for: node) { + Image(systemName: MailroomWatchPresentation.symbolName) + .font(.system(size: 9)) + .foregroundStyle(.white.opacity(0.4)) + .help(watching) + } switch entryRole { case .entry: EntryChip() case .cycleOnly: CycleChip() diff --git a/graphcode/Sources/Features/Canvas/MailroomWatchPresentation.swift b/graphcode/Sources/Features/Canvas/MailroomWatchPresentation.swift new file mode 100644 index 00000000..d08ce95c --- /dev/null +++ b/graphcode/Sources/Features/Canvas/MailroomWatchPresentation.swift @@ -0,0 +1,24 @@ +import GraphcodeKit +import MailroomKit + +/// The quiet mark for a loop that is watching its project's Mailroom: an outline +/// envelope on the sidebar row and the canvas card, and a tooltip that says what it +/// is listening for. Nothing more — no count, no unread dot, no colour. The watch is +/// a standing subscription, and the mark only says one exists. +/// +/// Hidden on a resolved loop. `mailroomWatch` outlives the session that set it, but a +/// loop that has finished for good cannot be rung, and a glyph promising otherwise +/// would be the card claiming a mailbox that nobody will open. +enum MailroomWatchPresentation { + static let symbolName = "envelope" + + static func showsGlyph(for node: LoopNode) -> Bool { tooltip(for: node) != nil } + + /// `"Watching the Mailroom"`, or with `· topic ` when the watch is narrowed to one; + /// `nil` when there is nothing to draw. + static func tooltip(for node: LoopNode) -> String? { + guard let watch = node.mailroomWatch, !node.isResolved else { return nil } + guard let topic = watch.topic else { return "Watching the Mailroom" } + return "Watching the Mailroom · topic \(topic)" + } +} diff --git a/graphcode/Tests/MailroomWatchPresentationTests.swift b/graphcode/Tests/MailroomWatchPresentationTests.swift new file mode 100644 index 00000000..e9c0d435 --- /dev/null +++ b/graphcode/Tests/MailroomWatchPresentationTests.swift @@ -0,0 +1,49 @@ +import Foundation +import GraphcodeKit +import MailroomKit +import Testing + +@testable import graphcode + +/// Whether a loop wears the Mailroom-watch envelope, and what its tooltip says +/// (`MailroomWatchPresentation`). +@Suite +struct MailroomWatchPresentationTests { + private func node(watch: MailroomWatch?, state: LoopState = .running) -> LoopNode { + LoopNode(title: "Triage", loopType: .goalBased, mailroomWatch: watch, state: state) + } + + @Test + func aLoopWithoutAWatchShowsNothing() { + let plain = node(watch: nil) + + #expect(!MailroomWatchPresentation.showsGlyph(for: plain)) + #expect(MailroomWatchPresentation.tooltip(for: plain) == nil) + } + + @Test + func aWatchOnEveryPostSaysSoWithoutATopic() { + let hearingAll = node(watch: MailroomWatch()) + + #expect(MailroomWatchPresentation.showsGlyph(for: hearingAll)) + #expect(MailroomWatchPresentation.tooltip(for: hearingAll) == "Watching the Mailroom") + } + + @Test + func aWatchOnOneTopicNamesIt() { + let narrowed = node(watch: MailroomWatch(topic: "design")) + + #expect(MailroomWatchPresentation.showsGlyph(for: narrowed)) + #expect( + MailroomWatchPresentation.tooltip(for: narrowed) == "Watching the Mailroom · topic design") + } + + @Test(arguments: [LoopState.succeeded, .failed, .stalled, .stopped]) + func aResolvedLoopHidesItsWatch(state: LoopState) { + // The watch survives on the node, but nothing can ring a loop that has finished. + let finished = node(watch: MailroomWatch(topic: "design"), state: state) + + #expect(!MailroomWatchPresentation.showsGlyph(for: finished)) + #expect(MailroomWatchPresentation.tooltip(for: finished) == nil) + } +} From 5710e946c2053d0da2acbde9d7190eb3df2c1906 Mon Sep 17 00:00:00 2001 From: scgopi Date: Tue, 8 Sep 2026 01:15:19 -0700 Subject: [PATCH 3/3] Trim MailroomWatchPresentation comments to the retained-watch rationale Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01M8s9Ranjzra1VAKrP27gN7 --- .../Features/Canvas/MailroomWatchPresentation.swift | 12 +++--------- graphcode/Tests/MailroomWatchPresentationTests.swift | 3 --- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/graphcode/Sources/Features/Canvas/MailroomWatchPresentation.swift b/graphcode/Sources/Features/Canvas/MailroomWatchPresentation.swift index d08ce95c..18e7e43e 100644 --- a/graphcode/Sources/Features/Canvas/MailroomWatchPresentation.swift +++ b/graphcode/Sources/Features/Canvas/MailroomWatchPresentation.swift @@ -1,21 +1,15 @@ import GraphcodeKit import MailroomKit -/// The quiet mark for a loop that is watching its project's Mailroom: an outline -/// envelope on the sidebar row and the canvas card, and a tooltip that says what it -/// is listening for. Nothing more — no count, no unread dot, no colour. The watch is -/// a standing subscription, and the mark only says one exists. +/// The envelope that marks a loop watching its project's Mailroom, and its tooltip. /// -/// Hidden on a resolved loop. `mailroomWatch` outlives the session that set it, but a -/// loop that has finished for good cannot be rung, and a glyph promising otherwise -/// would be the card claiming a mailbox that nobody will open. +/// Hidden on a resolved loop: `mailroomWatch` outlives the session that set it, but a +/// loop that has finished for good cannot be rung. enum MailroomWatchPresentation { static let symbolName = "envelope" static func showsGlyph(for node: LoopNode) -> Bool { tooltip(for: node) != nil } - /// `"Watching the Mailroom"`, or with `· topic ` when the watch is narrowed to one; - /// `nil` when there is nothing to draw. static func tooltip(for node: LoopNode) -> String? { guard let watch = node.mailroomWatch, !node.isResolved else { return nil } guard let topic = watch.topic else { return "Watching the Mailroom" } diff --git a/graphcode/Tests/MailroomWatchPresentationTests.swift b/graphcode/Tests/MailroomWatchPresentationTests.swift index e9c0d435..62cfc2a5 100644 --- a/graphcode/Tests/MailroomWatchPresentationTests.swift +++ b/graphcode/Tests/MailroomWatchPresentationTests.swift @@ -5,8 +5,6 @@ import Testing @testable import graphcode -/// Whether a loop wears the Mailroom-watch envelope, and what its tooltip says -/// (`MailroomWatchPresentation`). @Suite struct MailroomWatchPresentationTests { private func node(watch: MailroomWatch?, state: LoopState = .running) -> LoopNode { @@ -40,7 +38,6 @@ struct MailroomWatchPresentationTests { @Test(arguments: [LoopState.succeeded, .failed, .stalled, .stopped]) func aResolvedLoopHidesItsWatch(state: LoopState) { - // The watch survives on the node, but nothing can ring a loop that has finished. let finished = node(watch: MailroomWatch(topic: "design"), state: state) #expect(!MailroomWatchPresentation.showsGlyph(for: finished))