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: 11 additions & 0 deletions .codex/environments/environment.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# THIS IS AUTOGENERATED. DO NOT EDIT MANUALLY
version = 1
name = "ClawDnD"

[setup]
script = ""

[[actions]]
name = "Run"
icon = "run"
command = "./script/build_and_run.sh"
40 changes: 40 additions & 0 deletions .github/workflows/macos-swift.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: macOS Swift

on:
push:
branches:
- macos/native-app-shell
- macos/swift-ci
paths:
- ".github/workflows/*.yml"
- "macos/ClawDnDApp/**"
- "script/build_and_run.sh"
- "scripts/*.sh"
- "scripts/**/*.sh"
pull_request:
branches:
- macos/native-app-shell
paths:
- ".github/workflows/*.yml"
- "macos/ClawDnDApp/**"
- "script/build_and_run.sh"
- "scripts/*.sh"
- "scripts/**/*.sh"
workflow_dispatch:

jobs:
swift-build:
name: SwiftPM build
runs-on: macos-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4

- name: Validate launcher script syntax
shell: bash
run: |
bash -n script/build_and_run.sh
find scripts -type f -name "*.sh" -print0 | xargs -0 -n 1 bash -n

- name: Build native app
run: swift build --package-path macos/ClawDnDApp
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ venv/
.uv/
*.egg-info/

# Native macOS app build outputs
/dist/
/macos/ClawDnDApp/.build/
/macos/ClawDnDApp/.swiftpm/

# TTS models / audio caches (large, regenerated locally)
*.onnx
*.pt
Expand Down
14 changes: 13 additions & 1 deletion clawdnd-play.command
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,16 @@ cd "$(dirname "$0")" || exit 1
# play_party.sh == solo play.sh when no companion spec is given, and adds the opt-in party
# when one is (via the 4th arg or $CLAWDND_PLAY_COMPANIONS). Routing through it keeps the
# double-click solo experience identical while enabling companions for those who want them.
exec "$PWD/scripts/play_party.sh" "$@"
"$PWD/scripts/play_party.sh" "$@"
status=$?
if [ "$status" -ne 0 ] && [ "$status" -ne 130 ]; then
echo
echo "ClawDnD did not start cleanly (exit $status)."
echo "The message above should say what was missing or which port was busy."
if [ -t 0 ]; then
echo
echo "Press Return to close this window."
read -r _
fi
fi
exit "$status"
16 changes: 16 additions & 0 deletions macos/ClawDnDApp/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// swift-tools-version: 5.9

import PackageDescription

let package = Package(
name: "ClawDnDApp",
platforms: [
.macOS(.v13)
],
products: [
.executable(name: "ClawDnDApp", targets: ["ClawDnDApp"])
],
targets: [
.executableTarget(name: "ClawDnDApp")
]
)
31 changes: 31 additions & 0 deletions macos/ClawDnDApp/RELEASE_CHECKLIST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# ClawDnD Native macOS Release Checklist

The v0.3 macOS lane starts with a locally signed development app. Notarization is
release-trust work after the local shell, provider bridge, and dashboard hosting
are stable.

## Local build

```bash
./script/build_and_run.sh --verify
```

## Signing state

```bash
security find-identity -p codesigning -v
codesign --verify --deep --strict dist/ClawDnD.app
spctl -a -vv dist/ClawDnD.app
```

The build script ad-hoc signs the local app bundle when `codesign` is available.
Gatekeeper assessment may still reject the app until a Developer ID certificate,
hardened runtime, and notarization flow are configured.

## Distribution blockers to track separately

- Developer ID signing identity.
- Hardened runtime entitlements.
- Notarization profile and CI secret handling.
- User-facing update channel.
- Copyright/private world seed exclusion from packaged artifacts.
34 changes: 34 additions & 0 deletions macos/ClawDnDApp/Sources/ClawDnDApp/App/ClawDnDApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import AppKit
import SwiftUI

@main
struct ClawDnDApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@StateObject private var processService = AppProcessService()
@StateObject private var campaignStore = CampaignStore()

var body: some Scene {
WindowGroup {
RootView()
.environmentObject(processService)
.environmentObject(campaignStore)
.frame(minWidth: 1120, minHeight: 720)
}
.commands {
CommandGroup(replacing: .newItem) {}
CommandGroup(after: .appInfo) {
Button("Copy Diagnostics") {
Diagnostics.copy(processService: processService)
}
.keyboardShortcut("d", modifiers: [.command, .shift])
}
}
}
}

