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
12 changes: 6 additions & 6 deletions Sources/UI/MenuBar/MenuBarActionRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -223,13 +223,13 @@ final class MenuBarActionRowView: NSControl {
private func updateTypography() {
switch rowSize {
case .primary:
titleLabel.font = NSFont.systemFont(ofSize: 12.5, weight: .medium)
detailLabel.font = NSFont.systemFont(ofSize: 10)
trailingLabel.font = NSFont.systemFont(ofSize: 10.5, weight: .medium)
titleLabel.font = MenuTokens.Font.rowTitlePrimary
detailLabel.font = MenuTokens.Font.rowDetail
trailingLabel.font = MenuTokens.Font.rowTrailingPrimary
case .utility:
titleLabel.font = NSFont.systemFont(ofSize: 12.5, weight: .regular)
detailLabel.font = NSFont.systemFont(ofSize: 10)
trailingLabel.font = NSFont.systemFont(ofSize: 10, weight: .medium)
titleLabel.font = MenuTokens.Font.rowTitleUtility
detailLabel.font = MenuTokens.Font.rowDetail
trailingLabel.font = MenuTokens.Font.rowTrailingUtility
}
}

Expand Down
8 changes: 4 additions & 4 deletions Sources/UI/MenuBar/MenuBarHeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,15 @@ final class MenuBarHeaderView: NSView {
override var isFlipped: Bool { true }

private func setupViews() {
titleLabel.font = NSFont.systemFont(ofSize: 15.5, weight: .semibold)
titleLabel.font = MenuTokens.Font.headerTitle
titleLabel.textColor = MenuTokens.textPrimaryNS
addSubview(titleLabel)

statusDot.wantsLayer = true
statusDot.layer?.cornerRadius = MenuTokens.statusDotSize / 2
addSubview(statusDot)

statusLabel.font = NSFont.systemFont(ofSize: 11.5, weight: .medium)
statusLabel.font = MenuTokens.Font.headerStatus
statusLabel.textColor = MenuTokens.textSecondaryNS
addSubview(statusLabel)

Expand All @@ -53,7 +53,7 @@ final class MenuBarHeaderView: NSView {
progressBar.maxValue = 1
addSubview(progressBar)

detailLabel.font = NSFont.systemFont(ofSize: 10)
detailLabel.font = MenuTokens.Font.headerDetail
detailLabel.textColor = MenuTokens.textSecondaryNS
detailLabel.maximumNumberOfLines = 2
addSubview(detailLabel)
Expand All @@ -64,7 +64,7 @@ final class MenuBarHeaderView: NSView {
}
addSubview(warningIconView)

warningLabel.font = NSFont.systemFont(ofSize: 10)
warningLabel.font = MenuTokens.Font.headerDetail
warningLabel.textColor = MenuTokens.textSecondaryNS
warningLabel.maximumNumberOfLines = 2
addSubview(warningLabel)
Expand Down
16 changes: 16 additions & 0 deletions Sources/UI/MenuBar/MenuTokens.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,22 @@ enum MenuTokens {
static let compactActionRowHeight: CGFloat = 42
static let utilityActionRowHeight: CGFloat = 40
static let statusDotSize: CGFloat = 6

// Type — the popover's SF Pro roles. See docs/DESIGN_TOKENS.md. The menubar
// is the app's compact density tier, so its sizes sit a half-point below the
// main-window scale; they live here as named roles so views never reach for
// a raw `NSFont.systemFont(ofSize:)` literal. Changing a menubar font means
// changing it once, here.
enum Font {
static let rowTitlePrimary = NSFont.systemFont(ofSize: 12.5, weight: .medium)
static let rowTitleUtility = NSFont.systemFont(ofSize: 12.5, weight: .regular)
static let rowDetail = NSFont.systemFont(ofSize: 10, weight: .regular)
static let rowTrailingPrimary = NSFont.systemFont(ofSize: 10.5, weight: .medium)
static let rowTrailingUtility = NSFont.systemFont(ofSize: 10, weight: .medium)
static let headerTitle = NSFont.systemFont(ofSize: 15.5, weight: .semibold)
static let headerStatus = NSFont.systemFont(ofSize: 11.5, weight: .medium)
static let headerDetail = NSFont.systemFont(ofSize: 10, weight: .regular)
}
}

extension NSView {
Expand Down
83 changes: 65 additions & 18 deletions Sources/UI/Settings/AgentConnectionSettingsPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,22 @@ struct AgentConnectionSettingsPage: View {
@State private var detectedAgents: Set<AgentMCPAgent> = []
@State private var connectedAgents: Set<AgentMCPAgent> = []
@State private var rowPhases: [AgentMCPAgent: RowPhase] = [:]
// Raw error text for the connect-row failure, kept out of the user-visible
// message and offered behind "Copy Details".
@State private var rowFailureDetails: [AgentMCPAgent: String] = [:]
@State private var configRepairNotices: [AgentMCPAgent: String] = [:]
@State private var claudeDesktopSelfTest: TranscriptedMCPSelfTest?
@State private var copiedLocalAgentPrompt = false
@State private var copiedClaudeDesktopConfig = false
@State private var copiedFolderPaths = false
@State private var openedCodexInboxSetup = false
@State private var codexInboxSetupError: String?
@State private var codexInboxSetupErrorDetails: String?
@State private var openedLiveMeetingCodexSetup = false
@State private var openedLiveMeetingPreview = false
@State private var copiedLiveMeetingCoworkSetup = false
@State private var liveMeetingCodexSetupError: String?
@State private var liveMeetingCodexSetupErrorDetails: String?
@State private var showAdvancedAgentSetup = false
@AppStorage(LiveMeetingCodexPreferences.enabledKey) private var liveMeetingCodexEnabled = LiveMeetingCodexPreferences.defaultEnabled

Expand Down Expand Up @@ -121,10 +126,11 @@ struct AgentConnectionSettingsPage: View {
}

if case .failed(let message) = phase {
Label(message, systemImage: "exclamationmark.triangle.fill")
.font(.caption)
.foregroundStyle(.orange)
.fixedSize(horizontal: false, vertical: true)
failureNotice(
message: message,
details: rowFailureDetails[agent],
detailsAutomationIdentifier: "transcripted.settings.agent.connect-error-details.\(agent.rawValue)"
)
}

if let configRepairNotice = configRepairNotices[agent] {
Expand Down Expand Up @@ -247,10 +253,11 @@ struct AgentConnectionSettingsPage: View {
}

if let liveMeetingCodexSetupError {
Label(liveMeetingCodexSetupError, systemImage: "exclamationmark.triangle.fill")
.font(.caption)
.foregroundStyle(.orange)
.fixedSize(horizontal: false, vertical: true)
failureNotice(
message: liveMeetingCodexSetupError,
details: liveMeetingCodexSetupErrorDetails,
detailsAutomationIdentifier: "transcripted.settings.agent.live-meetings.error-details"
)
}
}
}
Expand Down Expand Up @@ -335,10 +342,11 @@ struct AgentConnectionSettingsPage: View {
}

if let codexInboxSetupError {
Label(codexInboxSetupError, systemImage: "exclamationmark.triangle.fill")
.font(.caption)
.foregroundStyle(.orange)
.fixedSize(horizontal: false, vertical: true)
failureNotice(
message: codexInboxSetupError,
details: codexInboxSetupErrorDetails,
detailsAutomationIdentifier: "transcripted.settings.agent.codex-inbox.error-details"
)
}
}
}
Expand Down Expand Up @@ -433,11 +441,39 @@ struct AgentConnectionSettingsPage: View {
}
}

// MARK: - Failure notice

/// Plain-words failure line plus a subtle "Copy Details" reveal for the raw
/// error. The triggering control (Connect / Set up / Open) is the retry.
@ViewBuilder
private func failureNotice(
message: String,
details: String?,
detailsAutomationIdentifier: String
) -> some View {
VStack(alignment: .leading, spacing: 4) {
Label(message, systemImage: "exclamationmark.triangle.fill")
.font(.caption)
.foregroundStyle(.orange)
.fixedSize(horizontal: false, vertical: true)

if let details {
Button(AgentSetupFailureCopy.detailsTitle) {
copyText(details)
}
.buttonStyle(.link)
.font(.caption)
.accessibilityIdentifier(detailsAutomationIdentifier)
}
}
}

// MARK: - Connect

private func connect(_ agent: AgentMCPAgent) {
guard rowPhases[agent] != .connecting else { return }
rowPhases[agent] = .connecting
rowFailureDetails[agent] = nil
configRepairNotices[agent] = nil
if agent == .claudeDesktop {
claudeDesktopSelfTest = nil
Expand Down Expand Up @@ -470,7 +506,8 @@ struct AgentConnectionSettingsPage: View {
}
trackConnect(agent, priorStatus: priorStatus, result: .success)
} catch {
rowPhases[agent] = .failed(error.localizedDescription)
rowPhases[agent] = .failed(AgentSetupFailureCopy.connect(agentName: agent.displayName))
rowFailureDetails[agent] = error.localizedDescription
trackConnect(agent, priorStatus: priorStatus, result: .failed)
}
}
Expand Down Expand Up @@ -511,6 +548,7 @@ struct AgentConnectionSettingsPage: View {

private func setupCodexInbox() {
codexInboxSetupError = nil
codexInboxSetupErrorDetails = nil

do {
let inboxURL = try AgentConnectionGuide.ensureCodexInboxFolder()
Expand Down Expand Up @@ -558,7 +596,8 @@ struct AgentConnectionSettingsPage: View {
)
}
} catch {
codexInboxSetupError = "Could not set up Codex Inbox: \(error.localizedDescription)"
codexInboxSetupError = AgentSetupFailureCopy.codexInbox
codexInboxSetupErrorDetails = error.localizedDescription
ActivationTelemetry.trackAgentSetupCTA(
setupKind: .codexInbox,
agentTarget: .codex,
Expand All @@ -572,6 +611,7 @@ struct AgentConnectionSettingsPage: View {

private func setupLiveMeetingCodex() {
liveMeetingCodexSetupError = nil
liveMeetingCodexSetupErrorDetails = nil

do {
liveMeetingCodexEnabled = true
Expand Down Expand Up @@ -622,7 +662,8 @@ struct AgentConnectionSettingsPage: View {
}
} catch {
disableLiveMeetingSidecarAfterFailure()
liveMeetingCodexSetupError = "Could not set up Live Meetings: \(error.localizedDescription)"
liveMeetingCodexSetupError = AgentSetupFailureCopy.liveMeetings
liveMeetingCodexSetupErrorDetails = error.localizedDescription
ActivationTelemetry.trackAgentSetupCTA(
setupKind: .liveSidecar,
agentTarget: .codex,
Expand All @@ -634,17 +675,20 @@ struct AgentConnectionSettingsPage: View {

private func prepareLiveMeetingSidecarWorkspace() {
liveMeetingCodexSetupError = nil
liveMeetingCodexSetupErrorDetails = nil

do {
_ = try prepareLiveMeetingSidecarWorkspaceForUse()
} catch {
disableLiveMeetingSidecarAfterFailure()
liveMeetingCodexSetupError = "Could not prepare Live Meetings: \(error.localizedDescription)"
liveMeetingCodexSetupError = AgentSetupFailureCopy.liveMeetingsPrepare
liveMeetingCodexSetupErrorDetails = error.localizedDescription
}
}

private func copyLiveMeetingCoworkSetup() {
liveMeetingCodexSetupError = nil
liveMeetingCodexSetupErrorDetails = nil

do {
liveMeetingCodexEnabled = true
Expand All @@ -666,7 +710,8 @@ struct AgentConnectionSettingsPage: View {
showCopiedFeedback($copiedLiveMeetingCoworkSetup)
} catch {
disableLiveMeetingSidecarAfterFailure()
liveMeetingCodexSetupError = "Could not set up Live Meetings: \(error.localizedDescription)"
liveMeetingCodexSetupError = AgentSetupFailureCopy.liveMeetings
liveMeetingCodexSetupErrorDetails = error.localizedDescription
ActivationTelemetry.trackAgentSetupCTA(
setupKind: .liveSidecar,
agentTarget: .cowork,
Expand All @@ -678,6 +723,7 @@ struct AgentConnectionSettingsPage: View {

private func openLiveMeetingPreview() {
liveMeetingCodexSetupError = nil
liveMeetingCodexSetupErrorDetails = nil

do {
liveMeetingCodexEnabled = true
Expand Down Expand Up @@ -718,7 +764,8 @@ struct AgentConnectionSettingsPage: View {
}
} catch {
disableLiveMeetingSidecarAfterFailure()
liveMeetingCodexSetupError = "Could not open Live View: \(error.localizedDescription)"
liveMeetingCodexSetupError = AgentSetupFailureCopy.liveView
liveMeetingCodexSetupErrorDetails = error.localizedDescription
ActivationTelemetry.trackAgentSetupCTA(
setupKind: .livePreview,
agentTarget: .localAgent,
Expand Down
33 changes: 33 additions & 0 deletions Sources/UI/Settings/AgentSetupFailureCopy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import Foundation

/// Plain-words copy for Agent-page setup failures.
///
/// Foundation-pure so the strings can be pinned by fast tests and can never
/// regress to a raw `error.localizedDescription` dump the way these sites did
/// before. Each message says what happened and the one thing to try; the raw
/// error is offered separately behind "Copy Details", never shown inline. Plain
/// words, no jargon, no exclamation marks — the repo voice convention.
///
/// This mirrors `HomeActionFailureCopy` (the meeting/Home path's template):
/// classify the failure into a context, return `{plain message}`, and keep the
/// raw string for a reveal affordance while the triggering control provides retry.
enum AgentSetupFailureCopy {
/// Title for the reveal affordance that copies the raw error to the clipboard.
static let detailsTitle = "Copy Details"

static func connect(agentName: String) -> String {
"Transcripted couldn't connect \(agentName). Check that it's installed and not already running, then try Connect again."
}

static let liveMeetings =
"Transcripted couldn't set up Live Meetings. Check that Codex is installed, then try again."

static let liveMeetingsPrepare =
"Transcripted couldn't prepare Live Meetings. Try turning it on again."

static let liveView =
"Transcripted couldn't open the live view. Try turning Live Meetings on again."

static let codexInbox =
"Transcripted couldn't set up the Codex inbox. Check that Codex is installed and you have free disk space, then try again."
}
5 changes: 4 additions & 1 deletion Sources/UI/Settings/PermissionsOnboardingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,10 @@ struct PermissionsOnboardingView: View {
result: .success
)
} catch {
claudeDesktopConnectPhase = .failed(error.localizedDescription)
// Plain words on the first-run card instead of a raw NSError dump.
// The button flips to "Try again" (the retry); the raw error still
// goes to telemetry above, not onto the user's onboarding screen.
claudeDesktopConnectPhase = .failed(AgentSetupFailureCopy.connect(agentName: "Claude Desktop"))
ActivationTelemetry.trackAgentSetupCTA(
setupKind: .claudeDesktop,
agentTarget: .claudeDesktop,
Expand Down
32 changes: 32 additions & 0 deletions Sources/UI/Settings/SettingsActionFailureCopy.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Foundation

/// Plain-words copy for Settings-page action failures (beta setup, model-cache
/// cleanup, launch-at-login, capture-library migration).
///
/// Foundation-pure so the strings can be pinned by fast tests and can never
/// regress to a raw `error.localizedDescription` dump. Each message says what
/// happened and the one thing to try; the raw error is offered separately behind
/// "Copy Details", never shown inline. Plain words, no jargon, no exclamation
/// marks — the repo voice convention. Mirrors `HomeActionFailureCopy` and
/// `AgentSetupFailureCopy`.
enum SettingsActionFailureCopy {
/// Title for the reveal affordance that copies the raw error to the clipboard.
static let detailsTitle = "Copy Details"

static let betaLiveSidecar =
"Transcripted couldn't prepare the live meeting sidecar. Try turning it on again."

static func localSummary(providerTitle: String) -> String {
"\(providerTitle) setup didn't finish. Check your connection and free disk space, then try again."
}

static let modelCacheRemoval =
"Transcripted couldn't remove those model files. Check that no capture is running, then try again."

static let launchAtLogin =
"Transcripted couldn't change the launch-at-login setting. Try again."

static func captureLibraryMigration(currentLibraryPath: String) -> String {
"The copy stopped before it finished. Your captures are still in \(currentLibraryPath) and the library was not switched. Try again."
}
}
Loading
Loading