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
11 changes: 9 additions & 2 deletions macapp/Sources/GoCodeApp/GoCodeApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,19 @@ struct GoCodeApp: App {
}

var body: some Scene {
WindowGroup("GoCode") {
// Empty title: the app's identity is a sidebar header row now, the
// way the reference does it. A window title here would reserve a
// second chrome band purely to repeat it.
WindowGroup("") {
AppShell(
initialWorkspace: Self.initialWorkspace,
externalBaseURL: Self.externalBaseURL,
initialPrompt: Self.environment["GOCODE_INITIAL_PROMPT"])
}
.windowToolbarStyle(.unified)
// Hidden. With the header owning its own controls there is no toolbar
// left to displace, which is what defeated this the first time. The
// system band was pure overhead: 52pt above a 40pt header, for a
// title the sidebar now carries.
.windowStyle(.hiddenTitleBar)
}
}
46 changes: 34 additions & 12 deletions macapp/Sources/GoCodeUI/AppShell.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,10 @@ private struct ProjectView: View {
var body: some View {
HStack(spacing: Spacing.none) {
ConversationRail(section: $section, project: project, onClose: onClose)
Divider()
// An explicit rule rather than Divider(): the system divider drew
// 47 where the reference draws 67, the only one of the app's four
// rules whose colour was wrong.
Rectangle().fill(Theme.columnDivider).frame(width: Spacing.hairline)
content
}
.task {
Expand All @@ -129,9 +132,14 @@ private struct ProjectView: View {
project.submit()
}
}
// The sidebar surface must also paint behind the traffic lights.
.toolbarBackground(Theme.surface, for: .windowToolbar)
.toolbarBackground(.visible, for: .windowToolbar)
// Hidden, not coloured. A visible toolbar background paints one colour
// across the whole window width, so the sidebar's grey ran over the
// content pane and produced a full-width band down to y=52 — the
// content pane did not exist at that height. The reference has no such
// band: each column paints its own colour to the top of the window, so
// hiding the toolbar background lets the rail and the content pane do
// exactly that. Both already extend through the top safe area.
.toolbarBackground(.hidden, for: .windowToolbar)
}

@ViewBuilder
Expand Down Expand Up @@ -197,12 +205,21 @@ struct RailRow: View {
.padding(.horizontal, compact ? Spacing.small : Spacing.comfortable)
.padding(.vertical, Spacing.standard)
.frame(maxWidth: compact ? nil : .infinity, alignment: .leading)
.background(
section == item ? Theme.selectedRowSurface : .clear,
in: .rect(cornerRadius: CornerRadius.control)
)
// No fill on nav rows. selectedRowSurface belongs to the active
// conversation and nothing else — painting it here too put two
// competing "this one is selected" affordances in the sidebar
// 31.5pt apart, and the reference reserves that fill for the
// conversation alone. Selection still reads, through ink weight.
// No fill here. The fill is the *conversation* list's, and the
// reference shows exactly one filled row in its whole sidebar.
// Round 8 removed this and left no selection anywhere; round 10
// restored it and produced two identical bands. A nav row is
// current, not selected — weight and ink carry that without
// competing with the conversation the user is actually in.
.foregroundStyle(
section == item ? Theme.selectedRowForeground : Theme.foregroundSecondary
// Secondary, not primary: the reference's nav labels sit at
// 222 selected or not, and spend 255 on content instead.
section == item ? Theme.foregroundSecondary : Theme.foregroundSubtle
)
.contentShape(.rect)
}
Expand All @@ -218,9 +235,14 @@ struct RailSectionHeader: View {

var body: some View {
Text(title)
.font(Typography.detail)
.foregroundStyle(Theme.foregroundTertiary)
.padding(.horizontal, Spacing.comfortable).padding(.top, Spacing.comfortable)
// Larger and dimmer than before: the reference's section label is
// bigger than this was and two rungs darker, so it reads as a
// heading that recedes. Ours was smaller and brighter, which is
// the opposite relationship. foregroundQuaternary is #747474,
// which is the reference's ink exactly.
.font(Typography.caption)
.foregroundStyle(Theme.foregroundQuaternary)
.padding(.horizontal, Spacing.section).padding(.top, Spacing.comfortable)
.padding(.bottom, Spacing.tight)
// A heading, not a control — VoiceOver should announce it once
// as a group label, not treat it as another focusable row.
Expand Down
135 changes: 109 additions & 26 deletions macapp/Sources/GoCodeUI/ChatView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ struct ChatView: View {
// simply reflows around it.
HStack(alignment: .top, spacing: Spacing.none) {
VStack(spacing: Spacing.none) {
ConversationHeader(project: project, run: run)
ConversationHeader(
project: project, run: run, showInspector: $showInspector)
TranscriptView(
items: run.transcript.items,
statusMessage: project.statusMessage,
Expand Down Expand Up @@ -54,18 +55,12 @@ struct ChatView: View {
.transition(.move(edge: .trailing).combined(with: .opacity))
}
}
.toolbar {
ToolbarItemGroup(placement: .primaryAction) {
CopyConversationButton(items: run.transcript.items)
Button {
showInspector.toggle()
} label: {
Image(systemName: showInspector ? "sidebar.trailing" : "sidebar.right")
}
.help(showInspector ? "Hide tool inspector" : "Show tool inspector")
.accessibilityLabel(showInspector ? "Hide tool inspector" : "Show tool inspector")
}
}
// The content pane paints its own colour through the top safe area,
// the way the rail does. Previously only the toolbar background
// covered this strip, in the *sidebar's* colour, so the pane began at
// y=52 and the window wore a full-width band above it.
.background(Theme.background.ignoresSafeArea(.container, edges: .top))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Extend the pane background for every section

This background extension exists only in ChatView, while AppShell now hides the toolbar background for the entire ProjectView. When the user switches to Activity, Sessions, Checkpoints, or Settings—or while startup/failure content is shown—those views do not ignore the top safe area, so the right-hand titlebar strip falls back to the window background instead of the pane surface. Apply the top-safe-area background around the shared content container rather than only Chat; the new module-wide token test currently passes without exercising any of these affected sections.

AGENTS.md reference: AGENTS.md:L31-L31

Useful? React with 👍 / 👎.


// Clicking a tool call is the reason the inspector exists; open it
// automatically on selection rather than leaving the click looking
// like it did nothing because the pane defaults to closed.
Expand Down Expand Up @@ -101,7 +96,10 @@ struct TranscriptView: View {
ScrollViewReader { proxy in
ScrollView {
ConversationColumn {
LazyVStack(alignment: .leading, spacing: Spacing.large) {
// The reference leaves 62pt between a user bubble and the
// tool row that follows it; ours left 19.5 — 3.2x tight,
// and the single most visible rhythm defect.
LazyVStack(alignment: .leading, spacing: Spacing.transcriptTurnGap) {
ForEach(TranscriptPresentation.rows(for: items)) { item in
row(for: item).id(item.id)
}
Expand Down Expand Up @@ -222,7 +220,9 @@ struct CopyMessageButton: View {
Image(systemName: copied ? "checkmark" : "doc.on.doc")
.font(Typography.caption)
.foregroundStyle(
copied ? AnyShapeStyle(.tint) : AnyShapeStyle(Theme.foregroundQuaternary)
// Only the copied state overrides; the row owns the
// resting ink so all four icons agree.
copied ? AnyShapeStyle(.tint) : AnyShapeStyle(Theme.foregroundSubtle)
)
.contentShape(.rect)
}
Expand All @@ -248,7 +248,11 @@ struct CopyConversationButton: View {
copied = false
}
} label: {
Image(systemName: copied ? "checkmark" : "doc.on.doc")
// A distinct glyph from CopyMessageButton's. Both were
// "doc.on.doc", so the two actions rendered identically and the
// row read as a duplicated button. This one copies the whole
// transcript, which is a page rather than a snippet.
Image(systemName: copied ? "checkmark" : "text.document")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a symbol available on macOS 14

text.document was introduced after macOS 14, but macapp/Package.swift:7 still declares macOS 14 as supported. On that supported OS, Image(systemName:) cannot resolve this name, leaving the whole-conversation copy toolbar action without its intended visible glyph. Use an older equivalent such as doc.text, or gate the new symbol with an availability fallback.

Useful? React with 👍 / 👎.

}
.disabled(items.isEmpty)
.help(copied ? "Copied conversation" : "Copy conversation")
Expand Down Expand Up @@ -367,8 +371,14 @@ struct MessageActions: View {
.help("Undo last turn")
.accessibilityLabel("Undo last turn")
}
// One size and one ink across all four. Different SF Symbols draw to
// different heights at the same point size, so the row measured a 62%
// spread; a fixed frame makes them a set. The colour is set once here
// and the buttons no longer override it — three of four had been
// inheriting the section-label rung instead.
.font(.system(size: IconSize.detail))
.foregroundStyle(Theme.foregroundQuaternary)
.symbolRenderingMode(.monochrome)
.foregroundStyle(Theme.foregroundSubtle)
.buttonStyle(.plain)
.frame(maxWidth: .infinity, alignment: .leading)
}
Expand Down Expand Up @@ -606,7 +616,7 @@ struct ToolRow: View {
}
Rectangle()
.fill(Theme.separator)
.frame(height: Spacing.hairline)
.frame(height: Spacing.toolRuleWeight)
}
.contentShape(.rect)
}
Expand Down Expand Up @@ -794,7 +804,10 @@ struct UsageLabel: View {
? "\(usage.totalTokens) tok · $\(String(format: "%.4f", usage.costUSD))"
: "\(usage.totalTokens) tok · cost n/a"
)
.font(Typography.caption).foregroundStyle(Theme.foregroundQuaternary)
// Body size, separated by colour rather than by scale. The reference's
// "Worked for 11s" sits 0.5pt below its own body ascender; ours sat
// 2.5pt below, de-scaled *and* dimmed where the reference only dims.
.font(Typography.body).foregroundStyle(Theme.foregroundTertiary)
}
}
}
Expand Down Expand Up @@ -867,6 +880,10 @@ struct Composer: View {
ConversationColumn {
VStack(alignment: .leading, spacing: Spacing.comfortable) {
TextField(placeholder, text: $run.draft, axis: .vertical)
// Without this the field inherits the macOS system
// default and the placeholder renders a full step
// under the reference's.
.font(Typography.body)
.textFieldStyle(.plain)
.lineLimit(1...10)
.focused($focused)
Expand All @@ -875,12 +892,19 @@ struct Composer: View {

HStack(spacing: Spacing.comfortable) {
ModelChip(project: project)
Toggle("Plan mode", isOn: $project.planMode)
.toggleStyle(.checkbox).font(Typography.caption)
.help("Restrict the agent to writing a plan file until you approve it")
// A chip, not a checkbox. The reference's composer
// uses icon+label chips and contains no checkbox
// anywhere; a square AppKit control in a chat composer
// was the most out-of-family element on the screen.
ComposerChip(
title: "Plan mode",
icon: "list.bullet.rectangle",
isOn: project.planMode
) { project.planMode.toggle() }
.help("Restrict the agent to writing a plan file until you approve it")
Spacer()
Button("New") { project.newConversation() }
.buttonStyle(.plain).font(Typography.caption).foregroundStyle(
.buttonStyle(.plain).font(Typography.body).foregroundStyle(
Theme.foregroundTertiary)

Button(action: { send(action) }) {
Expand All @@ -900,8 +924,21 @@ struct Composer: View {
action.isSteer ? "Steer the running task" : "Send message")
}
}
.padding(.horizontal, Spacing.large).padding(.vertical, Spacing.inset)
// The reference insets its composer controls 9pt from the
// right edge and 9.5 from the bottom; ours sat at 18.5 and 20,
// twice as far in, inside a shorter composer.
.padding(.horizontal, Spacing.comfortable).padding(.vertical, Spacing.inset)
.background(Theme.surfaceElevated, in: .rect(cornerRadius: CornerRadius.composer))
// The reference's composer carries a hairline on all four
// edges; ours sat as flat fill straight against the page.
// Theme.rule, not Theme.separator: at 43 against a 45 fill the
// stroke was darker than what it bordered and indistinguishable
// from the shape's own antialiased edge. A border has to be
// lighter than its fill to read as one.
.overlay(
RoundedRectangle(cornerRadius: CornerRadius.composer, style: .continuous)
.strokeBorder(Theme.rule, lineWidth: Spacing.hairline)
)
}
// No minimum height: the card hugs its content and grows with a
// multi-line draft. A fixed floor was measured against the old,
Expand Down Expand Up @@ -981,6 +1018,45 @@ enum ComposerAction: Equatable {
}
}

/// An icon+label chip for a composer toggle.
///
/// The reference's composer expresses its options this way; a square AppKit
/// checkbox in a chat composer was the most out-of-family control on the
/// screen. Selection reads through ink and a fill rather than a tick, so the
/// control keeps the composer's vocabulary instead of importing a form's.
struct ComposerChip: View {
let title: String
let icon: String
let isOn: Bool
let toggle: () -> Void

var body: some View {
Button(action: toggle) {
// Explicit icon+label rather than Label: Label sizes its symbol
// from the font's own metrics, which made this chip's icon 48%
// wider than the model chip's beside it and its label gap 85%
// larger. Fixing both to tokens puts the two chips on one size.
HStack(spacing: Spacing.chipLabelGap) {
Image(systemName: icon)
.font(.system(size: IconSize.chip))
.frame(width: IconSize.chip, height: IconSize.chip)
Text(title)
}
.font(Typography.body)
.foregroundStyle(isOn ? Theme.foreground : Theme.foregroundTertiary)
.padding(.horizontal, Spacing.small)
.padding(.vertical, Spacing.tight)
.background(
isOn ? Theme.selectedRowSurface : .clear,
in: .rect(cornerRadius: CornerRadius.control)
)
.contentShape(.rect)
}
.buttonStyle(.plain)
.accessibilityAddTraits(isOn ? [.isSelected] : [])
}
}

struct ModelChip: View {
@Bindable var project: ProjectSession

Expand All @@ -1007,8 +1083,15 @@ struct ModelChip: View {
Text(hiddenSummary).font(Typography.caption)
}
} label: {
Label(project.selectedModel ?? "Server default", systemImage: "cpu")
.font(Typography.caption)
// Same construction as ComposerChip so the two sit as a set:
// one icon size, one label gap, one baseline.
HStack(spacing: Spacing.chipLabelGap) {
Image(systemName: "cpu")
.font(.system(size: IconSize.chip))
.frame(width: IconSize.chip, height: IconSize.chip)
Text(project.selectedModel ?? "Server default")
}
.font(Typography.body)
}
.menuStyle(.borderlessButton)
.fixedSize()
Expand Down
Loading
Loading