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
4 changes: 2 additions & 2 deletions App/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.2</string>
<string>0.1.3</string>
<key>CFBundleVersion</key>
<string>5</string>
<string>6</string>
<key>LSMinimumSystemVersion</key>
<string>14.0</string>
<key>LSUIElement</key>
Expand Down
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Meanwhile is a macOS 14 menu-bar app that turns coding-agent wait time into one
small, actionable GitHub task. It is built in Swift 5.10 with no third-party
dependencies.

## v0.1.2 behavior
## v0.1.3 behavior

- Uses Claude Code and Codex lifecycle hooks—not process or CPU guesses—to track
each session as `thinking`, `needs-you`, or `idle`.
Expand All @@ -29,6 +29,15 @@ dependencies.
- Offers off-by-default, needs-you-only notifications with a selectable delay.
Meanwhile confirms the task is still waiting before posting one quiet reminder;
reviews and CI never notify.
- Lets you run a clearly labelled six-second attention test from Settings,
including a test notification when reminders are available. The test never
creates work or replaces a real interruption.
- Shows independent freshness for review and failing-CI sources, with manual
refresh and in-context recovery for GitHub authentication or agent hooks.
- Keeps agent requests always on while letting you independently enable reviews
and failing CI from Settings, with no relaunch required.
- Keeps failed agent and browser handoffs active, offering Settings or Copy Link
instead of silently losing the route back.
- Shows the installed version and latest GitHub release in Settings without
downloading or installing anything automatically.
- Identifies agent sessions that may be stuck and lets you clear only those
Expand Down Expand Up @@ -126,9 +135,9 @@ cask with:
GITHUB_REPOSITORY="tcballard/Meanwhile" ./Scripts/release-unsigned.sh
```

This produces `dist/Meanwhile-0.1.2-unsigned.zip` and `dist/meanwhile.rb`.
This produces `dist/Meanwhile-0.1.3-unsigned.zip` and `dist/meanwhile.rb`.
Publish the archive only as a GitHub **pre-release** tagged
`v0.1.2-unsigned`. The generated cask identifies it as unsigned and tells users
`v0.1.3-unsigned`. The generated cask identifies it as unsigned and tells users
that Gatekeeper will block the first launch. Users who trust the build must
explicitly remove quarantine themselves; the cask does not bypass Gatekeeper.

Expand All @@ -144,7 +153,7 @@ GITHUB_REPOSITORY="owner/Meanwhile" \
```

The script builds and signs the app in a temporary local directory, then
produces `dist/Meanwhile-0.1.2.zip` and `dist/meanwhile.rb`. It verifies the
produces `dist/Meanwhile-0.1.3.zip` and `dist/meanwhile.rb`. It verifies the
stapled app and a clean extraction of the final archive before returning.
Publishing the archive, cask, and GitHub release remains an explicit
release-owner action. Set `RELEASE_OUTPUT_DIR` to write the final artifacts
Expand Down
61 changes: 61 additions & 0 deletions Sources/Meanwhile/AttentionSourcesSettingsSection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import MeanwhileCore
import SwiftUI

struct AttentionSourcesSettingsSection: View {
@Binding var selection: AttentionSourceSelection

var body: some View {
VStack(alignment: .leading, spacing: 12) {
sourceRow(
title: "Agent requests",
detail: "Permission prompts, questions, and other agent handoffs.",
isOn: .constant(true),
isLocked: true
)
sourceRow(
title: "Failing CI",
detail: "Failed checks on your open pull requests while an agent is thinking.",
isOn: $selection.failingCIEnabled
)
sourceRow(
title: "Review requests",
detail: "Pull requests waiting for your review while an agent is thinking.",
isOn: $selection.reviewsEnabled
)
Text("GitHub sources remain wait-gated: enabling them does not show work while every agent is idle.")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
}

private func sourceRow(
title: String,
detail: String,
isOn: Binding<Bool>,
isLocked: Bool = false
) -> some View {
HStack(alignment: .top, spacing: 12) {
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.callout.weight(.semibold))
Text(detail)
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Spacer(minLength: 12)
if isLocked {
Label("Always on", systemImage: "checkmark.circle.fill")
.font(.caption.weight(.semibold))
.foregroundStyle(.green)
.accessibilityLabel("Agent requests always on")
} else {
Toggle("", isOn: isOn)
.labelsHidden()
.toggleStyle(.switch)
.accessibilityLabel(title)
}
}
}
}
59 changes: 59 additions & 0 deletions Sources/Meanwhile/AttentionTestSettingsSection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import SwiftUI

enum AttentionTestRunResult: Equatable {
case startedMenuBarOnly
case startedWithNotification
case blockedByRealAttention
case failedNotification

var message: String {
switch self {
case .startedMenuBarOnly:
return "The menu bar will show a test for six seconds. Reminders are off or unavailable."
case .startedWithNotification:
return "The menu bar and a clearly labelled test notification are active."
case .blockedByRealAttention:
return "A real task needs you now, so Meanwhile did not replace it with a test."
case .failedNotification:
return "The menu-bar test worked, but macOS did not accept the test notification."
}
}

var isError: Bool {
self == .blockedByRealAttention || self == .failedNotification
}
}

