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
16 changes: 7 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -332,8 +332,8 @@ jobs:

# Build the Debug app + UI test bundle once and share it with every job that
# only needs to *run* it (socket-integration-tests, tests-build-and-lag,
# ui-regressions). Those three jobs used to each pay their own ~7 min
# "Build app" step for an identical build; this collapses that to one.
# ui-regressions). Building once here saves each of those three jobs
# its own ~7 min "Build app" step for an identical build.
# unit-tests keeps building its own artifacts: it uses the programa-unit
# scheme/test host, a different target from the programa app scheme built
# here, so its output isn't the same product.
Expand Down Expand Up @@ -1175,13 +1175,11 @@ jobs:
DISPLAY_DONE="/tmp/programa-ui-test-display-${TOKEN}.done"
HELPER_LOG="/tmp/programa-ui-test-display-${TOKEN}-helper.log"

# This churn test used to spin up its own second CGVirtualDisplay
# alongside the persistent one from the previous step. A headless CI
# session only reliably holds one live CGVirtualDisplay at a time —
# the second instantiation was reproducibly failing with "Failed to
# create CGVirtualDisplay" on both retry attempts (a masked bug: an
# earlier version of this step didn't fail the job when that
# happened, so it silently never ran the regression test at all).
# This churn test must not spin up a second CGVirtualDisplay alongside
# the persistent one from the previous step. A headless CI session
# only reliably holds one live CGVirtualDisplay at a time; a second
# instantiation fails with "Failed to create CGVirtualDisplay" on
# every retry.
# Reuse the persistent display's ID instead of creating a second one.
PERSISTENT_ID_PATH="/tmp/programa-vdisplay-persistent.id"
if [ ! -f "$PERSISTENT_ID_PATH" ]; then
Expand Down
13 changes: 6 additions & 7 deletions CLI/CLI+AgentEventAdapters.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ extension ProgramaCLI {
}

