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 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