Skip to content

Commit a710366

Browse files
BunsDevNova
andcommitted
feat(ui): Control UI polish — skills revamp, markdown preview, agent workspace, macOS config tree (#53411) thanks @BunsDev
Co-authored-by: BunsDev <[email protected]> Co-authored-by: Nova <[email protected]>
1 parent ecb3aa7 commit a710366

40 files changed

+1521
-649
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ Docs: https://docs.openclaw.ai
88

99
### Changes
1010

11+
- Control UI/markdown preview: restyle the agent workspace file preview dialog with a frosted backdrop, sized panel, and styled header, and integrate `@create-markdown/preview` v2 system theme for rich markdown rendering (headings, tables, code blocks, callouts, blockquotes) that auto-adapts to the app's light/dark design tokens. (#53411) Thanks @BunsDev.
12+
- Skills/install metadata: add one-click install recipes to bundled skills (coding-agent, gh-issues, openai-whisper-api, session-logs, tmux, trello, weather) so the CLI and Control UI can offer dependency installation when requirements are missing. (#53411) Thanks @BunsDev.
13+
- CLI/skills: soften missing-requirements label from "missing" to "needs setup" and surface API key setup guidance (where to get a key, CLI save command, storage path) in `openclaw skills info` output. (#53411) Thanks @BunsDev.
14+
- Control UI/skills: add status-filter tabs (All / Ready / Needs Setup / Disabled) with counts, replace inline skill cards with a click-to-detail dialog showing requirements, toggle switch, install action, API key entry, source metadata, and homepage link. (#53411) Thanks @BunsDev.
15+
- Control UI/agents: convert agent workspace file rows to expandable `<details>` with lazy-loaded inline markdown preview, and add comprehensive `.sidebar-markdown` styles for headings, lists, code blocks, tables, blockquotes, and details/summary elements. (#53411) Thanks @BunsDev.
16+
- Control UI/agents: add a "Not set" placeholder to the default agent model selector dropdown. (#53411) Thanks @BunsDev.
17+
- macOS app/config: replace horizontal pill-based subsection navigation with a collapsible tree sidebar using disclosure chevrons and indented subsection rows. (#53411) Thanks @BunsDev.
18+
- macOS app/skills: add "Get your key" homepage link and storage-path hint to the API key editor dialog, and show the config path in save confirmation messages. (#53411) Thanks @BunsDev.
19+
1120
### Fixes
1221

1322
- Feishu/docx block ordering: preserve the document tree order from `docx.document.convert` when inserting blocks, fixing heading/paragraph/list misordering in newly written Feishu documents. (#40524) Thanks @TaoXieSZ.

apps/macos/Sources/OpenClaw/ConfigSettings.swift

Lines changed: 57 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ extension ConfigSettings {
7373

7474
private var sidebar: some View {
7575
SettingsSidebarScroll {
76-
LazyVStack(alignment: .leading, spacing: 8) {
76+
LazyVStack(alignment: .leading, spacing: 4) {
7777
if self.sections.isEmpty {
7878
Text("No config sections available.")
7979
.font(.caption)
@@ -82,7 +82,7 @@ extension ConfigSettings {
8282
.padding(.vertical, 4)
8383
} else {
8484
ForEach(self.sections) { section in
85-
self.sidebarRow(section)
85+
self.sidebarSection(section)
8686
}
8787
}
8888
}
@@ -128,7 +128,6 @@ extension ConfigSettings {
128128
}
129129
self.actionRow
130130
self.sectionHeader(section)
131-
self.subsectionNav(section)
132131
self.sectionForm(section)
133132
if self.store.configDirty, !self.isNixMode {
134133
Text("Unsaved changes")
@@ -182,76 +181,74 @@ extension ConfigSettings {
182181
.buttonStyle(.bordered)
183182
}
184183

185-
private func sidebarRow(_ section: ConfigSection) -> some View {
186-
let isSelected = self.activeSectionKey == section.key
187-
return Button {
188-
self.selectSection(section)
189-
} label: {
190-
VStack(alignment: .leading, spacing: 2) {
191-
Text(section.label)
192-
if let help = section.help {
193-
Text(help)
194-
.font(.caption)
195-
.foregroundStyle(.secondary)
196-
.lineLimit(2)
184+
private func sidebarSection(_ section: ConfigSection) -> some View {
185+
let isExpanded = self.activeSectionKey == section.key
186+
let subsections = isExpanded ? self.resolveSubsections(for: section) : []
187+
188+
return VStack(alignment: .leading, spacing: 2) {
189+
Button {
190+
self.selectSection(section)
191+
} label: {
192+
HStack(spacing: 6) {
193+
Image(systemName: "chevron.right")
194+
.font(.caption2.weight(.semibold))
195+
.foregroundStyle(.tertiary)
196+
.rotationEffect(.degrees(isExpanded ? 90 : 0))
197+
Text(section.label)
198+
.lineLimit(1)
197199
}
200+
.padding(.vertical, 5)
201+
.padding(.horizontal, 8)
202+
.frame(maxWidth: .infinity, alignment: .leading)
203+
.background(isExpanded && subsections.isEmpty
204+
? Color.accentColor.opacity(0.18)
205+
: Color.clear)
206+
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
207+
.contentShape(Rectangle())
198208
}
199-
.padding(.vertical, 6)
200-
.padding(.horizontal, 8)
201-
.frame(maxWidth: .infinity, alignment: .leading)
202-
.background(isSelected ? Color.accentColor.opacity(0.18) : Color.clear)
203-
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
204-
.background(Color.clear)
209+
.buttonStyle(.plain)
205210
.contentShape(Rectangle())
206-
}
207-
.frame(maxWidth: .infinity, alignment: .leading)
208-
.buttonStyle(.plain)
209-
.contentShape(Rectangle())
210-
}
211211

212-
@ViewBuilder
213-
private func subsectionNav(_ section: ConfigSection) -> some View {
214-
let subsections = self.resolveSubsections(for: section)
215-
if subsections.isEmpty {
216-
EmptyView()
217-
} else {
218-
ScrollView(.horizontal, showsIndicators: false) {
219-
HStack(spacing: 8) {
220-
self.subsectionButton(
221-
title: "All",
222-
isSelected: self.activeSubsection == .all)
223-
{
224-
self.activeSubsection = .all
225-
}
226-
ForEach(subsections) { subsection in
227-
self.subsectionButton(
228-
title: subsection.label,
229-
isSelected: self.activeSubsection == .key(subsection.key))
230-
{
231-
self.activeSubsection = .key(subsection.key)
232-
}
212+
if isExpanded, !subsections.isEmpty {
213+
VStack(alignment: .leading, spacing: 1) {
214+
self.sidebarSubRow(title: "All", key: nil, sectionKey: section.key)
215+
ForEach(subsections) { sub in
216+
self.sidebarSubRow(title: sub.label, key: sub.key, sectionKey: section.key)
233217
}
234218
}
235-
.padding(.vertical, 2)
219+
.padding(.leading, 20)
220+
.transition(.opacity.combined(with: .move(edge: .top)))
236221
}
237222
}
223+
.animation(.easeInOut(duration: 0.18), value: isExpanded)
238224
}
239225

240-
private func subsectionButton(
241-
title: String,
242-
isSelected: Bool,
243-
action: @escaping () -> Void) -> some View
244-
{
245-
Button(action: action) {
226+
private func sidebarSubRow(title: String, key: String?, sectionKey: String) -> some View {
227+
let isSelected: Bool = {
228+
guard self.activeSectionKey == sectionKey else { return false }
229+
if let key { return self.activeSubsection == .key(key) }
230+
return self.activeSubsection == .all
231+
}()
232+
233+
return Button {
234+
if let key {
235+
self.activeSubsection = .key(key)
236+
} else {
237+
self.activeSubsection = .all
238+
}
239+
} label: {
246240
Text(title)
247-
.font(.callout.weight(.semibold))
248-
.foregroundStyle(isSelected ? Color.accentColor : .primary)
249-
.padding(.horizontal, 10)
250-
.padding(.vertical, 6)
251-
.background(isSelected ? Color.accentColor.opacity(0.18) : Color(nsColor: .controlBackgroundColor))
252-
.clipShape(Capsule())
241+
.font(.callout)
242+
.lineLimit(1)
243+
.padding(.vertical, 4)
244+
.padding(.horizontal, 8)
245+
.frame(maxWidth: .infinity, alignment: .leading)
246+
.background(isSelected ? Color.accentColor.opacity(0.18) : Color.clear)
247+
.clipShape(RoundedRectangle(cornerRadius: 7, style: .continuous))
248+
.contentShape(Rectangle())
253249
}
254250
.buttonStyle(.plain)
251+
.contentShape(Rectangle())
255252
}
256253

257254
private func sectionForm(_ section: ConfigSection) -> some View {

apps/macos/Sources/OpenClaw/SkillsSettings.swift

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ struct SkillsSettings: View {
9595
skillKey: skill.skillKey,
9696
skillName: skill.name,
9797
envKey: envKey,
98-
isPrimary: isPrimary)
98+
isPrimary: isPrimary,
99+
homepage: skill.homepage)
99100
})
100101
}
101102
if !self.model.skills.isEmpty, self.filteredSkills.isEmpty {
@@ -258,8 +259,12 @@ private struct SkillRow: View {
258259
guard let raw = self.skill.homepage?.trimmingCharacters(in: .whitespacesAndNewlines) else {
259260
return nil
260261
}
261-
guard !raw.isEmpty else { return nil }
262-
return URL(string: raw)
262+
guard !raw.isEmpty, let url = URL(string: raw),
263+
let scheme = url.scheme?.lowercased(),
264+
scheme == "http" || scheme == "https" else {
265+
return nil
266+
}
267+
return url
263268
}
264269

265270
private var enabledBinding: Binding<Bool> {
@@ -428,6 +433,7 @@ private struct EnvEditorState: Identifiable {
428433
let skillName: String
429434
let envKey: String
430435
let isPrimary: Bool
436+
let homepage: String?
431437

432438
var id: String {
433439
"\(self.skillKey)::\(self.envKey)"
@@ -447,8 +453,15 @@ private struct EnvEditorView: View {
447453
Text(self.subtitle)
448454
.font(.subheadline)
449455
.foregroundStyle(.secondary)
456+
if let homepageUrl = self.homepageUrl {
457+
Link("Get your key →", destination: homepageUrl)
458+
.font(.caption)
459+
}
450460
SecureField(self.editor.envKey, text: self.$value)
451461
.textFieldStyle(.roundedBorder)
462+
Text("Saved to openclaw.json under skills.entries.\(self.editor.skillKey)")
463+
.font(.caption2)
464+
.foregroundStyle(.tertiary)
452465
HStack {
453466
Button("Cancel") { self.dismiss() }
454467
Spacer()
@@ -464,6 +477,18 @@ private struct EnvEditorView: View {
464477
.frame(width: 420)
465478
}
466479

480+
private var homepageUrl: URL? {
481+
guard let raw = self.editor.homepage?.trimmingCharacters(in: .whitespacesAndNewlines) else {
482+
return nil
483+
}
484+
guard !raw.isEmpty, let url = URL(string: raw),
485+
let scheme = url.scheme?.lowercased(),
486+
scheme == "http" || scheme == "https" else {
487+
return nil
488+
}
489+
return url
490+
}
491+
467492
private var title: String {
468493
self.editor.isPrimary ? "Set API Key" : "Set Environment Variable"
469494
}
@@ -539,12 +564,12 @@ final class SkillsSettingsModel {
539564
_ = try await GatewayConnection.shared.skillsUpdate(
540565
skillKey: skillKey,
541566
apiKey: value)
542-
self.statusMessage = "Saved API key"
567+
self.statusMessage = "Saved API key — stored in openclaw.json (skills.entries.\(skillKey))"
543568
} else {
544569
_ = try await GatewayConnection.shared.skillsUpdate(
545570
skillKey: skillKey,
546571
env: [envKey: value])
547-
self.statusMessage = "Saved \(envKey)"
572+
self.statusMessage = "Saved \(envKey) — stored in openclaw.json (skills.entries.\(skillKey).env)"
548573
}
549574
} catch {
550575
self.statusMessage = error.localizedDescription
@@ -608,7 +633,8 @@ extension SkillsSettings {
608633
skillKey: "test",
609634
skillName: "Test Skill",
610635
envKey: "API_KEY",
611-
isPrimary: true),
636+
isPrimary: true,
637+
homepage: "https://example.com"),
612638
onSave: { _ in })
613639
_ = editor.body
614640
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -834,7 +834,7 @@
834834
"engines": {
835835
"node": ">=22.16.0"
836836
},
837-
"packageManager": "pnpm@10.23.0",
837+
"packageManager": "pnpm@10.32.1",
838838
"pnpm": {
839839
"minimumReleaseAge": 2880,
840840
"overrides": {

0 commit comments

Comments
 (0)