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
2 changes: 1 addition & 1 deletion agents/swiftui.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ shell/mac/
│ │ ├── ContentView/ RepoContentView (+Sidebar, +StatusBar, +Toolbar, +CommandPalette,
│ │ │ +Presentation, +Sheets)
│ │ ├── DAG/ DAGView (+BookmarkDrag, +ContextMenu, +RebaseDrag), DAGViewModel,
│ │ │ DAGLayout, DAGRow (+GraphColumn, +Refs), DAGRowViewModel,
│ │ │ DAGLayout, DAGGraphColumn, DAGRow (+Refs), DAGRowViewModel,
│ │ │ rebase and bookmark-drag models and gesture policies
│ │ ├── Bookmarks/ BookmarkManagerView, BookmarkManagerRow, BookmarkPicker
│ │ ├── StackedPr/ StackedPrPanel (+Results), StackedPrNamer
Expand Down
7 changes: 3 additions & 4 deletions shell/mac/Sources/JayJay/Detail/DescriptionPreview.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@ struct DescriptionPreview: View {
.accessibilityIdentifier(AID.Detail.descriptionTitle)
if let onEdit {
Button(action: onEdit) {
Label {
Text("Edit").font(.system(size: baseFontSize))
} icon: {
HStack(alignment: .firstTextBaseline, spacing: 4) {
Image(systemName: "pencil")
.font(.system(size: 14 * baseFontSize / 12, weight: .semibold))
.font(.system(size: baseFontSize, weight: .semibold))
Text("Edit").font(.system(size: baseFontSize))
}
}
.buttonStyle(.plain)
Expand Down
134 changes: 68 additions & 66 deletions shell/mac/Sources/JayJay/Detail/FileList/FileRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,84 +46,86 @@ struct FileRow: View {
.accessibilityLabel(reviewAccessibilityLabel)
}

if hasConflict {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.red)
.jayjayFont(11)
} else {
Circle()
.fill(color)
.frame(width: 6, height: 6)
}
HStack(alignment: .firstTextBaseline, spacing: 8) {
if hasConflict {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundStyle(.red)
.jayjayFont(11)
} else {
Circle()
.fill(color)
.frame(width: 6, height: 6)
}

VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 6) {
Text(URL(fileURLWithPath: hunk.path).lastPathComponent)
.jayjayFont(12, weight: .medium)
.lineLimit(1)
.opacity(showsReviewedStyle ? 0.5 : 1)
if hunk.isSubmodulePlaceholder {
Text("Submodule")
.jayjayFont(9, weight: .semibold)
.foregroundStyle(.secondary)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.secondary.opacity(0.12), in: Capsule())
} else if hunk.isGitLfsPlaceholder {
Text("LFS")
.jayjayFont(9, weight: .semibold)
.foregroundStyle(.secondary)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.secondary.opacity(0.12), in: Capsule())
}
if showsAgentBadge {
Text("Agent")
.jayjayFont(9, weight: .semibold)
.foregroundStyle(.secondary)
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 6) {
Text(URL(fileURLWithPath: hunk.path).lastPathComponent)
.jayjayFont(12, weight: .medium)
.lineLimit(1)
.opacity(showsReviewedStyle ? 0.5 : 1)
if hunk.isSubmodulePlaceholder {
Text("Submodule")
.jayjayFont(9, weight: .semibold)
.foregroundStyle(.secondary)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.secondary.opacity(0.12), in: Capsule())
} else if hunk.isGitLfsPlaceholder {
Text("LFS")
.jayjayFont(9, weight: .semibold)
.foregroundStyle(.secondary)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.secondary.opacity(0.12), in: Capsule())
}
if showsAgentBadge {
Text("Agent")
.jayjayFont(9, weight: .semibold)
.foregroundStyle(.secondary)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.secondary.opacity(0.12), in: Capsule())
.help("Includes changes marked by an agent")
.accessibilityIdentifier(AID.FileList.agentReviewed(hunk.path))
}
if noteCount > 0 {
HStack(alignment: .firstTextBaseline, spacing: 3) {
Image(systemName: "note.text")
.jayjayFont(8)
Text("\(noteCount)")
.jayjayFont(9, weight: .semibold)
.accessibilityIdentifier(AID.ReviewNote.fileCount(path: hunk.path, count: noteCount))
}
.foregroundStyle(.orange)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.secondary.opacity(0.12), in: Capsule())
.help("Includes changes marked by an agent")
.accessibilityIdentifier(AID.FileList.agentReviewed(hunk.path))
}
if noteCount > 0 {
HStack(spacing: 3) {
Image(systemName: "note.text")
.jayjayFont(8)
Text("\(noteCount)")
.jayjayFont(9, weight: .semibold)
.accessibilityIdentifier(AID.ReviewNote.fileCount(path: hunk.path, count: noteCount))
.background(Color.orange.opacity(0.12), in: Capsule())
.help(noteCount.reviewNoteCountLabel)
}
.foregroundStyle(.orange)
.padding(.horizontal, 6)
.padding(.vertical, 2)
.background(Color.orange.opacity(0.12), in: Capsule())
.help(noteCount.reviewNoteCountLabel)
}
}

