Skip to content
4 changes: 2 additions & 2 deletions Packages/macOS/CmuxNext/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -478,15 +478,15 @@ let package = Package(
// feed owner; the App supplies the source.
.target(
name: "CmuxNextFeed",
dependencies: ["CmuxNextDesign", "CmuxNextWakeups"],
dependencies: ["CmuxNextDesign", "CmuxNextIcons", "CmuxNextWakeups"],
resources: [
.process("Resources"),
],
swiftSettings: uiSwiftSettings
),
.testTarget(
name: "CmuxNextFeedTests",
dependencies: ["CmuxNextFeed"],
dependencies: ["CmuxNextFeed", "CmuxNextIcons"],
swiftSettings: uiSwiftSettings
),
// Remote desktop pane (plans/cmux-next/remote-desktop.md section 7):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CmuxNextIcons
import SwiftUI

/// How much room the answer controls get.
Expand All @@ -20,7 +21,7 @@ struct FeedAnswerControls: View {

var body: some View {
if let closed = FeedStrings.closed(item) {
Label(closed, systemImage: item.state == .answered ? "checkmark" : "minus.circle")
FeedIconLabel(closed, icon: item.state == .answered ? .statusSuccess : .actionRemove, textSize: 11.5)
.font(.system(size: 11.5))
.foregroundStyle(colors.tertiary)
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import AppKit
import CmuxNextIcons
import SwiftUI

/// `approve`: scope (when the poster offers more than one), Deny, Allow.
Expand All @@ -20,7 +22,8 @@ struct FeedApproveControls: View {
} label: {
HStack(spacing: 3) {
Text(FeedStrings.scope(scope))
Image(systemName: "chevron.up.chevron.down").font(.system(size: 8, weight: .semibold))
// Menu labels render through AppKit, which keeps images but not canvases.
Image(nsImage: .icon(.controlPopup, size: .iconFloor)).renderingMode(.template)
}
.font(.system(size: 11.5))
.foregroundStyle(colors.secondary)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CmuxNextIcons
import SwiftUI

/// Renders live GitHub content kept in the client detail cache. The feed owner
Expand All @@ -20,14 +21,14 @@ struct FeedGitHubDetailSummary: View {
.fixedSize(horizontal: false, vertical: true)
}
if let branch = detail.branch, !branch.isEmpty {
Label(branch, systemImage: "arrow.triangle.branch")
FeedIconLabel(branch, icon: .gitBranch, textSize: 11)
.font(.system(size: 11)).foregroundStyle(colors.secondary)
}
if !detail.checks.isEmpty {
VStack(alignment: .leading, spacing: 3) {
ForEach(detail.checks, id: \.self) { check in
Label(check.name + (check.conclusion.map { ": \($0)" } ?? ""),
systemImage: check.conclusion == "failure" ? "xmark.circle" : "checkmark.circle")
FeedIconLabel(check.name + (check.conclusion.map { ": \($0)" } ?? ""),
icon: check.conclusion == "failure" ? .statusError : .statusSuccess, textSize: 11)
.font(.system(size: 11)).foregroundStyle(check.conclusion == "failure" ? colors.danger : colors.secondary)
}
}
Expand All @@ -46,14 +47,14 @@ struct FeedGitHubActions: View {
VStack(alignment: .leading, spacing: 9) {
Text(FeedStrings.githubActions).font(.system(size: 11, weight: .semibold)).foregroundStyle(colors.secondary)
HStack(spacing: 7) {
if supported(.open) { action(FeedStrings.openOnGitHub, "arrow.up.right", .open) }
if supported(.checkout) { action(FeedStrings.checkout, "arrow.down.to.line", .checkout) }
if supported(.startAgent) { action(FeedStrings.startAgent, "sparkles", .startAgent) }
if supported(.approve) { action(FeedStrings.approve, "checkmark", .approve) }
if supported(.requestChanges) { action(FeedStrings.requestChanges, "exclamationmark.bubble", .requestChanges) }
if supported(.open) { action(FeedStrings.openOnGitHub, .linkExternal, .open) }
if supported(.checkout) { action(FeedStrings.checkout, .gitBranch, .checkout) }
if supported(.startAgent) { action(FeedStrings.startAgent, .agentChatNew, .startAgent) }
if supported(.approve) { action(FeedStrings.approve, .actionConfirm, .approve) }
if supported(.requestChanges) { action(FeedStrings.requestChanges, .actionReview, .requestChanges) }
}
if let error = model.githubActionError {
Label(error, systemImage: "exclamationmark.triangle").font(.system(size: 11)).foregroundStyle(colors.danger)
FeedIconLabel(error, icon: .statusWarning, textSize: 11).font(.system(size: 11)).foregroundStyle(colors.danger)
}
HStack(spacing: 7) {
TextField(FeedStrings.comment, text: $comment)
Expand All @@ -78,9 +79,9 @@ struct FeedGitHubActions: View {
model.githubActions?(item).contains(action) ?? true
}

private func action(_ title: String, _ symbol: String, _ kind: FeedGitHubAction) -> some View {
private func action(_ title: String, _ icon: IconName, _ kind: FeedGitHubAction) -> some View {
Button { model.onGitHubAction?(item, kind, nil) } label: {
Label(title, systemImage: symbol).font(.system(size: 11))
FeedIconLabel(title, icon: icon, textSize: 11).font(.system(size: 11))
}
.buttonStyle(FeedButtonStyle(role: kind == .approve ? .primary : .plain, compact: true))
.disabled(model.githubActionPending)
Expand Down
56 changes: 31 additions & 25 deletions Packages/macOS/CmuxNext/Sources/CmuxNextFeed/Views/FeedGlyph.swift
Original file line number Diff line number Diff line change
@@ -1,47 +1,53 @@
import CmuxNextIcons
import Foundation
import SwiftUI

/// The kind of an item as a small symbol; color only for attention.
/// The kind of an item as a small registry icon; color only for attention.
struct FeedGlyph: View {
let item: FeedItem
/// The point size of the text the glyph sits beside.
var size: CGFloat = 12
@Environment(\.feedColors) private var colors

var body: some View {
Image(systemName: symbol)
.font(.system(size: size, weight: .medium))
Icon(Self.icon(for: item), size: Self.side(forTextSize: size))
.foregroundStyle(tint)
.frame(width: size + 6, height: size + 6)
}

private var symbol: String {
/// The icon box beside `size` point text.
static func side(forTextSize size: CGFloat) -> CGFloat {
.iconRowSize(forLabelPointSize: size)
}

static func icon(for item: FeedItem) -> IconName {
switch item.prompt {
case .notice:
switch item.poster.kind {
case .integration: "arrow.triangle.pull"
case .server, .vm: "server.rack"
case .automation: "gearshape.2"
case .system: item.context.terminal != nil ? "terminal" : "bell"
default: "bell"
case .integration: .integration
case .server, .vm: .machineRemote
case .automation: .automation
case .system: item.context.terminal != nil ? .terminal : .notification
default: .notification
}
case let .approve(approve):
switch approve.action.type {
case .command: "terminal"
case .edit: "pencil.line"
case .network: "network"
case .install: "shippingbox"
case .tool, .custom: "wrench.and.screwdriver"
case .command: .terminal
case .edit: .actionEdit
case .network: .network
case .install: .package
case .tool, .custom: .tools
}
case .question: "text.bubble"
case .choice: "list.bullet"
case .confirm: "checkmark.circle"
case .signIn: "person.badge.key"
case .passkey: "person.badge.key.fill"
case .review: "doc.text.magnifyingglass"
case .input: "rectangle.and.pencil.and.ellipsis"
case .file: "doc.badge.plus"
case .handoff: "hand.raised"
case .custom: "square.grid.2x2"
case .question: .agentQuestion
case .choice: .feedChoice
case .confirm: .actionConfirm
case .signIn: .accountSignin
case .passkey: .accountPasskey
case .review: .actionReview
case .input: .feedInput
case .file: .fileNew
case .handoff: .agentHandoff
case .custom: .feedCustom
}
}

Expand Down Expand Up @@ -76,7 +82,7 @@ struct PosterLine: View {
Text(verbatim: "·")
Text(FeedRelativeTime.string(item.createdAt, now: now)).monospacedDigit()
if item.home.isLocal {
Image(systemName: "laptopcomputer").help(FeedStrings.thisMac)
Icon(.machineLocal, size: .iconRowSize(forLabelPointSize: 11)).help(FeedStrings.thisMac)
}
if item.count > 1 {
Text(verbatim: "×\(item.count)").monospacedDigit()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import CmuxNextIcons
import SwiftUI

/// A label whose icon is a registry icon at the row size of its
/// `textSize` point text; the caller sets the font.
struct FeedIconLabel: View {
let title: String
let icon: IconName
let textSize: CGFloat

init(_ title: String, icon: IconName, textSize: CGFloat) {
self.title = title
self.icon = icon
self.textSize = textSize
}

var body: some View {
Label {
Text(title)
} icon: {
Icon(icon, size: .iconRowSize(forLabelPointSize: textSize))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import AppKit
import CmuxNextIcons
import SwiftUI

/// Connection tabs and compact inbox triage controls, driven by host callbacks.
struct FeedInboxHeader: View {
/// Header glyphs sit beside 12 pt text.
static let glyphSide = CGFloat.iconRowSize(forLabelPointSize: 12)
@Binding var filter: FeedInboxFilter
let hasUnread: Bool
let canRefresh: Bool
Expand All @@ -14,21 +18,25 @@ struct FeedInboxHeader: View {
var body: some View {
VStack(spacing: 0) {
HStack(spacing: 5) {
connectionTab(.github, title: FeedStrings.github, symbol: "arrow.triangle.pull")
connectionTab(.feed, title: FeedStrings.title, symbol: "tray")
connectionTab(.github, title: FeedStrings.github, icon: .brandGithub)
connectionTab(.feed, title: FeedStrings.title, icon: .inbox)
Spacer(minLength: 0)
if canAddConnection {
Button(action: addConnection) {
Label(FeedStrings.addConnection, systemImage: "plus")
.font(.system(size: 11.5))
Label {
Text(FeedStrings.addConnection)
} icon: {
Icon(.actionAdd, size: .iconRowSize(forLabelPointSize: 11.5))
}
.font(.system(size: 11.5))
}
.buttonStyle(.plain).foregroundStyle(colors.secondary)
}
}
.padding(.horizontal, 10).frame(height: 43)
FeedHairline()
HStack(spacing: 10) {
Image(systemName: "magnifyingglass").foregroundStyle(colors.tertiary)
Icon(.search, size: Self.glyphSide).foregroundStyle(colors.tertiary)
TextField(FeedStrings.filterInbox, text: $filter.query)
.textFieldStyle(.plain).font(.system(size: 12))
.foregroundStyle(colors.primary)
Expand All @@ -40,14 +48,16 @@ struct FeedInboxHeader: View {
Text(FeedStrings.notices).tag(FeedInboxFilter.Category.notices)
}
} label: {
Image(systemName: filter.category == .all ? "line.3.horizontal.decrease" : "line.3.horizontal.decrease.circle.fill")
// Menu labels render through AppKit, which keeps images but not canvases.
Image(nsImage: .icon(.filter, size: Self.glyphSide, style: filter.category == .all ? .line : .solid))
.renderingMode(.template)
}
.menuStyle(.borderlessButton).menuIndicator(.hidden)
.frame(width: 16).help(FeedStrings.filterInbox)
Button(action: markRead) { Image(systemName: "checkmark.circle") }
.buttonStyle(.plain).disabled(!hasUnread).help(FeedStrings.markAllRead)
if canRefresh {
Button(action: refresh) { Image(systemName: "arrow.clockwise") }
Button(action: refresh) { Icon(.actionReload, size: Self.glyphSide) }
.buttonStyle(.plain).help(FeedStrings.refresh)
}
}
Expand All @@ -57,9 +67,9 @@ struct FeedInboxHeader: View {
}
}

private func connectionTab(_ connection: FeedInboxFilter.Connection, title: String, symbol: String) -> some View {
private func connectionTab(_ connection: FeedInboxFilter.Connection, title: String, icon: IconName) -> some View {
Button { filter.connection = connection } label: {
Label(title, systemImage: symbol)
FeedIconLabel(title, icon: icon, textSize: 11.5)
.font(.system(size: 11.5, weight: filter.connection == connection ? .medium : .regular))
.padding(.horizontal, 9).padding(.vertical, 6)
.background(RoundedRectangle(cornerRadius: 6).fill(filter.connection == connection ? colors.selection : .clear))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import AppKit
import CmuxNextIcons
import SwiftUI

/// The feed's wide mode. Connection and text filters are client view state;
Expand Down Expand Up @@ -45,7 +47,8 @@ struct FeedInboxView: View {
FeedInboxDetail(item: shown, thread: groups.all.first { $0.members.contains { $0.id == shown.id } }, model: model)
} else {
VStack(spacing: 12) {
Image(systemName: "tray").font(.system(size: 27, weight: .light))
// A pack drawing inks about two thirds of its box; 44 matches the 27 pt tray it replaced.
Icon(.inboxEmpty, size: 44)
Text(FeedStrings.selectInboxItem).font(.system(size: 13))
}
.foregroundStyle(colors.tertiary)
Expand Down Expand Up @@ -133,7 +136,13 @@ struct FeedDetailToolbar: View {
Button(FeedStrings.snoozeHour) { model.snooze([item.id], for: 3_600) }
Button(FeedStrings.snoozeTomorrow) { model.snooze([item.id], for: 86_400) }
} label: {
Label(FeedStrings.snooze, systemImage: "moon.zzz").font(.system(size: 11.5))
// Menu labels render through AppKit, which keeps images but not canvases.
Label {
Text(FeedStrings.snooze)
} icon: {
Image(nsImage: .icon(.actionSnooze, size: .iconRowSize(forLabelPointSize: 11.5))).renderingMode(.template)
}
.font(.system(size: 11.5))
}
.menuStyle(.button)
.buttonStyle(.plain)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CmuxNextIcons
import SwiftUI

/// Reads the colors in a tracked scope for the menu bar host.
Expand Down Expand Up @@ -40,7 +41,7 @@ struct FeedMenubarView: View {
HStack {
Text(FeedStrings.openFeed)
Spacer()
Image(systemName: "arrow.up.forward.app").font(.system(size: 11))
Icon(.appOpenExternal, size: .iconRowSize(forLabelPointSize: 11))
}
.font(.system(size: 12, weight: .medium))
.foregroundStyle(colors.secondary)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CmuxNextIcons
import SwiftUI

/// What a request asks, above its controls: the command, the diff, the
Expand Down Expand Up @@ -42,7 +43,7 @@ struct FeedPromptSummary: View {
if full && !review.checklist.isEmpty {
VStack(alignment: .leading, spacing: 3) {
ForEach(review.checklist, id: \.self) { line in
Label(line, systemImage: "circle").font(.system(size: 11.5)).foregroundStyle(colors.secondary)
FeedIconLabel(line, icon: .listBulletItem, textSize: 11.5).font(.system(size: 11.5)).foregroundStyle(colors.secondary)
}
}
}
Expand All @@ -61,7 +62,7 @@ struct FeedPromptSummary: View {

private func origin(_ host: String, _ reason: String) -> some View {
VStack(alignment: .leading, spacing: 3) {
Label(host.replacingOccurrences(of: "https://", with: ""), systemImage: "lock")
FeedIconLabel(host.replacingOccurrences(of: "https://", with: ""), icon: .securityLock, textSize: 12)
.font(.system(size: 12, weight: .medium, design: .monospaced))
.foregroundStyle(colors.primary)
Text(reason).font(.system(size: 12)).foregroundStyle(colors.secondary)
Expand Down Expand Up @@ -101,7 +102,7 @@ struct FeedDiffPreview: View {
let lines = (attachment.text ?? "").split(separator: "\n", omittingEmptySubsequences: false).map(String.init)
let shown = maxLines.map { Array(lines.prefix($0)) } ?? lines
VStack(alignment: .leading, spacing: 0) {
Label(attachment.name, systemImage: "doc.text")
FeedIconLabel(attachment.name, icon: .fileText, textSize: 11)
.font(.system(size: 11, weight: .medium))
.foregroundStyle(colors.secondary)
.padding(.horizontal, 9).padding(.vertical, 6)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import CmuxNextIcons
import SwiftUI

/// A small floating notice (a refusal, a late answer). Dismissed by the user.
Expand All @@ -10,7 +11,7 @@ struct FeedToast: View {
HStack(spacing: 8) {
Text(text).font(.system(size: 11.5)).foregroundStyle(colors.primary).lineLimit(2)
Button(action: dismiss) {
Image(systemName: "xmark").font(.system(size: 9, weight: .bold)).foregroundStyle(colors.tertiary)
Icon(.actionClose, size: .iconFloor).foregroundStyle(colors.tertiary)
}
.buttonStyle(.plain)
}
Expand All @@ -30,7 +31,8 @@ struct FeedEmptyState: View {

var body: some View {
VStack(spacing: 8) {
Image(systemName: "tray").font(.system(size: 20, weight: .light)).foregroundStyle(colors.tertiary)
// A pack drawing inks about two thirds of its box; 32 matches the 20 pt tray it replaced.
Icon(.inboxEmpty, size: 32).foregroundStyle(colors.tertiary)
Text(text).font(.system(size: 12)).foregroundStyle(colors.tertiary)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
Expand Down
Loading
Loading