From 5b25eeb44981a0d32dc332ef0bda2ce95cf51353 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 21 Dec 2025 01:39:50 +0000 Subject: [PATCH] refactor(macos): remove manual identity onboarding --- .../macos/Sources/Clawdis/AgentIdentity.swift | 45 ----- .../Sources/Clawdis/AgentWorkspace.swift | 54 +----- .../Sources/Clawdis/ClawdisConfigFile.swift | 23 --- apps/macos/Sources/Clawdis/Onboarding.swift | 175 ++---------------- .../ClawdisIPCTests/AgentIdentityTests.swift | 21 --- docs/onboarding.md | 2 +- 6 files changed, 27 insertions(+), 293 deletions(-) delete mode 100644 apps/macos/Sources/Clawdis/AgentIdentity.swift delete mode 100644 apps/macos/Tests/ClawdisIPCTests/AgentIdentityTests.swift diff --git a/apps/macos/Sources/Clawdis/AgentIdentity.swift b/apps/macos/Sources/Clawdis/AgentIdentity.swift deleted file mode 100644 index c79b4555d..000000000 --- a/apps/macos/Sources/Clawdis/AgentIdentity.swift +++ /dev/null @@ -1,45 +0,0 @@ -import Foundation - -struct AgentIdentity: Codable, Equatable { - var name: String - var theme: String - var emoji: String - - var isEmpty: Bool { - self.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && - self.theme.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && - self.emoji.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty - } -} - -enum AgentIdentityEmoji { - static func suggest(theme: String) -> String { - let normalized = theme.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() - if normalized.isEmpty { return "🦞" } - - let table: [(needle: String, emoji: String)] = [ - ("lobster", "🦞"), - ("sloth", "πŸ¦₯"), - ("octopus", "πŸ™"), - ("crab", "πŸ¦€"), - ("shark", "🦈"), - ("cat", "🐈"), - ("dog", "πŸ•"), - ("owl", "πŸ¦‰"), - ("fox", "🦊"), - ("otter", "🦦"), - ("raccoon", "🦝"), - ("badger", "🦑"), - ("hedgehog", "πŸ¦”"), - ("koala", "🐨"), - ("penguin", "🐧"), - ("frog", "🐸"), - ("bear", "🐻"), - ] - - for entry in table where normalized.contains(entry.needle) { - return entry.emoji - } - return "🦞" - } -} diff --git a/apps/macos/Sources/Clawdis/AgentWorkspace.swift b/apps/macos/Sources/Clawdis/AgentWorkspace.swift index 565666da1..96018a080 100644 --- a/apps/macos/Sources/Clawdis/AgentWorkspace.swift +++ b/apps/macos/Sources/Clawdis/AgentWorkspace.swift @@ -9,8 +9,6 @@ enum AgentWorkspace { static let userFilename = "USER.md" static let bootstrapFilename = "BOOTSTRAP.md" private static let templateDirname = "templates" - static let identityStartMarker = "" - static let identityEndMarker = "" enum BootstrapSafety: Equatable { case safe case unsafe(reason: String) @@ -92,29 +90,15 @@ enum AgentWorkspace { return agentsURL } - static func upsertIdentity(workspaceURL: URL, identity: AgentIdentity) throws { - let agentsURL = try self.bootstrap(workspaceURL: workspaceURL) - var content = (try? String(contentsOf: agentsURL, encoding: .utf8)) ?? "" - let block = self.identityBlock(identity: identity) - - if let start = content.range(of: self.identityStartMarker), - let end = content.range(of: self.identityEndMarker), - start.lowerBound < end.upperBound - { - content.replaceSubrange( - start.lowerBound.. Bool { + let fm = FileManager.default + var isDir: ObjCBool = false + if !fm.fileExists(atPath: workspaceURL.path, isDirectory: &isDir) { + return true } - - try content.write(to: agentsURL, atomically: true, encoding: .utf8) - self.logger.info("Updated identity in \(agentsURL.path, privacy: .public)") + guard isDir.boolValue else { return true } + let bootstrapURL = workspaceURL.appendingPathComponent(self.bootstrapFilename) + return fm.fileExists(atPath: bootstrapURL.path) } static func defaultTemplate() -> String { @@ -301,25 +285,5 @@ enum AgentWorkspace { return trimmed + "\n" } - private static func identityBlock(identity: AgentIdentity) -> String { - let name = identity.name.trimmingCharacters(in: .whitespacesAndNewlines) - let theme = identity.theme.trimmingCharacters(in: .whitespacesAndNewlines) - let emoji = identity.emoji.trimmingCharacters(in: .whitespacesAndNewlines) - - return """ - \(self.identityStartMarker) - - Name: \(name) - - Theme: \(theme) - - Emoji: \(emoji) - \(self.identityEndMarker) - """ - } - - private static func identityInsertRange(in content: String) -> Range? { - if let firstHeading = content.range(of: "\n") { - // Insert after the first line (usually "# AGENTS.md …") - return firstHeading - } - return nil - } + // Identity is written by the agent during the bootstrap ritual. } diff --git a/apps/macos/Sources/Clawdis/ClawdisConfigFile.swift b/apps/macos/Sources/Clawdis/ClawdisConfigFile.swift index ce4418074..49f6557cf 100644 --- a/apps/macos/Sources/Clawdis/ClawdisConfigFile.swift +++ b/apps/macos/Sources/Clawdis/ClawdisConfigFile.swift @@ -80,27 +80,4 @@ enum ClawdisConfigFile { self.saveDict(root) } - static func loadIdentity() -> AgentIdentity? { - let root = self.loadDict() - guard let identity = root["identity"] as? [String: Any] else { return nil } - let name = identity["name"] as? String ?? "" - let theme = identity["theme"] as? String ?? "" - let emoji = identity["emoji"] as? String ?? "" - let result = AgentIdentity(name: name, theme: theme, emoji: emoji) - return result.isEmpty ? nil : result - } - - static func setIdentity(_ identity: AgentIdentity?) { - var root = self.loadDict() - if let identity, !identity.isEmpty { - root["identity"] = [ - "name": identity.name.trimmingCharacters(in: .whitespacesAndNewlines), - "theme": identity.theme.trimmingCharacters(in: .whitespacesAndNewlines), - "emoji": identity.emoji.trimmingCharacters(in: .whitespacesAndNewlines), - ] - } else { - root.removeValue(forKey: "identity") - } - self.saveDict(root) - } } diff --git a/apps/macos/Sources/Clawdis/Onboarding.swift b/apps/macos/Sources/Clawdis/Onboarding.swift index 5857f3d9c..77aba90cd 100644 --- a/apps/macos/Sources/Clawdis/Onboarding.swift +++ b/apps/macos/Sources/Clawdis/Onboarding.swift @@ -68,12 +68,7 @@ struct OnboardingView: View { @State private var anthropicAuthLastPasteboardChangeCount = NSPasteboard.general.changeCount @State private var monitoringAuth = false @State private var authMonitorTask: Task? - @State private var identityName: String = "" - @State private var identityTheme: String = "" - @State private var identityEmoji: String = "" - @State private var identityStatus: String? - @State private var identityApplying = false - @State private var hasIdentity = false + @State private var needsBootstrap = false @State private var didAutoKickoff = false @State private var showAdvancedConnection = false @State private var preferredGatewayID: String? @@ -93,22 +88,22 @@ struct OnboardingView: View { private let permissionsPageIndex = 5 static func pageOrder( for mode: AppState.ConnectionMode, - hasIdentity: Bool) -> [Int] + needsBootstrap: Bool) -> [Int] { switch mode { case .remote: // Remote setup doesn't need local gateway/CLI/workspace setup pages, // and WhatsApp/Telegram setup is optional. - hasIdentity ? [0, 1, 5, 9] : [0, 1, 5, 8, 9] + needsBootstrap ? [0, 1, 5, 8, 9] : [0, 1, 5, 9] case .unconfigured: - hasIdentity ? [0, 1, 9] : [0, 1, 8, 9] + needsBootstrap ? [0, 1, 8, 9] : [0, 1, 9] case .local: - hasIdentity ? [0, 1, 2, 5, 6, 9] : [0, 1, 2, 5, 6, 8, 9] + needsBootstrap ? [0, 1, 2, 5, 6, 8, 9] : [0, 1, 2, 5, 6, 9] } } private var pageOrder: [Int] { - Self.pageOrder(for: self.state.connectionMode, hasIdentity: self.hasIdentity) + Self.pageOrder(for: self.state.connectionMode, needsBootstrap: self.needsBootstrap) } private var pageCount: Int { self.pageOrder.count } @@ -182,7 +177,7 @@ struct OnboardingView: View { self.reconcilePageForModeChange(previousActivePageIndex: oldActive) self.updateDiscoveryMonitoring(for: self.activePageIndex) } - .onChange(of: self.hasIdentity) { _, _ in + .onChange(of: self.needsBootstrap) { _, _ in if self.currentPage >= self.pageOrder.count { self.currentPage = max(0, self.pageOrder.count - 1) } @@ -198,7 +193,7 @@ struct OnboardingView: View { self.loadWorkspaceDefaults() self.ensureDefaultWorkspace() self.refreshAnthropicOAuthStatus() - self.loadIdentityDefaults() + self.refreshBootstrapStatus() self.preferredGatewayID = BridgeDiscoveryPreferences.preferredStableID() } } @@ -230,8 +225,6 @@ struct OnboardingView: View { self.connectionPage() case 2: self.anthropicAuthPage() - case 3: - self.identityPage() case 5: self.permissionsPage() case 6: @@ -731,99 +724,6 @@ struct OnboardingView: View { self.anthropicAuthConnected = status.isConnected } - private func identityPage() -> some View { - self.onboardingPage { - Text("Identity") - .font(.largeTitle.weight(.semibold)) - Text("Name your agent, pick a vibe, and choose an emoji.") - .font(.body) - .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - .frame(maxWidth: 520) - .fixedSize(horizontal: false, vertical: true) - - self.onboardingCard(spacing: 12, padding: 16) { - VStack(alignment: .leading, spacing: 10) { - Text("Agent name") - .font(.headline) - TextField("Clawd", text: self.$identityName) - .textFieldStyle(.roundedBorder) - } - - VStack(alignment: .leading, spacing: 10) { - Text("Theme") - .font(.headline) - TextField("helpful space lobster", text: self.$identityTheme) - .textFieldStyle(.roundedBorder) - } - - VStack(alignment: .leading, spacing: 10) { - Text("Emoji") - .font(.headline) - HStack(spacing: 12) { - TextField("🦞", text: self.$identityEmoji) - .textFieldStyle(.roundedBorder) - .frame(width: 120) - - Button("Suggest") { - let suggested = AgentIdentityEmoji.suggest(theme: self.identityTheme) - self.identityEmoji = suggested - } - .buttonStyle(.bordered) - } - } - - Divider().padding(.vertical, 2) - - VStack(alignment: .leading, spacing: 8) { - Text("Workspace") - .font(.headline) - Text(self.workspacePath.isEmpty ? AgentWorkspace - .displayPath(for: ClawdisConfigFile.defaultWorkspaceURL()) : self.workspacePath) - .font(.callout) - .foregroundStyle(.secondary) - } - - HStack(spacing: 12) { - Button { - Task { await self.applyIdentity() } - } label: { - if self.identityApplying { - ProgressView() - } else { - Text("Save identity") - } - } - .buttonStyle(.borderedProminent) - .disabled(self.identityApplying || self.identityName.trimmingCharacters(in: .whitespacesAndNewlines) - .isEmpty) - - Button("Open workspace") { - let url = AgentWorkspace.resolveWorkspaceURL(from: self.workspacePath) - NSWorkspace.shared.open(url) - } - .buttonStyle(.bordered) - .disabled(self.identityApplying) - } - - Text( - "This writes your identity to `~/.clawdis/clawdis.json` and into `AGENTS.md` " + - "inside the workspace. " + - "Treat that workspace as the agent’s β€œmemory” and consider making it a private git repo.") - .font(.caption) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) - - if let status = self.identityStatus { - Text(status) - .font(.caption) - .foregroundStyle(.secondary) - .fixedSize(horizontal: false, vertical: true) - } - } - } - } - private func permissionsPage() -> some View { self.onboardingPage { Text("Grant permissions") @@ -1372,6 +1272,7 @@ struct OnboardingView: View { let configured = ClawdisConfigFile.inboundWorkspace() let url = AgentWorkspace.resolveWorkspaceURL(from: configured) self.workspacePath = AgentWorkspace.displayPath(for: url) + self.refreshBootstrapStatus() } private func ensureDefaultWorkspace() { @@ -1391,26 +1292,14 @@ struct OnboardingView: View { case let .unsafe(reason): self.workspaceStatus = "Workspace not touched: \(reason)" } + self.refreshBootstrapStatus() } - private func loadIdentityDefaults() { - if let identity = ClawdisConfigFile.loadIdentity() { - self.identityName = identity.name - self.identityTheme = identity.theme - self.identityEmoji = identity.emoji - self.hasIdentity = !identity.isEmpty - return - } - - self.hasIdentity = false - if self.identityName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - self.identityName = "Clawd" - } - if self.identityTheme.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - self.identityTheme = "helpful space lobster" - } - if self.identityEmoji.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - self.identityEmoji = AgentIdentityEmoji.suggest(theme: self.identityTheme) + private func refreshBootstrapStatus() { + let url = AgentWorkspace.resolveWorkspaceURL(from: self.workspacePath) + self.needsBootstrap = AgentWorkspace.needsBootstrap(workspaceURL: url) + if self.needsBootstrap { + self.didAutoKickoff = false } } @@ -1438,45 +1327,15 @@ struct OnboardingView: View { _ = try AgentWorkspace.bootstrap(workspaceURL: url) self.workspacePath = AgentWorkspace.displayPath(for: url) self.workspaceStatus = "Workspace ready at \(self.workspacePath)" + self.refreshBootstrapStatus() } catch { self.workspaceStatus = "Failed to create workspace: \(error.localizedDescription)" } } - private func applyIdentity() async { - guard !self.identityApplying else { return } - self.identityApplying = true - defer { self.identityApplying = false } - - if self.identityName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - self.identityStatus = "Please enter a name first." - return - } - - var identity = AgentIdentity( - name: self.identityName, - theme: self.identityTheme, - emoji: self.identityEmoji) - - if identity.emoji.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { - identity.emoji = AgentIdentityEmoji.suggest(theme: identity.theme) - self.identityEmoji = identity.emoji - } - - do { - let workspaceURL = AgentWorkspace.resolveWorkspaceURL(from: self.workspacePath) - try AgentWorkspace.upsertIdentity(workspaceURL: workspaceURL, identity: identity) - ClawdisConfigFile.setInboundWorkspace(AgentWorkspace.displayPath(for: workspaceURL)) - ClawdisConfigFile.setIdentity(identity) - self.identityStatus = "Saved identity to AGENTS.md and ~/.clawdis/clawdis.json" - } catch { - self.identityStatus = "Failed to save identity: \(error.localizedDescription)" - } - } - private func maybeKickoffOnboardingChat(for pageIndex: Int) { guard pageIndex == self.onboardingChatPageIndex else { return } - guard !self.hasIdentity else { return } + guard self.needsBootstrap else { return } guard !self.didAutoKickoff else { return } self.didAutoKickoff = true diff --git a/apps/macos/Tests/ClawdisIPCTests/AgentIdentityTests.swift b/apps/macos/Tests/ClawdisIPCTests/AgentIdentityTests.swift deleted file mode 100644 index b326e6146..000000000 --- a/apps/macos/Tests/ClawdisIPCTests/AgentIdentityTests.swift +++ /dev/null @@ -1,21 +0,0 @@ -import Foundation -import Testing -@testable import Clawdis - -@Suite -struct AgentIdentityTests { - @Test - func isEmptyTreatsWhitespaceAsEmpty() { - #expect(AgentIdentity(name: " ", theme: "\n", emoji: "\t").isEmpty == true) - #expect(AgentIdentity(name: "Pi", theme: "", emoji: "").isEmpty == false) - } - - @Test - func emojiSuggestMatchesKnownThemes() { - #expect(AgentIdentityEmoji.suggest(theme: "") == "🦞") - #expect(AgentIdentityEmoji.suggest(theme: "shark") == "🦈") - #expect(AgentIdentityEmoji.suggest(theme: " Octopus helper ") == "πŸ™") - #expect(AgentIdentityEmoji.suggest(theme: "unknown") == "🦞") - } -} - diff --git a/docs/onboarding.md b/docs/onboarding.md index ab82062ba..953c9ccdb 100644 --- a/docs/onboarding.md +++ b/docs/onboarding.md @@ -86,7 +86,7 @@ This onboarding chat is where the agent: - asks how the user wants to talk (web-only / WhatsApp / Telegram) - guides linking steps (including showing a QR inline for WhatsApp via the `whatsapp_login` tool) -If the agent identity already exists in `~/.clawdis/clawdis.json`, the onboarding chat step is skipped. +If the workspace bootstrap is already complete (BOOTSTRAP.md removed), the onboarding chat step is skipped. Once setup is complete, the user can switch to the normal chat (`main`) via the menu bar panel.