struct AttentionTestSettingsSection: View {
let isRunning: Bool
let result: AttentionTestRunResult?
let run: () -> Void

var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .firstTextBaseline, spacing: 12) {
VStack(alignment: .leading, spacing: 2) {
Text("Test the attention loop")
.font(.callout.weight(.semibold))
Text("Preview the menu-bar bloom and, when enabled, a test reminder. No agent or GitHub activity is created.")
.font(.caption)
.foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Spacer(minLength: 8)
Button(isRunning ? "Testing…" : "Run Attention Test", action: run)
.disabled(isRunning)
.accessibilityHint("Shows a labelled six-second test without creating work")
}
if let result {
SettingsInlineMessage(
result.message,
systemImage: result.isError
? "exclamationmark.triangle.fill"
: "checkmark.circle.fill",
tint: result.isError ? .orange : .secondary
)
}
}
}
}
90 changes: 87 additions & 3 deletions Sources/Meanwhile/ConnectionHealthSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@ struct ConnectionHealthSection: View {
let integrationHealthError: String?
let lastAgentEvent: AgentSessionState?
let githubAuthenticationStatus: GitHubAuthenticationStatus
let githubLoginCopyMessage: String?
let sessionInspection: AgentSessionInspection
let sessionRecoveryMessage: String?
let sessionRecoveryIsError: Bool
let isClearingStuckSessions: Bool
let sourceRefreshSnapshot: SourceRefreshSnapshot
let staleAfter: String
let repairIntegrations: () -> Void
let clearStuckSessions: () -> Void
let copyGitHubLoginCommand: () -> Void
let refreshGitHubSources: () -> Void
let now: Date
@State private var isConfirmingClear = false

Expand All @@ -22,7 +27,10 @@ struct ConnectionHealthSection: View {
tint: integrationHealthTint,
title: "Agent hooks",
value: integrationHealthTitle,
detail: integrationHealthDetail
detail: integrationHealthDetail,
actionTitle: integrationHealth.state == .installed ? nil : "Repair…",
actionAccessibilityLabel: "Repair agent integrations",
action: repairIntegrations
)
HealthRow(
systemImage: sessionHealthSymbol,
Expand All @@ -47,7 +55,24 @@ struct ConnectionHealthSection: View {
tint: githubHealthTint,
title: "GitHub",
value: githubHealthTitle,
detail: githubHealthDetail
detail: githubHealthDetail,
actionTitle: githubAuthenticationStatus == .authenticated
? nil
: "Copy Login Command",
actionAccessibilityLabel: "Copy GitHub login command",
action: copyGitHubLoginCommand
)
SourceHealthRow(
title: "Review source",
record: sourceRefreshSnapshot.reviews,
now: now,
refresh: refreshGitHubSources
)
SourceHealthRow(
title: "Failing CI source",
record: sourceRefreshSnapshot.failingCI,
now: now,
refresh: refreshGitHubSources
)
}
.confirmationDialog(
Expand Down Expand Up @@ -181,7 +206,8 @@ struct ConnectionHealthSection: View {
}

private var githubHealthDetail: String {
githubAuthenticationStatus == .authenticated
if let githubLoginCopyMessage { return githubLoginCopyMessage }
return githubAuthenticationStatus == .authenticated
? "Using the GitHub CLI session on this Mac."
: "Run gh auth login, then refresh repository sources."
}
Expand All @@ -197,6 +223,64 @@ struct ConnectionHealthSection: View {
}
}

private struct SourceHealthRow: View {
let title: String
let record: SourceRefreshRecord
let now: Date
let refresh: () -> Void

var body: some View {
HealthRow(
systemImage: symbol,
tint: tint,
title: title,
value: value,
detail: detail,
actionTitle: record.isEnabled ? (record.lastFailureAt == nil ? "Refresh All" : "Retry All") : nil,
actionAccessibilityLabel: "\(record.lastFailureAt == nil ? "Refresh" : "Retry") all GitHub sources",
action: refresh,
actionDisabled: record.isRefreshing
)
}

private var value: String {
guard record.isEnabled else { return "Disabled" }
if record.isRefreshing { return "Refreshing…" }
if let failure = record.lastFailureAt {
guard let success = record.lastSuccessAt else { return "Refresh failed" }
if failure > success { return "Refresh failed" }
}
if let success = record.lastSuccessAt {
return "Updated \(relativeDateString(success, relativeTo: now))"
}
return "Not checked yet"
}

private var detail: String {
guard record.isEnabled else { return "Disabled in Attention sources." }
if record.isRefreshing { return "Checking GitHub now." }
if record.lastFailureAt != nil { return "Check GitHub CLI authentication, then retry." }
if record.lastSuccessAt != nil {
return "Automatic checks pause while agents are idle. Refresh works at any time."
}
return "Run a manual refresh, or start an agent session to enable automatic checks."
}

private var symbol: String {
if !record.isEnabled { return "minus.circle" }
if record.isRefreshing { return "arrow.triangle.2.circlepath" }
if record.lastFailureAt != nil { return "exclamationmark.triangle.fill" }
if record.lastSuccessAt != nil { return "checkmark.circle.fill" }
return "clock.badge.questionmark"
}

private var tint: Color {
if record.lastFailureAt != nil { return .orange }
if record.lastSuccessAt != nil { return .green }
return .secondary
}
}

private struct HealthRow: View {
let systemImage: String
let tint: Color
Expand Down
Loading
Loading