fix(mac): shrink onboarding + respect existing workspace
This commit is contained in:
@@ -9,6 +9,14 @@ enum AgentWorkspace {
|
|||||||
static let userFilename = "USER.md"
|
static let userFilename = "USER.md"
|
||||||
static let bootstrapFilename = "BOOTSTRAP.md"
|
static let bootstrapFilename = "BOOTSTRAP.md"
|
||||||
private static let templateDirname = "templates"
|
private static let templateDirname = "templates"
|
||||||
|
private static let ignoredEntries: Set<String> = [".DS_Store", ".git", ".gitignore"]
|
||||||
|
private static let templateEntries: Set<String> = [
|
||||||
|
AgentWorkspace.agentsFilename,
|
||||||
|
AgentWorkspace.soulFilename,
|
||||||
|
AgentWorkspace.identityFilename,
|
||||||
|
AgentWorkspace.userFilename,
|
||||||
|
AgentWorkspace.bootstrapFilename,
|
||||||
|
]
|
||||||
enum BootstrapSafety: Equatable {
|
enum BootstrapSafety: Equatable {
|
||||||
case safe
|
case safe
|
||||||
case unsafe(reason: String)
|
case unsafe(reason: String)
|
||||||
@@ -35,6 +43,28 @@ enum AgentWorkspace {
|
|||||||
workspaceURL.appendingPathComponent(self.agentsFilename)
|
workspaceURL.appendingPathComponent(self.agentsFilename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func workspaceEntries(workspaceURL: URL) throws -> [String] {
|
||||||
|
let contents = try FileManager.default.contentsOfDirectory(atPath: workspaceURL.path)
|
||||||
|
return contents.filter { !self.ignoredEntries.contains($0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
static func isWorkspaceEmpty(workspaceURL: URL) -> Bool {
|
||||||
|
let fm = FileManager.default
|
||||||
|
var isDir: ObjCBool = false
|
||||||
|
if !fm.fileExists(atPath: workspaceURL.path, isDirectory: &isDir) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
guard isDir.boolValue else { return false }
|
||||||
|
guard let entries = try? self.workspaceEntries(workspaceURL: workspaceURL) else { return false }
|
||||||
|
return entries.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
static func isTemplateOnlyWorkspace(workspaceURL: URL) -> Bool {
|
||||||
|
guard let entries = try? self.workspaceEntries(workspaceURL: workspaceURL) else { return false }
|
||||||
|
guard !entries.isEmpty else { return true }
|
||||||
|
return Set(entries).isSubset(of: self.templateEntries)
|
||||||
|
}
|
||||||
|
|
||||||
static func bootstrapSafety(for workspaceURL: URL) -> BootstrapSafety {
|
static func bootstrapSafety(for workspaceURL: URL) -> BootstrapSafety {
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
var isDir: ObjCBool = false
|
var isDir: ObjCBool = false
|
||||||
@@ -49,10 +79,8 @@ enum AgentWorkspace {
|
|||||||
return .safe
|
return .safe
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
let contents = try fm.contentsOfDirectory(atPath: workspaceURL.path)
|
let entries = try self.workspaceEntries(workspaceURL: workspaceURL)
|
||||||
let ignored: Set<String> = [".DS_Store", ".git", ".gitignore"]
|
return entries.isEmpty
|
||||||
let filtered = contents.filter { !ignored.contains($0) }
|
|
||||||
return filtered.isEmpty
|
|
||||||
? .safe
|
? .safe
|
||||||
: .unsafe(reason: "Folder isn't empty. Choose a new folder or add AGENTS.md first.")
|
: .unsafe(reason: "Folder isn't empty. Choose a new folder or add AGENTS.md first.")
|
||||||
} catch {
|
} catch {
|
||||||
@@ -61,6 +89,7 @@ enum AgentWorkspace {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func bootstrap(workspaceURL: URL) throws -> URL {
|
static func bootstrap(workspaceURL: URL) throws -> URL {
|
||||||
|
let shouldSeedBootstrap = self.isWorkspaceEmpty(workspaceURL: workspaceURL)
|
||||||
try FileManager.default.createDirectory(at: workspaceURL, withIntermediateDirectories: true)
|
try FileManager.default.createDirectory(at: workspaceURL, withIntermediateDirectories: true)
|
||||||
let agentsURL = self.agentsURL(workspaceURL: workspaceURL)
|
let agentsURL = self.agentsURL(workspaceURL: workspaceURL)
|
||||||
if !FileManager.default.fileExists(atPath: agentsURL.path) {
|
if !FileManager.default.fileExists(atPath: agentsURL.path) {
|
||||||
@@ -83,7 +112,7 @@ enum AgentWorkspace {
|
|||||||
self.logger.info("Created USER.md at \(userURL.path, privacy: .public)")
|
self.logger.info("Created USER.md at \(userURL.path, privacy: .public)")
|
||||||
}
|
}
|
||||||
let bootstrapURL = workspaceURL.appendingPathComponent(self.bootstrapFilename)
|
let bootstrapURL = workspaceURL.appendingPathComponent(self.bootstrapFilename)
|
||||||
if !FileManager.default.fileExists(atPath: bootstrapURL.path) {
|
if shouldSeedBootstrap, !FileManager.default.fileExists(atPath: bootstrapURL.path) {
|
||||||
try self.defaultBootstrapTemplate().write(to: bootstrapURL, atomically: true, encoding: .utf8)
|
try self.defaultBootstrapTemplate().write(to: bootstrapURL, atomically: true, encoding: .utf8)
|
||||||
self.logger.info("Created BOOTSTRAP.md at \(bootstrapURL.path, privacy: .public)")
|
self.logger.info("Created BOOTSTRAP.md at \(bootstrapURL.path, privacy: .public)")
|
||||||
}
|
}
|
||||||
@@ -97,8 +126,30 @@ enum AgentWorkspace {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
guard isDir.boolValue else { return true }
|
guard isDir.boolValue else { return true }
|
||||||
|
if self.hasIdentity(workspaceURL: workspaceURL) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
let bootstrapURL = workspaceURL.appendingPathComponent(self.bootstrapFilename)
|
let bootstrapURL = workspaceURL.appendingPathComponent(self.bootstrapFilename)
|
||||||
return fm.fileExists(atPath: bootstrapURL.path)
|
guard fm.fileExists(atPath: bootstrapURL.path) else { return false }
|
||||||
|
return self.isTemplateOnlyWorkspace(workspaceURL: workspaceURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func hasIdentity(workspaceURL: URL) -> Bool {
|
||||||
|
let identityURL = workspaceURL.appendingPathComponent(self.identityFilename)
|
||||||
|
guard let contents = try? String(contentsOf: identityURL, encoding: .utf8) else { return false }
|
||||||
|
return self.identityLinesHaveValues(contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func identityLinesHaveValues(_ content: String) -> Bool {
|
||||||
|
for line in content.split(separator: "\n") {
|
||||||
|
let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
guard trimmed.hasPrefix("-"), let colon = trimmed.firstIndex(of: ":") else { continue }
|
||||||
|
let value = trimmed[trimmed.index(after: colon)...].trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
if !value.isEmpty {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
static func defaultTemplate() -> String {
|
static func defaultTemplate() -> String {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ final class OnboardingController {
|
|||||||
let hosting = NSHostingController(rootView: OnboardingView())
|
let hosting = NSHostingController(rootView: OnboardingView())
|
||||||
let window = NSWindow(contentViewController: hosting)
|
let window = NSWindow(contentViewController: hosting)
|
||||||
window.title = UIStrings.welcomeTitle
|
window.title = UIStrings.welcomeTitle
|
||||||
window.setContentSize(NSSize(width: 630, height: 684))
|
window.setContentSize(NSSize(width: 630, height: 644))
|
||||||
window.styleMask = [.titled, .closable, .fullSizeContentView]
|
window.styleMask = [.titled, .closable, .fullSizeContentView]
|
||||||
window.titlebarAppearsTransparent = true
|
window.titlebarAppearsTransparent = true
|
||||||
window.titleVisibility = .hidden
|
window.titleVisibility = .hidden
|
||||||
@@ -79,7 +79,7 @@ struct OnboardingView: View {
|
|||||||
private var permissionMonitor: PermissionMonitor
|
private var permissionMonitor: PermissionMonitor
|
||||||
|
|
||||||
private let pageWidth: CGFloat = 630
|
private let pageWidth: CGFloat = 630
|
||||||
private let contentHeight: CGFloat = 520
|
private let contentHeight: CGFloat = 420
|
||||||
private let connectionPageIndex = 1
|
private let connectionPageIndex = 1
|
||||||
private let anthropicAuthPageIndex = 2
|
private let anthropicAuthPageIndex = 2
|
||||||
private let onboardingChatPageIndex = 8
|
private let onboardingChatPageIndex = 8
|
||||||
@@ -140,9 +140,9 @@ struct OnboardingView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
GlowingClawdisIcon(size: 156, glowIntensity: 0.28)
|
GlowingClawdisIcon(size: 130, glowIntensity: 0.28)
|
||||||
.offset(y: 13)
|
.offset(y: 10)
|
||||||
.frame(height: 181)
|
.frame(height: 145)
|
||||||
|
|
||||||
GeometryReader { _ in
|
GeometryReader { _ in
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
|
|||||||
@@ -84,4 +84,40 @@ struct AgentWorkspaceTests {
|
|||||||
#expect(false, "Expected safe bootstrap safety result.")
|
#expect(false, "Expected safe bootstrap safety result.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func bootstrapSkipsBootstrapFileWhenWorkspaceHasContent() throws {
|
||||||
|
let tmp = FileManager.default.temporaryDirectory
|
||||||
|
.appendingPathComponent("clawdis-ws-\(UUID().uuidString)", isDirectory: true)
|
||||||
|
defer { try? FileManager.default.removeItem(at: tmp) }
|
||||||
|
try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true)
|
||||||
|
let marker = tmp.appendingPathComponent("notes.txt")
|
||||||
|
try "hello".write(to: marker, atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
|
_ = try AgentWorkspace.bootstrap(workspaceURL: tmp)
|
||||||
|
|
||||||
|
let bootstrapURL = tmp.appendingPathComponent(AgentWorkspace.bootstrapFilename)
|
||||||
|
#expect(!FileManager.default.fileExists(atPath: bootstrapURL.path))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func needsBootstrapFalseWhenIdentityAlreadySet() throws {
|
||||||
|
let tmp = FileManager.default.temporaryDirectory
|
||||||
|
.appendingPathComponent("clawdis-ws-\(UUID().uuidString)", isDirectory: true)
|
||||||
|
defer { try? FileManager.default.removeItem(at: tmp) }
|
||||||
|
try FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true)
|
||||||
|
let identityURL = tmp.appendingPathComponent(AgentWorkspace.identityFilename)
|
||||||
|
try """
|
||||||
|
# IDENTITY.md - Agent Identity
|
||||||
|
|
||||||
|
- Name: Clawd
|
||||||
|
- Creature: Space Lobster
|
||||||
|
- Vibe: Helpful
|
||||||
|
- Emoji: lobster
|
||||||
|
""".write(to: identityURL, atomically: true, encoding: .utf8)
|
||||||
|
let bootstrapURL = tmp.appendingPathComponent(AgentWorkspace.bootstrapFilename)
|
||||||
|
try "bootstrap".write(to: bootstrapURL, atomically: true, encoding: .utf8)
|
||||||
|
|
||||||
|
#expect(!AgentWorkspace.needsBootstrap(workspaceURL: tmp))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user