Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
b92b60d
Replace fixed-interval polling with a push-based runtime event bus
RealDyllon Jun 20, 2026
64a77e6
chore(openspec): add apple-design-award-ui change proposal
RealDyllon Jun 20, 2026
00c2900
feat(design-system): introduce DesignSystem module and port primitive…
RealDyllon Jun 20, 2026
3a35fe6
feat(chrome): native NSToolbar, balanced sidebar, runtime health foot…
RealDyllon Jun 21, 2026
df86c1f
feat(tables): migrate resource views to SwiftUI Table with sort/selec…
RealDyllon Jun 21, 2026
4cf748a
feat(editor): card-based profile editor sheet
RealDyllon Jun 21, 2026
afdfda9
feat(settings): sidebar-style settings window with category cards
RealDyllon Jun 21, 2026
79c6a24
feat(terminal): NSTextView-backed streaming log view
RealDyllon Jun 21, 2026
423b33b
feat(container): per-container lifecycle actions, inspect, logs
RealDyllon Jun 21, 2026
580aade
feat(onboarding): first-run welcome + dependency check + profile crea…
RealDyllon Jun 21, 2026
eae68a4
feat(empty-states): every list/section uses EmptyStateView
RealDyllon Jun 21, 2026
573b71c
feat(motion): hover, focus ring, lifecycle flash, toasts, VoiceOver t…
RealDyllon Jun 21, 2026
67d53e5
feat(accessibility): combined labels, high-contrast, dynamic type, sh…
RealDyllon Jun 21, 2026
0d310a9
chore(tests): fix SwiftUI imports and async test calls
RealDyllon Jun 21, 2026
db64a28
fix(motion): avoid KVC exception in readReduceMotion
RealDyllon Jun 21, 2026
d6b4d74
chore: bump version to 0.1.0
RealDyllon Jun 21, 2026
517c824
merge origin/main into feat/ui-redesign
RealDyllon Jun 21, 2026
2120488
fix(icon): replace broken AppIcon.imageset with the working appiconset
RealDyllon Jun 21, 2026
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 ColimaStack.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 0.0.1;
MARKETING_VERSION = 0.1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.dyllon.ColimaStack;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
Expand Down Expand Up @@ -459,7 +459,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 0.0.1;
MARKETING_VERSION = 0.1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.dyllon.ColimaStack;
PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES;
Expand All @@ -481,7 +481,7 @@
DEVELOPMENT_TEAM = TF835S78NT;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 0.0.1;
MARKETING_VERSION = 0.1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.dyllon.ColimaStackTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -502,7 +502,7 @@
DEVELOPMENT_TEAM = TF835S78NT;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 14.0;
MARKETING_VERSION = 0.0.1;
MARKETING_VERSION = 0.1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.dyllon.ColimaStackTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -522,7 +522,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = TF835S78NT;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 0.0.1;
MARKETING_VERSION = 0.1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.dyllon.ColimaStackUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand All @@ -542,7 +542,7 @@
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = TF835S78NT;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 0.0.1;
MARKETING_VERSION = 0.1.0;
PRODUCT_BUNDLE_IDENTIFIER = io.dyllon.ColimaStackUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
Expand Down
95 changes: 68 additions & 27 deletions ColimaStack/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ enum AutoRefreshFrequency: String, CaseIterable, Identifiable {
enum ProfileEditorMode: Equatable {
case create
case edit(profileID: ColimaProfile.ID)

var isEdit: Bool {
if case .edit = self { return true }
return false
}
}

@MainActor
Expand Down Expand Up @@ -58,6 +63,10 @@ final class AppState: ObservableObject {
@Published var isShowingProfileEditor = false
@Published var profileEditorMode: ProfileEditorMode?
@Published var editingConfiguration: ProfileConfiguration = .default
/// The configuration as it was when the editor opened. Used to
/// detect destructive field changes (runtime, vmType, diskGiB)
/// that require a recreate confirmation.
@Published var originalEditingConfiguration: ProfileConfiguration?
@Published var autoRefresh = true {
didSet { userDefaults?.set(autoRefresh, forKey: DefaultsKey.autoRefresh) }
}
Expand All @@ -72,6 +81,13 @@ final class AppState: ObservableObject {
}
@Published var connectionStatus = RuntimeConnectionStatus()
@Published var hasCompletedDiagnostics = false
@Published var defaultTableDensity: TableDensity = .standard {
didSet {
guard oldValue != defaultTableDensity else { return }
userDefaults?.set(defaultTableDensity.rawValue, forKey: DefaultsKey.tableDensity)
}
}
@Published var tableColumnCustomization: TableColumnCustomization = .init()

private let colima: ColimaControlling
private let backend: BackendSnapshotProviding?
Expand All @@ -86,6 +102,9 @@ final class AppState: ObservableObject {
private var currentCommandTask: Task<Void, Never>?
private var currentCommandCancellation: ProcessCancellation?
private var toolCheckTask: Task<Void, Never>?
/// Container lifecycle service. Owned by `AppState` so the
/// notification subscribers outlive the per-screen views.
public var containerService: ContainerService!

init(
colima: ColimaControlling,
Expand All @@ -101,6 +120,7 @@ final class AppState: ObservableObject {
self.searchIndexer = searchIndexer ?? BackendSearchIndexer()
self.userDefaults = userDefaults
self.profiles = profiles
self.containerService = nil
if let rawSection = userDefaults?.string(forKey: DefaultsKey.selectedSection),
let section = WorkspaceRoute(rawValue: rawSection) {
self.selectedSection = section
Expand All @@ -118,9 +138,16 @@ final class AppState: ObservableObject {
if userDefaults?.object(forKey: DefaultsKey.useEventBus) != nil {
self.useEventBus = userDefaults?.bool(forKey: DefaultsKey.useEventBus) ?? true
}
if let rawDensity = userDefaults?.string(forKey: DefaultsKey.tableDensity),
let density = TableDensity(rawValue: rawDensity) {
self.defaultTableDensity = density
}
let persistedProfileID = userDefaults?.string(forKey: DefaultsKey.selectedProfileID)
self.selectedProfileID = persistedProfileID.flatMap { id in profiles.contains(where: { $0.id == id }) ? id : nil } ?? profiles.first?.id
rebuildSearchIndex()
// Now that all stored properties are initialized, swap the
// placeholder for a fully-wired ContainerService.
self.containerService = ContainerService(appState: self)
}

static func live() -> AppState {
Expand Down Expand Up @@ -366,60 +393,62 @@ final class AppState: ObservableObject {
}
}

// MARK: - Container lifecycle (thin wrappers over ContainerService)

/// Convenience wrapper for `ContainersScreen` from main (pre-group-3
/// integration). Forwards to `ContainerService.start(containerID:)`.
func startContainer(_ container: DockerContainerResource) async {
await runDockerContainerCommand("Start container \(container.displayName)") {
try await dockerContainerController.start(containerID: container.id, context: selectedDockerContext)
}
await containerService.start(containerID: container.id)
}

func stopContainer(_ container: DockerContainerResource) async {
await runDockerContainerCommand("Stop container \(container.displayName)") {
try await dockerContainerController.stop(containerID: container.id, context: selectedDockerContext)
}
await containerService.stop(containerID: container.id)
}

func restartContainer(_ container: DockerContainerResource) async {
await runDockerContainerCommand("Restart container \(container.displayName)") {
try await dockerContainerController.restart(containerID: container.id, context: selectedDockerContext)
}
await containerService.restart(containerID: container.id)
}

func pauseContainer(_ container: DockerContainerResource) async {
await runDockerContainerCommand("Pause container \(container.displayName)") {
try await dockerContainerController.pause(containerID: container.id, context: selectedDockerContext)
}
await containerService.pause(containerID: container.id)
}

func resumeContainer(_ container: DockerContainerResource) async {
await runDockerContainerCommand("Resume container \(container.displayName)") {
try await dockerContainerController.resume(containerID: container.id, context: selectedDockerContext)
}
await containerService.resume(containerID: container.id)
}

func killContainer(_ container: DockerContainerResource) async {
await runDockerContainerCommand("Kill container \(container.displayName)") {
try await dockerContainerController.kill(containerID: container.id, context: selectedDockerContext)
}
await containerService.kill(containerID: container.id)
}

/// Legacy alias used by the old `ContainerDeleteConfirmationSheet`.
/// `removeContainer` deletes with `--force` to match the previous
/// behaviour of running `docker rm -f`.
func removeContainer(_ container: DockerContainerResource) async {
await runDockerContainerCommand("Delete container \(container.displayName)") {
try await dockerContainerController.remove(containerID: container.id, context: selectedDockerContext)
}
await containerService.delete(containerID: container.id, force: true)
}

/// Stream the latest log capture for the supplied container.
/// Returns the captured log text (capped to a sensible buffer
/// length to avoid runaway memory use).
func containerLogs(_ container: DockerContainerResource, timestamps: Bool, tail: Int) async throws -> String {
let output = try await dockerContainerController.logs(containerID: container.id, context: selectedDockerContext, timestamps: timestamps, tail: tail)
return cappedLog(output)
let buffer = LogStreamBuffer()
containerService.logs(containerID: container.id, into: buffer)
return buffer.renderPlainText()
}

/// Return the JSON inspect output for the supplied container.
func inspectContainer(_ container: DockerContainerResource) async throws -> String {
let output = try await dockerContainerController.inspect(containerID: container.id, context: selectedDockerContext)
return cappedLog(output)
if let text = await containerService.inspect(containerID: container.id) {
return text
}
return "{}"
}

/// Build a `docker exec` command for opening a terminal inside a
/// running container. Surfaced to the menubar / context menu.
func terminalCommand(for container: DockerContainerResource, shell: String = "/bin/sh") -> String {
dockerContainerController.terminalCommand(containerID: container.id, context: selectedDockerContext, shell: shell)
"docker exec -it \(container.id) \(shell)"
}

func setKubernetes(enabled: Bool) async {
Expand Down Expand Up @@ -456,13 +485,24 @@ final class AppState: ObservableObject {
profileEditorTask?.cancel()
profileEditorTask = Task { [weak self] in
guard let self else { return }
editingConfiguration = await configuration(for: profile)
let configuration = await configuration(for: profile)
guard !Task.isCancelled, selectedProfileID == profile.id else { return }
originalEditingConfiguration = configuration
editingConfiguration = configuration
profileEditorMode = .edit(profileID: profile.id)
isShowingProfileEditor = true
}
}

/// True if the current edit changed a field that requires the
/// profile to be recreated (runtime, vmType, diskGiB).
var hasDestructiveFieldChange: Bool {
guard let original = originalEditingConfiguration else { return false }
return original.runtime != editingConfiguration.runtime
|| original.vmType != editingConfiguration.vmType
|| original.resources.diskGiB != editingConfiguration.resources.diskGiB
}

func cancelProfileEditing() {
profileEditorTask?.cancel()
profileEditorTask = nil
Expand Down Expand Up @@ -972,6 +1012,7 @@ private enum DefaultsKey {
static let autoRefreshFrequency = "autoRefreshFrequency"
static let useStreamingCommandOutput = "useStreamingCommandOutput"
static let useEventBus = "useEventBus"
static let tableDensity = "tableDensity"
}

struct AppError: Identifiable, Equatable {
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
68 changes: 0 additions & 68 deletions ColimaStack/Assets.xcassets/AppIcon.imageset/Contents.json

This file was deleted.

6 changes: 6 additions & 0 deletions ColimaStack/Assets.xcassets/BrandMark.imageset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Diff not rendered.
2 changes: 2 additions & 0 deletions ColimaStack/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ struct ContentView: View {
var body: some View {
MainWindowView()
.environmentObject(appState)
.designSystemColorResolution()
.observeReducedMotion()
}
}

Expand Down
Loading
Loading