/// Reports a surface's agent activity state, then a matching `agent.event`, in that
/// order, with the same workspace/surface ids -- combines the two adjacent calls that
/// used to sit back-to-back at each hook call site in CLI+Hooks.swift. Same two socket
/// sends, same order, same arguments as before; purely a call-site collapse.
/// order, with the same workspace/surface ids -- combines the two adjacent calls a hook
/// call site in CLI+Hooks.swift would otherwise make back-to-back: two socket
/// sends, in that order, with the same arguments.
func reportAgentStateAndEvent(
client: SocketClient,
provider: String,
Expand All @@ -60,9 +60,9 @@ extension ProgramaCLI {
}

/// Sends `agent.needs_input` (docs/plans/agent-state-unification.md T2): one atomic
/// socket call replacing the `notification.create_for_target` +
/// socket call covering the `notification.create_for_target` +
/// `workspace.set_status` "Needs input" + `surface.report_agent_state` trio a hook
/// used to send independently for the same "needs input" moment.
/// would otherwise send independently for the same "needs input" moment.
func reportAgentNeedsInput(
client: SocketClient,
provider: String,
Expand Down Expand Up @@ -107,8 +107,7 @@ extension ProgramaCLI {
}

/// Maps a classified notification subtitle ("Permission" / "Waiting") to the matching
/// `agent.event` type. Mirrors the `switch summary.subtitle` blocks that used to sit
/// inline at the Claude Code and Codex notification-hook call sites in CLI+Hooks.swift.
/// `agent.event` type. Shared by the Claude Code and Codex notification-hook call sites in CLI+Hooks.swift.
func reportAgentEventForClassifiedNotificationSubtitle(
client: SocketClient,
provider: String,
Expand Down
2 changes: 1 addition & 1 deletion CLI/CLI+Hooks.swift
Original file line number Diff line number Diff line change
Expand Up @@ -730,7 +730,7 @@ extension ProgramaCLI {
let mappedSession = ctx.parsed.sessionId.flatMap { ctx.store.lookup(sessionId: $0) }

// Deliberately best-effort, for the same reason as the surfaceId resolution
// further down. A throw here used to abort the hook before any of the three
// further down. A throw here would abort the hook before any of the three
// independent clears below, while Claude Code continued with a stale red
// "blocked" badge that nothing would clear.
//
Expand Down
20 changes: 6 additions & 14 deletions CLI/programa.swift
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ enum CLIProcessRunner {

/// Execution context passed to a `CommandDescriptor`'s `execute` closure.
///
/// Bundles everything a command body previously read from `run()`'s locals
/// Bundles everything a command body reads from `run()`'s locals
/// (commandArgs, client, jsonOutput, idFormat, windowId, and the literal
/// command name, needed by handlers shared across several names such as
/// the tmux-compat group).
Expand Down Expand Up @@ -431,7 +431,7 @@ struct CLIArgumentGrammar {
/// Single source of truth for a CLI command's name(s), its one-line entry in
/// the grouped `Commands:` help block, and how it executes.
///
/// This collapses what used to be independently maintained in three places:
/// This one table covers what would otherwise be three separate places:
/// the kebab-case dispatch switch, the free-text `usage()` help string, and
/// (implicitly) the "is this a known command" check used for unknown-command
/// handling. A command added to the table automatically gets dispatch,
Expand Down Expand Up @@ -4402,7 +4402,7 @@ struct ProgramaCLI {
return trimmed.isEmpty ? nil : trimmed
}

// MARK: - Race command (fan one prompt across N agents in isolated worktrees, v1: spawn only)
// MARK: - Race command (fan one prompt across N agents in isolated worktrees, spawn only)

/// The only agents `race` supports: the three that report state via lifecycle hooks
/// (Claude Code, OpenCode, Codex). Racing needs a trustworthy idle signal, which
Expand All @@ -4411,14 +4411,7 @@ struct ProgramaCLI {
/// elsewhere in the app for status badges.
private static let raceSupportedAgents: [String] = ["claude", "opencode", "codex"]

/// Builds the shell command line that starts an agent with `prompt` as its initial task.
/// Reuses the same bare binary names Programa already invokes elsewhere for these agents
/// (`initialTerminalInput: "claude\n"` for the "New Claude Code Workspace" shortcut in
/// AppDelegate.swift, and the `process_names` in Resources/AgentDetection/*.json), just
/// with the prompt appended as a shell-quoted positional argument so the agent starts
/// working immediately instead of sitting at its own empty prompt. No prior art exists in
/// this codebase for "agent + initial prompt argument" specifically, so this is a new,
/// minimal one-place mapping rather than an existing construction being reused verbatim.
/// Builds the shell command line that starts an agent with `prompt` as a shell-quoted positional argument.
private func raceAgentLaunchCommand(agent: String, prompt: String) -> String {
"\(agent) \(shellSingleQuote(prompt))"
}
Expand Down Expand Up @@ -4477,9 +4470,8 @@ struct ProgramaCLI {
// Fail immediately, before creating anything, if the cwd isn't inside a git repo.
let repo = try resolveWorktreeRepoRoot(explicit: nil)

// Racing twice in a row used to collide on every index, because the branches were
// always `<prefix>/1...<prefix>/N`. Start after whatever `<prefix>/<number>` branches
// already exist so a second run just continues the numbering.
// Start after whatever `<prefix>/<number>` branches already exist so repeated runs
// continue the numbering instead of colliding.
let taken = existingRaceIndexes(repo: repo, prefix: prefix)
let firstIndex = (taken.max() ?? 0) + 1
let indexes = Array(firstIndex..<(firstIndex + n))
Expand Down
6 changes: 3 additions & 3 deletions Sources/AgentActivityState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ extension AgentPresence {
}
}

/// The single derived glyph/label/tint for a workspace's aggregate agent state, replacing the
/// three independent renderings that used to read `panelAgentStates`, `statusEntries["claude_code"]`,
/// and the notification subtitle separately. `TabItemView` is the only consumer.
/// The single derived glyph/label/tint for a workspace's aggregate agent state, derived once from
/// `panelAgentStates`, `statusEntries["claude_code"]`, and the notification subtitle
/// so no consumer renders them independently. `TabItemView` is the only consumer.
struct SidebarAgentIndicator: Equatable {
enum Tint: Equatable {
case blocked
Expand Down
6 changes: 3 additions & 3 deletions Sources/AppDelegate+SessionSnapshotPersistence.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import AppKit
import Bonsplit
import Foundation

/// Session snapshot build/save/persist machinery, extracted from `AppDelegate.swift` to stay
/// under its structural line budget (`scripts/check-structural-budgets.py`). Behavior-neutral
/// split: everything here still runs on `AppDelegate`, just from a sibling file.
/// Session snapshot build/save/persist machinery, kept out of `AppDelegate.swift` to stay
/// under its structural line budget (`scripts/check-structural-budgets.py`).
/// Everything here runs on `AppDelegate`, from a sibling file.
extension AppDelegate {
/// A synchronous save returns the disk-write result; an asynchronous save returns queue
/// acceptance and reports the completed disk write through its main-queue completion.
Expand Down
1 change: 0 additions & 1 deletion Sources/AppDelegate+UITestCmdClick.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import SwiftUI
import Bonsplit
import CoreServices
import UserNotifications
import Combine
import ObjectiveC.runtime
import Darwin

Expand Down
1 change: 0 additions & 1 deletion Sources/AppDelegate+UITestStressWorkspaces.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import SwiftUI
import Bonsplit
import CoreServices
import UserNotifications
import Combine
import ObjectiveC.runtime
import Darwin

Expand Down
18 changes: 8 additions & 10 deletions Sources/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import UserNotifications
import Combine
import ObjectiveC.runtime
import Darwin
import Security

private enum ProgramaThemeNotifications {
static let reloadConfig = Notification.Name("com.darkroom.programa.themes.reload-config")
Expand Down Expand Up @@ -2264,15 +2263,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser

/// Runs everything that must be re-asserted after the Mac wakes from sleep.
/// See `SessionEscrow.swift`'s `serve` death-detection loop for the
/// confirmed root cause this responds to: the escrow holder used to
/// measure heartbeat staleness with wall-clock time, so any sleep longer
/// than `SessionEscrowPolicy.heartbeatStaleAfter` (6s) made it declare
/// every escrowed PTY dead the instant the app woke back up and start
/// reading from them concurrently with the still-alive app, corrupting
/// terminal output until the app was force-quit. That's fixed at the
/// source (mach-uptime-based staleness, which pauses across sleep), but
/// this handler still proactively refreshes the escrow connection and
/// renderer state on every wake as defense in depth and to leave a
/// failure this guards against: if the escrow holder measured heartbeat
/// staleness with wall-clock time, any sleep longer than
/// `SessionEscrowPolicy.heartbeatStaleAfter` (6s) would make it declare
/// every escrowed PTY dead the instant the app woke and start reading
/// from them concurrently with the still-alive app, corrupting terminal
/// output until force-quit. Staleness is mach-uptime based (pauses across
/// sleep), and this handler additionally refreshes the escrow connection
/// and renderer state on every wake as defense in depth and to leave a
/// record in the always-on diagnostics log if it ever happens again.
private func handleSystemDidWake() {
dilog("wake.lifecycle", "didWake")
Expand Down
2 changes: 1 addition & 1 deletion Sources/BackgroundLogWriter.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Foundation

// MARK: - BackgroundLogWriter (ported from upstream cmux cb2129a5a1)
// MARK: - BackgroundLogWriter
//
// Avoids a per-call FileManager.fileExists + FileHandle(forWritingTo:)
// open -> seekToEnd -> write -> close pattern, which would run synchronously under a lock
Expand Down
5 changes: 2 additions & 3 deletions Sources/ClosedTerminalUndoStore.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Foundation

/// Generic, closure-based undo staging for terminal panel closes (issue #140, ported from
/// upstream cmux's task-undo-close-grace-period). Closing a terminal stages it here instead of
/// Generic, closure-based undo staging for terminal panel closes (issue #140). Closing a terminal stages it here instead of
/// tearing it down immediately; if nothing restores it within the grace period, `finalize` runs
/// the real teardown.
///
Expand Down Expand Up @@ -37,7 +36,7 @@ final class ClosedTerminalUndoStore {

/// Stages a close for undo. `restore` reattaches the closed panel; `finalize` performs the
/// real teardown once the grace period elapses without a restore. Returns an opaque id that
/// can be used to cancel staging early (not currently needed by any caller).
/// can be used to cancel staging early.
@discardableResult
func stage(restore: @escaping () -> Void, finalize: @escaping () -> Void) -> UUID {
let id = UUID()
Expand Down
13 changes: 6 additions & 7 deletions Sources/CommandPaletteController.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Command-palette state ownership.
//
// CommandPaletteController owns every @State property that used to live on
// ContentView and is exclusively used by the command palette (query, mode,
// CommandPaletteController owns every piece of palette state that is
// exclusively used by the command palette (query, mode,
// search corpus/results, rename/workspace-description drafts, focus-restore
// targets, usage history, etc.). ContentView holds a single
// `@StateObject private var commandPaletteController` and exposes each
Expand All @@ -15,13 +15,12 @@
// SwiftUI View-only property wrapper and cannot be hosted on an
// ObservableObject.
//
// Access-level widening: CommandPaletteMode, CommandPaletteRestoreFocusTarget,
// CommandPaletteMode, CommandPaletteRestoreFocusTarget,
// CommandPaletteTextSelectionBehavior, and CommandPaletteMultilineTextEditorRepresentable
// were `private` nested types inside ContentView; widened to internal (dropped
// the `private` modifier) so this controller (a different file) can reference
// them. No other behavior change. CommandPaletteCommand, CommandPaletteSearchResult,
// are internal nested types of ContentView so this controller (a different file) can
// reference them. CommandPaletteCommand, CommandPaletteSearchResult,
// CommandPaletteListScope, CommandPalettePendingActivation, and
// CommandPaletteUsageEntry were already internal nested types; referenced here
// CommandPaletteUsageEntry are also internal nested types; referenced here
// via `ContentView.` qualification since nested-type name lookup requires it
// from outside the enclosing type's lexical scope.

Expand Down
2 changes: 1 addition & 1 deletion Sources/CommandPaletteSearchEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ enum CommandPaletteFuzzyMatcher {
// the two interpretations point at candidates of very different lengths/title
// positions: `distancePenalty`/`trailingPenalty` (both scale with the matched
// word's position and the candidate's overall length) can swing by hundreds of
// points, which used to swamp a mere 10-point base gap and let a shorter,
// points, which would swamp a mere 10-point base gap and let a shorter,
// less-relevant command's stray-extra-character interpretation outrank a
// longer command's genuine one-character-omitted match on its own keyword.
return 260
Expand Down
5 changes: 0 additions & 5 deletions Sources/ContentView+CommandPalette.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,8 @@
// remain on the `ContentView` struct in ContentView.swift, since SwiftUI's
// `@State` cannot be split across files without a larger controller-based
// rewrite (tracked as a follow-up).
//
// A handful of members these functions call into were widened from `private`
// to `internal` (no other behavior change) so this extension can see them;
// see the CV1 PR description for the exact list.

import AppKit
import Combine
import SwiftUI

extension ContentView {
Expand Down
4 changes: 2 additions & 2 deletions Sources/ContentView+SidebarResizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// The backing @State stays on `ContentView` (SwiftUI requires stored properties on the primary
// declaration); those properties, plus `SidebarResizerHandle`, `updateSidebarResizerBandState`,
// `installSidebarResizerPointerMonitorIfNeeded`, `removeSidebarResizerPointerMonitor`, and
// `sidebarResizerOverlay`, were widened from `private` to internal so this extension and
// ContentView.swift's view body can both see them. See the PR description for the exact list.
// `sidebarResizerOverlay`, are internal so this extension and
// ContentView.swift's view body can both see them.

import AppKit
import SwiftUI
Expand Down
1 change: 0 additions & 1 deletion Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import Combine
import ImageIO
import SwiftUI
import ObjectiveC
import UniformTypeIdentifiers

struct ContentView: View {
@ObservedObject var updateViewModel: UpdateViewModel
Expand Down
1 change: 0 additions & 1 deletion Sources/DebugWindows.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import AppKit
import SwiftUI
import Darwin
import Bonsplit
import UniformTypeIdentifiers

private func localizedDebugLabel(_ value: String) -> String {
String(localized: String.LocalizationValue(value))
Expand Down
Loading
Loading