final class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
}
}
34 changes: 34 additions & 0 deletions macos/ClawDnDApp/Sources/ClawDnDApp/Models/AppSection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import Foundation

enum AppSection: String, CaseIterable, Identifiable {
case play
case campaigns
case monitor
case providers
case settings
case logs

var id: String { rawValue }

var title: String {
switch self {
case .play: "Play"
case .campaigns: "Campaigns"
case .monitor: "Monitor"
case .providers: "Providers"
case .settings: "Settings"
case .logs: "Logs"
}
}

var symbolName: String {
switch self {
case .play: "play.circle"
case .campaigns: "books.vertical"
case .monitor: "waveform.path.ecg.rectangle"
case .providers: "person.2.wave.2"
case .settings: "gearshape"
case .logs: "doc.text.magnifyingglass"
}
}
}
41 changes: 41 additions & 0 deletions macos/ClawDnDApp/Sources/ClawDnDApp/Models/CampaignSummary.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import Foundation

enum CampaignSource: String, Codable {
case play
case qa
}

struct CampaignSummary: Identifiable, Equatable {
let id: String
let runID: String
let source: CampaignSource
let snapshotPath: URL
let stateRoot: URL
let title: String
let world: String
let day: Int?
let timeOfDay: String
let location: String
let party: [String]
let provider: String
let lastUpdate: Date
let isLive: Bool

var sourceLabel: String {
switch source {
case .play: "Play"
case .qa: "QA"
}
}

var partyLabel: String {
party.isEmpty ? "No party yet" : party.joined(separator: ", ")
}

var dayLabel: String {
if let day {
return timeOfDay.isEmpty ? "Day \(day)" : "Day \(day), \(timeOfDay)"
}
return timeOfDay.isEmpty ? "Unknown time" : timeOfDay
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
10 changes: 10 additions & 0 deletions macos/ClawDnDApp/Sources/ClawDnDApp/Models/DependencyStatus.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

struct DependencyStatus: Identifiable, Equatable {
var id: String { command }
let command: String
let requiredFor: String
let path: String?

var isInstalled: Bool { path != nil }
}
28 changes: 28 additions & 0 deletions macos/ClawDnDApp/Sources/ClawDnDApp/Models/LocalEndpoint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import Foundation

enum EndpointStatus: String, Equatable {
case stopped
case starting
case running
case failed
}

struct LocalEndpoint: Identifiable, Equatable {
let id = UUID()
var name: String
var url: URL
var healthPath: String
var status: EndpointStatus

var port: Int {
URLComponents(url: url, resolvingAgainstBaseURL: false)?.port ?? 0
}

var dashboardURL: URL {
url.appendingPathComponent("dashboard")
}

var monitorURL: URL {
url.appendingPathComponent("monitor")
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
81 changes: 81 additions & 0 deletions macos/ClawDnDApp/Sources/ClawDnDApp/Models/ProviderModels.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import Foundation

enum ProviderKind: String, CaseIterable, Identifiable {
case claude
case codex
case openclaw

var id: String { rawValue }

var displayName: String {
switch self {
case .claude: "Claude"
case .codex: "Codex"
case .openclaw: "OpenClaw"
}
}

var symbolName: String {
switch self {
case .claude: "sparkles"
case .codex: "terminal"
case .openclaw: "link"
}
}
}

enum ProviderAvailability: String, Equatable {
case installed
case configured
case missing
case error
}

struct ProviderStatus: Identifiable, Equatable {
var id: String { kind.rawValue }
let kind: ProviderKind
let availability: ProviderAvailability
let detail: String
let detectedPath: String?

var isLaunchable: Bool {
availability == .configured || (kind == .claude && availability == .installed)
}
}

struct ProviderRun: Identifiable, Equatable {
let id: String
let provider: ProviderKind
let processID: Int32?
let message: String
let startedAt: Date
}

struct ProviderPreferences {
let codexCommand: String
let openClawCommand: String
let budget: String
let sessionBudget: String
let maxTurns: String
}

struct ProviderLaunchRequest {
let name: String
let executable: String
let arguments: [String]
let environment: [String: String]
let workingDirectory: URL
let message: String
}

enum ProviderError: LocalizedError {
case missingDependency(String)
case configuration(String)

var errorDescription: String? {
switch self {
case .missingDependency(let message), .configuration(let message):
message
}
}
}
Loading
Loading