feat(macos): show skills in onboarding
This commit is contained in:
@@ -260,12 +260,33 @@ enum GatewayEnvironment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
statusHandler("Installing clawdis@\(target) via \(label)…")
|
statusHandler("Installing clawdis@\(target) via \(label)…")
|
||||||
let response = await ShellExecutor.run(command: cmd, cwd: nil, env: ["PATH": preferred], timeout: 300)
|
|
||||||
if response.ok {
|
func summarize(_ text: String) -> String? {
|
||||||
|
let lines = text
|
||||||
|
.split(whereSeparator: \.isNewline)
|
||||||
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
guard let last = lines.last else { return nil }
|
||||||
|
let normalized = last.replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression)
|
||||||
|
return normalized.count > 200 ? String(normalized.prefix(199)) + "…" : normalized
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = await ShellExecutor.runDetailed(command: cmd, cwd: nil, env: ["PATH": preferred], timeout: 300)
|
||||||
|
if response.success {
|
||||||
statusHandler("Installed clawdis@\(target)")
|
statusHandler("Installed clawdis@\(target)")
|
||||||
} else {
|
} else {
|
||||||
let detail = response.message ?? "install failed"
|
if response.timedOut {
|
||||||
statusHandler("Install failed: \(detail)")
|
statusHandler("Install failed: timed out. Check your internet connection and try again.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let exit = response.exitCode.map { "exit \($0)" } ?? (response.errorMessage ?? "failed")
|
||||||
|
let detail = summarize(response.stderr) ?? summarize(response.stdout)
|
||||||
|
if let detail {
|
||||||
|
statusHandler("Install failed (\(exit)): \(detail)")
|
||||||
|
} else {
|
||||||
|
statusHandler("Install failed (\(exit))")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -80,6 +80,8 @@ struct OnboardingView: View {
|
|||||||
@State var preferredGatewayID: String?
|
@State var preferredGatewayID: String?
|
||||||
@State var gatewayDiscovery: GatewayDiscoveryModel
|
@State var gatewayDiscovery: GatewayDiscoveryModel
|
||||||
@State var onboardingChatModel: ClawdisChatViewModel
|
@State var onboardingChatModel: ClawdisChatViewModel
|
||||||
|
@State var onboardingSkillsModel = SkillsSettingsModel()
|
||||||
|
@State var didLoadOnboardingSkills = false
|
||||||
@State var localGatewayProbe: LocalGatewayProbe?
|
@State var localGatewayProbe: LocalGatewayProbe?
|
||||||
@Bindable var state: AppState
|
@Bindable var state: AppState
|
||||||
var permissionMonitor: PermissionMonitor
|
var permissionMonitor: PermissionMonitor
|
||||||
|
|||||||
@@ -671,11 +671,87 @@ extension OnboardingView {
|
|||||||
{
|
{
|
||||||
self.openSettings(tab: .skills)
|
self.openSettings(tab: .skills)
|
||||||
}
|
}
|
||||||
|
self.skillsOverview
|
||||||
Toggle("Launch at login", isOn: self.$state.launchAtLogin)
|
Toggle("Launch at login", isOn: self.$state.launchAtLogin)
|
||||||
.onChange(of: self.state.launchAtLogin) { _, newValue in
|
.onChange(of: self.state.launchAtLogin) { _, newValue in
|
||||||
AppStateStore.updateLaunchAtLogin(enabled: newValue)
|
AppStateStore.updateLaunchAtLogin(enabled: newValue)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.task { await self.maybeLoadOnboardingSkills() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func maybeLoadOnboardingSkills() async {
|
||||||
|
guard !self.didLoadOnboardingSkills else { return }
|
||||||
|
self.didLoadOnboardingSkills = true
|
||||||
|
await self.onboardingSkillsModel.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
private var skillsOverview: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
Divider()
|
||||||
|
.padding(.vertical, 6)
|
||||||
|
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
Text("Skills included")
|
||||||
|
.font(.headline)
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
if self.onboardingSkillsModel.isLoading {
|
||||||
|
ProgressView()
|
||||||
|
.controlSize(.small)
|
||||||
|
} else {
|
||||||
|
Button("Refresh") {
|
||||||
|
Task { await self.onboardingSkillsModel.refresh() }
|
||||||
|
}
|
||||||
|
.buttonStyle(.link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error = self.onboardingSkillsModel.error {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text("Couldn’t load skills from the Gateway.")
|
||||||
|
.font(.footnote.weight(.semibold))
|
||||||
|
.foregroundStyle(.orange)
|
||||||
|
Text("Make sure the Gateway is running and connected, then hit Refresh (or open Settings → Skills).")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
Text("Details: \(error)")
|
||||||
|
.font(.caption.monospaced())
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
} else if self.onboardingSkillsModel.skills.isEmpty {
|
||||||
|
Text("No skills reported yet.")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
} else {
|
||||||
|
ScrollView {
|
||||||
|
LazyVStack(alignment: .leading, spacing: 10) {
|
||||||
|
ForEach(self.onboardingSkillsModel.skills) { skill in
|
||||||
|
HStack(alignment: .top, spacing: 10) {
|
||||||
|
Text(skill.emoji ?? "✨")
|
||||||
|
.font(.callout)
|
||||||
|
.frame(width: 22, alignment: .leading)
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(skill.name)
|
||||||
|
.font(.callout.weight(.semibold))
|
||||||
|
Text(skill.description)
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(10)
|
||||||
|
.background(
|
||||||
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
|
.fill(Color(NSColor.windowBackgroundColor)))
|
||||||
|
}
|
||||||
|
.frame(maxHeight: 160)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user