if hunk.hunkType == .renamed, let oldPath = hunk.oldPath {
HStack(spacing: 3) {
Text(oldPath)
.strikethrough()
Image(systemName: "arrow.right")
.imageScale(.small)
Text(hunk.path)
}
.jayjayFont(9, design: .monospaced)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
} else {
Text(hunk.path)
if hunk.hunkType == .renamed, let oldPath = hunk.oldPath {
HStack(alignment: .firstTextBaseline, spacing: 3) {
Text(oldPath)
.strikethrough()
Image(systemName: "arrow.right")
.imageScale(.small)
Text(hunk.path)
}
.jayjayFont(9, design: .monospaced)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
} else {
Text(hunk.path)
.jayjayFont(9, design: .monospaced)
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
}
}
Spacer(minLength: 0)
}
Spacer(minLength: 0)
}
.padding(.horizontal, 6)
.padding(.vertical, 6)
Expand Down
3 changes: 2 additions & 1 deletion shell/mac/Sources/JayJay/Repo/Bookmarks/BookmarkPicker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,14 @@ struct BookmarkPicker: View {

var body: some View {
Button(action: togglePanel) {
HStack(spacing: 4) {
HStack(alignment: .firstTextBaseline, spacing: 4) {
Image(systemName: "arrow.triangle.branch")
.imageScale(.small)
Text(bookmarkLabel)
.jayjayFont(12, weight: .medium)
.lineLimit(1)
}
.padding(.horizontal, 8)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
Expand Down
147 changes: 147 additions & 0 deletions shell/mac/Sources/JayJay/Repo/DAG/DAGGraphColumn.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import JayJayCore
import SwiftUI

struct DAGGraphColumn: View {
let viewModel: DAGRowViewModel
let nodeCenterY: CGFloat

var body: some View {
let myLane = viewModel.layout.lane(for: viewModel.change.commitId.id)
let myDisplayLane = viewModel.layout.displayLane(for: myLane)
let myX = viewModel.layout.xPosition(forDisplayLane: myDisplayLane)
let hasOverflow = viewModel.layout.hasLaneOverflow(at: viewModel.index)
let overflowDisplayLane = viewModel.layout.displayLaneCount() - 1
let passThroughDisplayLanes = Set(
viewModel.layout.passThroughLaneIndices(at: viewModel.index).map {
viewModel.layout.displayLane(for: $0)
}
).sorted()
let hasVisibleParent = viewModel.entry.edges.contains { $0.edgeType != .missing }

return Canvas { ctx, size in
let height = size.height
let lineColor = Color.secondary.opacity(0.2)
let edgeColor = Color.secondary.opacity(0.3)
let laneStroke: (Int) -> StrokeStyle = { displayLane in
hasOverflow && displayLane == overflowDisplayLane
? dagOverflowStroke
: dagSolidStroke
}

for displayLane in passThroughDisplayLanes where displayLane != myDisplayLane {
let laneX = viewModel.layout.xPosition(forDisplayLane: displayLane)
let path = Path { p in
p.move(to: CGPoint(x: laneX, y: 0))
p.addLine(to: CGPoint(x: laneX, y: height))
}
ctx.stroke(path, with: .color(lineColor), style: laneStroke(displayLane))
}

// Top stub: connect down from the row above when the lane continues.
if viewModel.index > 0 {
let prevActive = viewModel.layout.activeLaneIndices(at: viewModel.index - 1)
if prevActive.contains(myLane) {
let path = Path { p in
p.move(to: CGPoint(x: myX, y: 0))
p.addLine(to: CGPoint(x: myX, y: nodeCenterY - nodeRadius))
}
ctx.stroke(path, with: .color(lineColor), style: laneStroke(myDisplayLane))
}
}

for edge in viewModel.entry.edges {
if edge.edgeType == .missing {
continue
}
let targetLane = viewModel.layout.lane(for: edge.target)
let targetDisplayLane = viewModel.layout.displayLane(for: targetLane)
let targetX = viewModel.layout.xPosition(forDisplayLane: targetDisplayLane)

let path = Path { p in
p.move(to: CGPoint(x: myX, y: nodeCenterY + nodeRadius))
if targetDisplayLane == myDisplayLane {
p.addLine(to: CGPoint(x: myX, y: height))
} else {
let midY = nodeCenterY + nodeRadius + (height - nodeCenterY - nodeRadius) * 0.4
p.addLine(to: CGPoint(x: myX, y: midY))
p.addQuadCurve(
to: CGPoint(x: targetX, y: height),
control: CGPoint(x: targetX, y: midY)
)
}
}
let style: StrokeStyle = if edge.edgeType == .indirect {
dagIndirectEdgeStroke
} else if hasOverflow, myDisplayLane == overflowDisplayLane || targetDisplayLane == overflowDisplayLane {
dagOverflowStroke
} else {
dagSolidStroke
}
ctx.stroke(path, with: .color(edgeColor), style: style)
}

if viewModel.layout.hasMissingAncestry(at: viewModel.index) {
let terminalX = myX + (hasVisibleParent ? laneWidth * 0.35 : 0)
let startY = nodeCenterY + nodeRadius
// End the side cap before the parent curves fan out at 40% of the remaining height.
let endY = startY + (height - startY) * (hasVisibleParent ? 0.25 : 0.55)
let stem = Path { path in
path.move(to: CGPoint(x: myX, y: startY))
path.addLine(to: CGPoint(x: terminalX, y: endY))
}
let cap = Path { path in
path.move(to: CGPoint(x: terminalX - 2, y: endY))
path.addLine(to: CGPoint(x: terminalX + 2, y: endY))
}
ctx.stroke(stem, with: .color(edgeColor), style: dagMissingEdgeStroke)
ctx.stroke(cap, with: .color(edgeColor), style: dagSolidStroke)
}

let style = DAGNodeStyle.resolve(change: viewModel.change)
let nodeRect = CGRect(
x: myX - style.radius,
y: nodeCenterY - style.radius,
width: style.radius * 2,
height: style.radius * 2
)
let nodePath = style.path(in: nodeRect)
switch style.fill {
case let .filled(color):
ctx.fill(nodePath, with: .color(color))
case let .outlined(color, lineWidth):
ctx.stroke(nodePath, with: .color(color), style: StrokeStyle(lineWidth: lineWidth))
}

if viewModel.isRebaseCandidate {
ctx.stroke(
nodePath,
with: .color(.accentColor.opacity(viewModel.isRebaseHoverTarget ? 1 : 0.55)),
style: StrokeStyle(lineWidth: viewModel.isRebaseHoverTarget ? 2.5 : 1.4)
)
if viewModel.isRebaseHoverTarget {
let ringRect = nodeRect.insetBy(dx: -4, dy: -4)
ctx.stroke(
style.path(in: ringRect),
with: .color(.accentColor.opacity(0.45)),
style: StrokeStyle(lineWidth: 2)
)
}
} else if viewModel.isRebaseSource {
ctx.stroke(
nodePath,
with: .color(.accentColor.opacity(0.75)),
style: StrokeStyle(lineWidth: 2)
)
if viewModel.isRebaseArmed {
let ringRect = nodeRect.insetBy(dx: -3, dy: -3)
ctx.stroke(
style.path(in: ringRect),
with: .color(.accentColor.opacity(0.35)),
style: StrokeStyle(lineWidth: 1.5, dash: [3, 3])
)
}
}
}
.clipped()
}
}
3 changes: 2 additions & 1 deletion shell/mac/Sources/JayJay/Repo/DAG/DAGLayout.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ let laneWidth: CGFloat = 16
let nodeRadius: CGFloat = 4
let dagRowLeadingPadding: CGFloat = 4
let dagRowVerticalPadding: CGFloat = 8
let dagNodeCenterY: CGFloat = 12
/// Used only until the refs row reports its bounds; the node then centers on that row, so it tracks the user font size.
let dagFallbackNodeCenterY: CGFloat = 12
let dagCompactVisibleLanes = 4
let dagOverflowStroke = StrokeStyle(lineWidth: 1, dash: [10, 4, 10, 12])
let dagIndirectEdgeStroke = StrokeStyle(lineWidth: 1, dash: [3, 3])
Expand Down
Loading
Loading