Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,16 @@ Responsive native renders cover 390, 768 and 1440 points. These are fixture rend
## Claude reset detail completion

Preserve the released two-provider-card composition. Within Claude Reset grants, label Full resets and 5-hour resets separately, put each reported expiry immediately below its scope, and qualify paused or currently unusable grants. Keep failed details unknown with a visible message and the existing Claude Usage handoff. Provenance and check time remain inside the source disclosure. No redemption control is introduced.

## Native command menu

On 5 October 2026, the owner selected direction A, the native command menu, from
three rendered systems. The menu follows macOS typography and row geometry;
the existing black/mint full window, title bar, Daddy imagery and paired landing
remain the review system. Never-scanned, refreshing, failed and ready status stay
explicit, with last-success time retained after a failed scan. Context refresh
is separate from provider checks. Closing the window retains the menu; Open
returns to the same model, and active model-owned reads trigger native quit review.
Native fixture captures cover the supported minimum 960×640, standard 1180×740
and wide 1440×900 logical points, at the display's 2x scale. Interactive menu
checks use synthetic discovery and do not qualify a published or installed build.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ Plugin/system definitions are read-only in this workflow and identify their owni
swift run ContextDaddy
```

ContextDaddy remains available in the menu bar after its window closes. **Open ContextDaddy** reopens the review window; **Refresh Local Context** scans local skill/context metadata without starting provider allowance or usage-history checks. Quit reviews ongoing model-owned background reads. See [branch reconciliation](docs/project-reconciliation.md).

The app requires macOS 14 or newer. Its Codex adapter reads the loopback-only local telemetry stack and renders Prometheus metrics and Tempo sessions inside ContextDaddy. A separate Claude adapter reads Claude Code metrics from the same local Prometheus path only if Claude exports to that collector; ContextDaddy does not enable or reroute Claude telemetry. The rest of the product still works when either source is absent or partial.

ContextDaddy runs [ccusage](https://github.com/ccusage/ccusage) 20.0.24 directly in offline mode for local history. The packaged app carries its own pinned helper and [MIT acknowledgement](CONTEXTDADDY_NOTICES.md); no CodeVetter installation or CLI is needed at runtime. A **Refresh history** action rescans local logs. **Check allowance** separately calls Codex app-server and Claude Code `/usage` through their installed CLIs. Claude's usage endpoint supplies allowance windows and reset grants from one read, while the CLI supplies plan and paid-credit details. If the CLI display fails, those unreported details remain unavailable. Opt-in automatic checking uses the same adapters with a 15-minute minimum interval. These readings are not ccusage totals.
Expand Down
33 changes: 32 additions & 1 deletion Sources/ContextDaddy/ContextDaddyApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,28 @@ struct ContextDaddyApp: App {
@State private var model = ContextDaddyModel()

var body: some Scene {
WindowGroup {
Window("ContextDaddy", id: "main") {
RootView()
.environment(model)
.preferredColorScheme(.dark)
.frame(minWidth: 960, minHeight: 640)
.onAppear { appDelegate.activeWork = { model.activeWorkDescription } }
}
.defaultSize(width: 1180, height: 740)
.defaultPosition(.center)
.windowStyle(.titleBar)
MenuBarExtra {
ContextMenu(model: model)
} label: {
Label("ContextDaddy", systemImage: "square.stack.3d.up")
}
.menuBarExtraStyle(.menu)
}
}

@MainActor
final class ContextDaddyAppDelegate: NSObject, NSApplicationDelegate {
var activeWork: (() -> String?)?
func applicationDidFinishLaunching(_ notification: Notification) {
NSApplication.shared.applicationIconImage = ContextDoodleArt.appIcon()
DispatchQueue.main.async {
Expand All @@ -41,4 +49,27 @@ final class ContextDaddyAppDelegate: NSObject, NSApplicationDelegate {
func applicationDidBecomeActive(_ notification: Notification) {
NSApplication.shared.applicationIconImage = ContextDoodleArt.appIcon()
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { false }

func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
DaddyQuitReview.shouldQuit(appName: "ContextDaddy", activeWork: activeWork?()) ? .terminateNow : .terminateCancel
}

}

struct ContextMenu: View {
let model: ContextDaddyModel

var body: some View {
DaddyMenuStatus(message: model.contextMenuStatus)
if let lastRefresh = model.lastSuccessfulRefreshAt {
Text("Last refresh: \(lastRefresh.formatted(date: .abbreviated, time: .shortened))")
}
Divider()
DaddyMenuOpenButton(appName: "ContextDaddy")
Button("Refresh Local Context") { Task { await model.refreshSkillLibrary() } }
.disabled(model.isLoading)
Divider()
DaddyMenuQuitButton(appName: "ContextDaddy")
}
}
24 changes: 23 additions & 1 deletion Sources/ContextDaddy/ContextDaddyModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ final class ContextDaddyModel {
var pendingSkillInvocation: InvocationMode?
var pendingSkillAllFolders = false
var isLoading = false
var isContextRefreshRunning = false
var lastSuccessfulRefreshAt: Date?
var loadStarted = Date()
var discoveryStatus = "Not scanned yet"
var lastError: String?
Expand All @@ -153,6 +155,23 @@ final class ContextDaddyModel {
self.discover = discover
}

var contextMenuStatus: String {
if isLoading { return "Refreshing local context…" }
if lastError != nil { return "Last refresh needs attention" }
return lastSuccessfulRefreshAt == nil ? "Local context not scanned yet" : "Ready for local review"
}

var activeWorkDescription: String? {
if isLoading || isContextRefreshRunning || isSkillFolderLoading || isVerifyingSkillIssues || isVerifyingConfigurationIssues {
return "A local context review is still running."
}
if isUsageLoading || isDevinLoading { return "Local usage history is still being read." }
if isTelemetryLoading { return "Local telemetry is still being read." }
if isSkillActivityLoading { return "Skill activity history is still being read." }
if isQuotaLoading { return "A provider allowance check is still running." }
return nil
}

func show(_ destination: AppSection) {
if destination == .skills, pendingSkillRuntime != nil { skillsMode = .library }
section = destination
Expand Down Expand Up @@ -339,6 +358,7 @@ final class ContextDaddyModel {
discoveryReport = loaded.0
catalog = loaded.1
projects = loaded.2
lastSuccessfulRefreshAt = Date()
discoveryStatus = "\(loaded.1.physicalSkillCount) physical skills · \(loaded.1.exposureCount) locations"
} catch {
lastError = error.localizedDescription
Expand All @@ -349,12 +369,13 @@ final class ContextDaddyModel {
Task { await refreshUsage() }
let request = UUID()
refreshGeneration = request
isContextRefreshRunning = true
isLoading = true
loadStarted = Date()
discoveryStatus = discoveryReport == nil ? "Reading local context metadata…" : "Refreshing while previous results remain available…"
lastError = nil
defer {
if refreshGeneration == request { isLoading = false }
if refreshGeneration == request { isLoading = false; isContextRefreshRunning = false }
}
let roots = extraRoots.map { URL(fileURLWithPath: $0, isDirectory: true) }
let discover = self.discover
Expand All @@ -370,6 +391,7 @@ final class ContextDaddyModel {
discoveryReport = loaded.0
catalog = loaded.1
projects = loaded.2
lastSuccessfulRefreshAt = Date()
discoveryStatus = "\(loaded.2.count.formatted()) projects · \(loaded.0.items.count.formatted()) file locations · \(String(format: "%.1f", loaded.0.elapsed)) s"
} catch {
guard refreshGeneration == request else { return }
Expand Down
100 changes: 100 additions & 0 deletions Sources/ContextDaddy/DaddyVisualCore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import AppKit
import SwiftUI

// The approved Daddy-series colors and control geometry. This source is copied
// into each app so native packages remain independently buildable.
enum DaddyPalette {
static let canvas = Color.black
static let ink = Color.white
static let mint = Color(red: 0.42, green: 0.79, blue: 0.62)
static let secondaryInk = Color(red: 0.78, green: 0.90, blue: 0.86)
static let coral = Color(red: 0.90, green: 0.46, blue: 0.40)
static let blue = Color(red: 0.33, green: 0.58, blue: 0.83)
static let cyan = Color(red: 0.27, green: 0.70, blue: 0.75)
static let amber = Color(red: 0.87, green: 0.67, blue: 0.28)
}

struct DaddyControlStyle: ButtonStyle {
var prominent = false
var hoverFeedback = false

func makeBody(configuration: Configuration) -> some View {
DaddyControl(
label: configuration.label,
prominent: prominent,
hoverFeedback: hoverFeedback,
isPressed: configuration.isPressed
)
}
}

private struct DaddyControl<Label: View>: View {
let label: Label
let prominent: Bool
let hoverFeedback: Bool
let isPressed: Bool
@State private var hovering = false
@Environment(\.isEnabled) private var isEnabled

var body: some View {
label
.font(.system(size: 13, weight: .medium))
.padding(.horizontal, 11)
.padding(.vertical, 7)
.foregroundStyle(prominent ? DaddyPalette.canvas : DaddyPalette.mint)
.background(
prominent ? DaddyPalette.mint :
(hoverFeedback && hovering && isEnabled ? DaddyPalette.mint.opacity(0.18) : DaddyPalette.canvas),
in: RoundedRectangle(cornerRadius: 7)
)
.overlay(RoundedRectangle(cornerRadius: 7)
.stroke(DaddyPalette.mint.opacity(prominent ? 1 : 0.35), lineWidth: 1))
.opacity(isEnabled ? (isPressed ? 0.7 : 1) : 0.4)
.contentShape(RoundedRectangle(cornerRadius: 7))
.onHover { hovering = $0 }
}
}

// Native menu bar actions shared by the four independently packaged apps.
// Each app owns the status and task actions between these controls.
struct DaddyMenuOpenButton: View {
let appName: String
@Environment(\.openWindow) private var openWindow

var body: some View {
Button("Open \(appName)") {
openWindow(id: "main")
NSApplication.shared.activate(ignoringOtherApps: true)
}
}
}

struct DaddyMenuQuitButton: View {
let appName: String

var body: some View {
Button("Quit \(appName)") { NSApplication.shared.terminate(nil) }
}
}

struct DaddyMenuStatus: View {
let message: String

var body: some View {
Text(message).accessibilityLabel("Status: \(message)")
}
}

@MainActor
enum DaddyQuitReview {
static func shouldQuit(appName: String, activeWork: String?) -> Bool {
guard let activeWork else { return true }
let alert = NSAlert()
alert.messageText = "Quit \(appName)?"
alert.informativeText = "\(activeWork) Quitting now may interrupt it."
alert.addButton(withTitle: "Keep Running")
alert.addButton(withTitle: "Quit Anyway")
return alert.runModal() == .alertSecondButtonReturn
}
}

26 changes: 8 additions & 18 deletions Sources/ContextDaddy/Theme.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import ContextCore
import SwiftUI

enum DaddyTheme {
static let canvas = Color.black
static let panel = Color.black
static let canvas = DaddyPalette.canvas
static let panel = DaddyPalette.canvas
static let raised = Color(red: 0.055, green: 0.075, blue: 0.066)
static let line = mint.opacity(0.2)
static let muted = Color(red: 0.78, green: 0.90, blue: 0.86)
static let mint = Color(red: 0.42, green: 0.79, blue: 0.62)
static let blue = Color(red: 0.33, green: 0.58, blue: 0.83)
static let amber = Color(red: 0.87, green: 0.67, blue: 0.28)
static let coral = Color(red: 0.90, green: 0.46, blue: 0.40)
static let muted = DaddyPalette.secondaryInk
static let mint = DaddyPalette.mint
static let blue = DaddyPalette.blue
static let amber = DaddyPalette.amber
static let coral = DaddyPalette.coral

static func color(for quality: EvidenceQuality) -> Color {
switch quality {
Expand Down Expand Up @@ -61,18 +61,8 @@ struct Panel<Content: View>: View {

struct ContextDaddyButtonStyle: ButtonStyle {
var prominent = false
@Environment(\.isEnabled) private var isEnabled

func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(.system(size: 13, weight: .medium))
.padding(.horizontal, 11)
.padding(.vertical, 7)
.foregroundStyle(prominent ? Color.black : DaddyTheme.mint)
.background(prominent ? DaddyTheme.mint : Color.black, in: RoundedRectangle(cornerRadius: 7))
.overlay(RoundedRectangle(cornerRadius: 7).stroke(DaddyTheme.mint.opacity(prominent ? 1 : 0.35)))
.opacity(isEnabled ? (configuration.isPressed ? 0.7 : 1) : 0.4)
.contentShape(RoundedRectangle(cornerRadius: 7))
DaddyControlStyle(prominent: prominent).makeBody(configuration: configuration)
}
}

Expand Down
39 changes: 39 additions & 0 deletions Tests/ContextDaddyTests/ContextDaddyModelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,52 @@ struct ContextDaddyModelTests {
#expect(model.sourcesMode == .diagnostics)
}

@Test func menuDoesNotClaimReadyBeforeDiscovery() {
let model = ContextDaddyModel()
#expect(model.contextMenuStatus == "Local context not scanned yet")
model.lastError = "Fixture failure"
#expect(model.contextMenuStatus == "Last refresh needs attention")
model.isLoading = true
#expect(model.contextMenuStatus == "Refreshing local context…")
}

@Test func quitReviewIncludesEveryModelOwnedBackgroundRead() {
let model = ContextDaddyModel()
#expect(model.activeWorkDescription == nil)
let flags: [ReferenceWritableKeyPath<ContextDaddyModel, Bool>] = [
\.isLoading, \.isContextRefreshRunning, \.isSkillFolderLoading, \.isVerifyingSkillIssues,
\.isVerifyingConfigurationIssues, \.isUsageLoading, \.isDevinLoading,
\.isTelemetryLoading, \.isSkillActivityLoading, \.isQuotaLoading,
]
for flag in flags {
model[keyPath: flag] = true
#expect(model.activeWorkDescription != nil)
model[keyPath: flag] = false
#expect(model.activeWorkDescription == nil)
}
}

@Test func closingLastWindowKeepsTheMenuAppAvailable() {
let delegate = ContextDaddyAppDelegate()
#expect(!delegate.applicationShouldTerminateAfterLastWindowClosed(.shared))
#expect(DaddyQuitReview.shouldQuit(appName: "ContextDaddy", activeWork: nil))
}

@Test func failedLibraryRefreshPreservesPreviousDiscovery() async {
let source = LockedDiscoveryResults([.success(report(elapsed: 0.25)), .failure(.expected)])
let model = ContextDaddyModel { _ in try source.next() }
await model.refreshSkillLibrary()
let previous = model.catalog
let lastSuccess = model.lastSuccessfulRefreshAt
#expect(lastSuccess != nil)
#expect(model.contextMenuStatus == "Ready for local review")
#expect(model.usageReport == nil)
#expect(!model.isUsageLoading)
#expect(!model.isQuotaLoading)
await model.refreshSkillLibrary()
#expect(model.catalog == previous)
#expect(model.lastSuccessfulRefreshAt == lastSuccess)
#expect(model.contextMenuStatus == "Last refresh needs attention")
#expect(model.lastError != nil)
#expect(!model.isLoading)
}
Expand Down
Loading
Loading