codeburn/mac/Sources/CodeBurnMenubar/Views/WorkflowSection.swift
Resham Joshi 8097052f67
menubar: add Workflow strip after the Models section (#785)
Compact one-row strip: correction rate with count, median time to first
edit, and the top reworked file, plus one coaching note derived locally
with the same thresholds and copy as the CLI's workflow-insights
buildCoachingNotes. Decodes two new optional payload blocks
(current.workflow and current.topReworkedFiles), so payloads from older
CLIs still parse. The section hides entirely when there is no signal and
individual stats never render as zero placeholders. It reads the store's
current payload, so it follows the selected agent tab automatically.

Adds unit tests for the duration formatter, note selection, model
derivation, and payload decoding.

Co-authored-by: reviewer <review@local>
2026-07-20 13:55:55 -07:00

111 lines
3.8 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
/// Compact workflow-intelligence strip: one row of up to three stats plus one
/// coaching note. Reads whatever payload the store currently holds, so it
/// follows the selected agent tab automatically. Renders nothing when the
/// payload carries no workflow signal (older CLIs, or a period with nothing
/// measurable), so it never shows zero placeholders.
struct WorkflowSection: View {
@Environment(AppStore.self) private var store
var body: some View {
let model = WorkflowStripModel(
workflow: store.payload.current.workflow,
topReworkedFiles: store.payload.current.topReworkedFiles
)
if !model.isEmpty {
// Own the leading divider so the section slots into the popover's
// divider rhythm and leaves no doubled line when it's hidden.
VStack(spacing: 0) {
Divider().opacity(0.5)
VStack(alignment: .leading, spacing: 8) {
SectionCaption(text: "Workflow")
WorkflowStatRow(model: model)
if let note = model.note {
Text(note)
.font(.system(size: 11))
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.tail)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.padding(.horizontal, 14)
.padding(.vertical, 11)
}
}
}
}
private enum WorkflowStat: Identifiable {
case labeled(id: String, label: String, value: String)
case reworked(name: String, sessions: Int)
var id: String {
switch self {
case .labeled(let id, _, _): return id
case .reworked: return "reworked"
}
}
}
private struct WorkflowStatRow: View {
let model: WorkflowStripModel
private var stats: [WorkflowStat] {
var s: [WorkflowStat] = []
if let corrections = model.corrections {
s.append(.labeled(id: "corrections", label: "Corrections", value: corrections))
}
if let firstEdit = model.firstEdit {
s.append(.labeled(id: "firstEdit", label: "First edit", value: firstEdit))
}
if let name = model.reworkedName, let sessions = model.reworkedSessions {
s.append(.reworked(name: name, sessions: sessions))
}
return s
}
var body: some View {
HStack(spacing: 7) {
ForEach(Array(stats.enumerated()), id: \.element.id) { index, stat in
if index > 0 {
Text("·")
.font(.system(size: 11))
.foregroundStyle(.tertiary)
}
WorkflowStatCell(stat: stat)
}
Spacer(minLength: 0)
}
}
}
private struct WorkflowStatCell: View {
let stat: WorkflowStat
var body: some View {
HStack(spacing: 4) {
switch stat {
case .labeled(_, let label, let value):
Text(label)
.font(.system(size: 11))
.foregroundStyle(.tertiary)
Text(value)
.font(.codeMono(size: 12, weight: .medium))
.foregroundStyle(.primary)
.tracking(-0.2)
case .reworked(let name, let sessions):
Text(name)
.font(.system(size: 12, weight: .medium))
.foregroundStyle(.primary)
.lineLimit(1)
.truncationMode(.middle)
Text("×\(sessions)")
.font(.codeMono(size: 11, weight: .medium))
.foregroundStyle(.secondary)
.tracking(-0.2)
}
}
}
}