refactor(macos): remove manual identity onboarding
parent
fb259e8a50
commit
5b25eeb449
|
|
@ -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 "🦞"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -9,8 +9,6 @@ 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"
|
||||||
static let identityStartMarker = "<!-- clawdis:identity:start -->"
|
|
||||||
static let identityEndMarker = "<!-- clawdis:identity:end -->"
|
|
||||||
enum BootstrapSafety: Equatable {
|
enum BootstrapSafety: Equatable {
|
||||||
case safe
|
case safe
|
||||||
case unsafe(reason: String)
|
case unsafe(reason: String)
|
||||||
|
|
@ -92,29 +90,15 @@ enum AgentWorkspace {
|
||||||
return agentsURL
|
return agentsURL
|
||||||
}
|
}
|
||||||
|
|
||||||
static func upsertIdentity(workspaceURL: URL, identity: AgentIdentity) throws {
|
static func needsBootstrap(workspaceURL: URL) -> Bool {
|
||||||
let agentsURL = try self.bootstrap(workspaceURL: workspaceURL)
|
let fm = FileManager.default
|
||||||
var content = (try? String(contentsOf: agentsURL, encoding: .utf8)) ?? ""
|
var isDir: ObjCBool = false
|
||||||
let block = self.identityBlock(identity: identity)
|
if !fm.fileExists(atPath: workspaceURL.path, isDirectory: &isDir) {
|
||||||
|
return true
|
||||||
if let start = content.range(of: self.identityStartMarker),
|
|
||||||
let end = content.range(of: self.identityEndMarker),
|
|
||||||
start.lowerBound < end.upperBound
|
|
||||||
{
|
|
||||||
content.replaceSubrange(
|
|
||||||
start.lowerBound..<end.upperBound,
|
|
||||||
with: block.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
||||||
} else if let insert = self.identityInsertRange(in: content) {
|
|
||||||
content.insert(contentsOf: "\n\n## Identity\n\(block)\n", at: insert.upperBound)
|
|
||||||
} else {
|
|
||||||
content = [content.trimmingCharacters(in: .whitespacesAndNewlines), "## Identity\n\(block)"]
|
|
||||||
.filter { !$0.isEmpty }
|
|
||||||
.joined(separator: "\n\n")
|
|
||||||
.appending("\n")
|
|
||||||
}
|
}
|
||||||
|
guard isDir.boolValue else { return true }
|
||||||
try content.write(to: agentsURL, atomically: true, encoding: .utf8)
|
let bootstrapURL = workspaceURL.appendingPathComponent(self.bootstrapFilename)
|
||||||
self.logger.info("Updated identity in \(agentsURL.path, privacy: .public)")
|
return fm.fileExists(atPath: bootstrapURL.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func defaultTemplate() -> String {
|
static func defaultTemplate() -> String {
|
||||||
|
|
@ -301,25 +285,5 @@ enum AgentWorkspace {
|
||||||
return trimmed + "\n"
|
return trimmed + "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func identityBlock(identity: AgentIdentity) -> String {
|
// Identity is written by the agent during the bootstrap ritual.
|
||||||
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<String.Index>? {
|
|
||||||
if let firstHeading = content.range(of: "\n") {
|
|
||||||
// Insert after the first line (usually "# AGENTS.md …")
|
|
||||||
return firstHeading
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -80,27 +80,4 @@ enum ClawdisConfigFile {
|
||||||
self.saveDict(root)
|
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -68,12 +68,7 @@ struct OnboardingView: View {
|
||||||
@State private var anthropicAuthLastPasteboardChangeCount = NSPasteboard.general.changeCount
|
@State private var anthropicAuthLastPasteboardChangeCount = NSPasteboard.general.changeCount
|
||||||
@State private var monitoringAuth = false
|
@State private var monitoringAuth = false
|
||||||
@State private var authMonitorTask: Task<Void, Never>?
|
@State private var authMonitorTask: Task<Void, Never>?
|
||||||
@State private var identityName: String = ""
|
@State private var needsBootstrap = false
|
||||||
@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 didAutoKickoff = false
|
@State private var didAutoKickoff = false
|
||||||
@State private var showAdvancedConnection = false
|
@State private var showAdvancedConnection = false
|
||||||
@State private var preferredGatewayID: String?
|
@State private var preferredGatewayID: String?
|
||||||
|
|
@ -93,22 +88,22 @@ struct OnboardingView: View {
|
||||||
private let permissionsPageIndex = 5
|
private let permissionsPageIndex = 5
|
||||||
static func pageOrder(
|
static func pageOrder(
|
||||||
for mode: AppState.ConnectionMode,
|
for mode: AppState.ConnectionMode,
|
||||||
hasIdentity: Bool) -> [Int]
|
needsBootstrap: Bool) -> [Int]
|
||||||
{
|
{
|
||||||
switch mode {
|
switch mode {
|
||||||
case .remote:
|
case .remote:
|
||||||
// Remote setup doesn't need local gateway/CLI/workspace setup pages,
|
// Remote setup doesn't need local gateway/CLI/workspace setup pages,
|
||||||
// and WhatsApp/Telegram setup is optional.
|
// 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:
|
case .unconfigured:
|
||||||
hasIdentity ? [0, 1, 9] : [0, 1, 8, 9]
|
needsBootstrap ? [0, 1, 8, 9] : [0, 1, 9]
|
||||||
case .local:
|
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] {
|
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 }
|
private var pageCount: Int { self.pageOrder.count }
|
||||||
|
|
@ -182,7 +177,7 @@ struct OnboardingView: View {
|
||||||
self.reconcilePageForModeChange(previousActivePageIndex: oldActive)
|
self.reconcilePageForModeChange(previousActivePageIndex: oldActive)
|
||||||
self.updateDiscoveryMonitoring(for: self.activePageIndex)
|
self.updateDiscoveryMonitoring(for: self.activePageIndex)
|
||||||
}
|
}
|
||||||
.onChange(of: self.hasIdentity) { _, _ in
|
.onChange(of: self.needsBootstrap) { _, _ in
|
||||||
if self.currentPage >= self.pageOrder.count {
|
if self.currentPage >= self.pageOrder.count {
|
||||||
self.currentPage = max(0, self.pageOrder.count - 1)
|
self.currentPage = max(0, self.pageOrder.count - 1)
|
||||||
}
|
}
|
||||||
|
|
@ -198,7 +193,7 @@ struct OnboardingView: View {
|
||||||
self.loadWorkspaceDefaults()
|
self.loadWorkspaceDefaults()
|
||||||
self.ensureDefaultWorkspace()
|
self.ensureDefaultWorkspace()
|
||||||
self.refreshAnthropicOAuthStatus()
|
self.refreshAnthropicOAuthStatus()
|
||||||
self.loadIdentityDefaults()
|
self.refreshBootstrapStatus()
|
||||||
self.preferredGatewayID = BridgeDiscoveryPreferences.preferredStableID()
|
self.preferredGatewayID = BridgeDiscoveryPreferences.preferredStableID()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -230,8 +225,6 @@ struct OnboardingView: View {
|
||||||
self.connectionPage()
|
self.connectionPage()
|
||||||
case 2:
|
case 2:
|
||||||
self.anthropicAuthPage()
|
self.anthropicAuthPage()
|
||||||
case 3:
|
|
||||||
self.identityPage()
|
|
||||||
case 5:
|
case 5:
|
||||||
self.permissionsPage()
|
self.permissionsPage()
|
||||||
case 6:
|
case 6:
|
||||||
|
|
@ -731,99 +724,6 @@ struct OnboardingView: View {
|
||||||
self.anthropicAuthConnected = status.isConnected
|
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 {
|
private func permissionsPage() -> some View {
|
||||||
self.onboardingPage {
|
self.onboardingPage {
|
||||||
Text("Grant permissions")
|
Text("Grant permissions")
|
||||||
|
|
@ -1372,6 +1272,7 @@ struct OnboardingView: View {
|
||||||
let configured = ClawdisConfigFile.inboundWorkspace()
|
let configured = ClawdisConfigFile.inboundWorkspace()
|
||||||
let url = AgentWorkspace.resolveWorkspaceURL(from: configured)
|
let url = AgentWorkspace.resolveWorkspaceURL(from: configured)
|
||||||
self.workspacePath = AgentWorkspace.displayPath(for: url)
|
self.workspacePath = AgentWorkspace.displayPath(for: url)
|
||||||
|
self.refreshBootstrapStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func ensureDefaultWorkspace() {
|
private func ensureDefaultWorkspace() {
|
||||||
|
|
@ -1391,26 +1292,14 @@ struct OnboardingView: View {
|
||||||
case let .unsafe(reason):
|
case let .unsafe(reason):
|
||||||
self.workspaceStatus = "Workspace not touched: \(reason)"
|
self.workspaceStatus = "Workspace not touched: \(reason)"
|
||||||
}
|
}
|
||||||
|
self.refreshBootstrapStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadIdentityDefaults() {
|
private func refreshBootstrapStatus() {
|
||||||
if let identity = ClawdisConfigFile.loadIdentity() {
|
let url = AgentWorkspace.resolveWorkspaceURL(from: self.workspacePath)
|
||||||
self.identityName = identity.name
|
self.needsBootstrap = AgentWorkspace.needsBootstrap(workspaceURL: url)
|
||||||
self.identityTheme = identity.theme
|
if self.needsBootstrap {
|
||||||
self.identityEmoji = identity.emoji
|
self.didAutoKickoff = false
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1438,45 +1327,15 @@ struct OnboardingView: View {
|
||||||
_ = try AgentWorkspace.bootstrap(workspaceURL: url)
|
_ = try AgentWorkspace.bootstrap(workspaceURL: url)
|
||||||
self.workspacePath = AgentWorkspace.displayPath(for: url)
|
self.workspacePath = AgentWorkspace.displayPath(for: url)
|
||||||
self.workspaceStatus = "Workspace ready at \(self.workspacePath)"
|
self.workspaceStatus = "Workspace ready at \(self.workspacePath)"
|
||||||
|
self.refreshBootstrapStatus()
|
||||||
} catch {
|
} catch {
|
||||||
self.workspaceStatus = "Failed to create workspace: \(error.localizedDescription)"
|
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) {
|
private func maybeKickoffOnboardingChat(for pageIndex: Int) {
|
||||||
guard pageIndex == self.onboardingChatPageIndex else { return }
|
guard pageIndex == self.onboardingChatPageIndex else { return }
|
||||||
guard !self.hasIdentity else { return }
|
guard self.needsBootstrap else { return }
|
||||||
guard !self.didAutoKickoff else { return }
|
guard !self.didAutoKickoff else { return }
|
||||||
self.didAutoKickoff = true
|
self.didAutoKickoff = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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") == "🦞")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -86,7 +86,7 @@ This onboarding chat is where the agent:
|
||||||
- asks how the user wants to talk (web-only / WhatsApp / Telegram)
|
- 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)
|
- 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.
|
Once setup is complete, the user can switch to the normal chat (`main`) via the menu bar panel.
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue