diff --git a/apps/macos/Sources/Clawdis/GatewayEnvironment.swift b/apps/macos/Sources/Clawdis/GatewayEnvironment.swift index f15bce592..d3196439e 100644 --- a/apps/macos/Sources/Clawdis/GatewayEnvironment.swift +++ b/apps/macos/Sources/Clawdis/GatewayEnvironment.swift @@ -260,12 +260,33 @@ enum GatewayEnvironment { } 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)") } else { - let detail = response.message ?? "install failed" - statusHandler("Install failed: \(detail)") + if response.timedOut { + 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))") + } } } diff --git a/apps/macos/Sources/Clawdis/Onboarding.swift b/apps/macos/Sources/Clawdis/Onboarding.swift index 07dfb9776..96ebebefc 100644 --- a/apps/macos/Sources/Clawdis/Onboarding.swift +++ b/apps/macos/Sources/Clawdis/Onboarding.swift @@ -80,6 +80,8 @@ struct OnboardingView: View { @State var preferredGatewayID: String? @State var gatewayDiscovery: GatewayDiscoveryModel @State var onboardingChatModel: ClawdisChatViewModel + @State var onboardingSkillsModel = SkillsSettingsModel() + @State var didLoadOnboardingSkills = false @State var localGatewayProbe: LocalGatewayProbe? @Bindable var state: AppState var permissionMonitor: PermissionMonitor diff --git a/apps/macos/Sources/Clawdis/OnboardingView+Pages.swift b/apps/macos/Sources/Clawdis/OnboardingView+Pages.swift index b72a8e10c..7d863ebe7 100644 --- a/apps/macos/Sources/Clawdis/OnboardingView+Pages.swift +++ b/apps/macos/Sources/Clawdis/OnboardingView+Pages.swift @@ -671,11 +671,87 @@ extension OnboardingView { { self.openSettings(tab: .skills) } + self.skillsOverview Toggle("Launch at login", isOn: self.$state.launchAtLogin) .onChange(of: self.state.launchAtLogin) { _, newValue in 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) + } + } } }