diff --git a/CHANGELOG.md b/CHANGELOG.md index db245334cd5..0d4a71fc084 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,15 @@ Docs: https://docs.openclaw.ai ### Changes +- 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. +- 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. +- 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. +- 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. +- Control UI/agents: convert agent workspace file rows to expandable `
` 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. +- Control UI/agents: add a "Not set" placeholder to the default agent model selector dropdown. (#53411) Thanks @BunsDev. +- macOS app/config: replace horizontal pill-based subsection navigation with a collapsible tree sidebar using disclosure chevrons and indented subsection rows. (#53411) Thanks @BunsDev. +- 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. + ### Fixes - 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. diff --git a/apps/macos/Sources/OpenClaw/ConfigSettings.swift b/apps/macos/Sources/OpenClaw/ConfigSettings.swift index d5f3ee7343a..e2c20c2de6b 100644 --- a/apps/macos/Sources/OpenClaw/ConfigSettings.swift +++ b/apps/macos/Sources/OpenClaw/ConfigSettings.swift @@ -73,7 +73,7 @@ extension ConfigSettings { private var sidebar: some View { SettingsSidebarScroll { - LazyVStack(alignment: .leading, spacing: 8) { + LazyVStack(alignment: .leading, spacing: 4) { if self.sections.isEmpty { Text("No config sections available.") .font(.caption) @@ -82,7 +82,7 @@ extension ConfigSettings { .padding(.vertical, 4) } else { ForEach(self.sections) { section in - self.sidebarRow(section) + self.sidebarSection(section) } } } @@ -128,7 +128,6 @@ extension ConfigSettings { } self.actionRow self.sectionHeader(section) - self.subsectionNav(section) self.sectionForm(section) if self.store.configDirty, !self.isNixMode { Text("Unsaved changes") @@ -182,78 +181,76 @@ extension ConfigSettings { .buttonStyle(.bordered) } - private func sidebarRow(_ section: ConfigSection) -> some View { - let isSelected = self.activeSectionKey == section.key - return Button { - self.selectSection(section) - } label: { - VStack(alignment: .leading, spacing: 2) { - Text(section.label) - if let help = section.help { - Text(help) - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(2) + private func sidebarSection(_ section: ConfigSection) -> some View { + let isExpanded = self.activeSectionKey == section.key + let subsections = isExpanded ? self.resolveSubsections(for: section) : [] + + return VStack(alignment: .leading, spacing: 2) { + Button { + self.selectSection(section) + } label: { + HStack(spacing: 6) { + Image(systemName: "chevron.right") + .font(.caption2.weight(.semibold)) + .foregroundStyle(.tertiary) + .rotationEffect(.degrees(isExpanded ? 90 : 0)) + Text(section.label) + .lineLimit(1) } + .padding(.vertical, 5) + .padding(.horizontal, 8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(isExpanded && subsections.isEmpty + ? Color.accentColor.opacity(0.18) + : Color.clear) + .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) + .contentShape(Rectangle()) } - .padding(.vertical, 6) - .padding(.horizontal, 8) - .frame(maxWidth: .infinity, alignment: .leading) - .background(isSelected ? Color.accentColor.opacity(0.18) : Color.clear) - .clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous)) - .background(Color.clear) + .buttonStyle(.plain) .contentShape(Rectangle()) + + if isExpanded, !subsections.isEmpty { + VStack(alignment: .leading, spacing: 1) { + self.sidebarSubRow(title: "All", key: nil, sectionKey: section.key) + ForEach(subsections) { sub in + self.sidebarSubRow(title: sub.label, key: sub.key, sectionKey: section.key) + } + } + .padding(.leading, 20) + .transition(.opacity.combined(with: .move(edge: .top))) + } + } + .animation(.easeInOut(duration: 0.18), value: isExpanded) + } + + private func sidebarSubRow(title: String, key: String?, sectionKey: String) -> some View { + let isSelected: Bool = { + guard self.activeSectionKey == sectionKey else { return false } + if let key { return self.activeSubsection == .key(key) } + return self.activeSubsection == .all + }() + + return Button { + if let key { + self.activeSubsection = .key(key) + } else { + self.activeSubsection = .all + } + } label: { + Text(title) + .font(.callout) + .lineLimit(1) + .padding(.vertical, 4) + .padding(.horizontal, 8) + .frame(maxWidth: .infinity, alignment: .leading) + .background(isSelected ? Color.accentColor.opacity(0.18) : Color.clear) + .clipShape(RoundedRectangle(cornerRadius: 7, style: .continuous)) + .contentShape(Rectangle()) } - .frame(maxWidth: .infinity, alignment: .leading) .buttonStyle(.plain) .contentShape(Rectangle()) } - @ViewBuilder - private func subsectionNav(_ section: ConfigSection) -> some View { - let subsections = self.resolveSubsections(for: section) - if subsections.isEmpty { - EmptyView() - } else { - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 8) { - self.subsectionButton( - title: "All", - isSelected: self.activeSubsection == .all) - { - self.activeSubsection = .all - } - ForEach(subsections) { subsection in - self.subsectionButton( - title: subsection.label, - isSelected: self.activeSubsection == .key(subsection.key)) - { - self.activeSubsection = .key(subsection.key) - } - } - } - .padding(.vertical, 2) - } - } - } - - private func subsectionButton( - title: String, - isSelected: Bool, - action: @escaping () -> Void) -> some View - { - Button(action: action) { - Text(title) - .font(.callout.weight(.semibold)) - .foregroundStyle(isSelected ? Color.accentColor : .primary) - .padding(.horizontal, 10) - .padding(.vertical, 6) - .background(isSelected ? Color.accentColor.opacity(0.18) : Color(nsColor: .controlBackgroundColor)) - .clipShape(Capsule()) - } - .buttonStyle(.plain) - } - private func sectionForm(_ section: ConfigSection) -> some View { let subsection = self.activeSubsection let defaultPath: ConfigPath = [.key(section.key)] diff --git a/apps/macos/Sources/OpenClaw/SkillsSettings.swift b/apps/macos/Sources/OpenClaw/SkillsSettings.swift index 02db8495112..d3733d77b54 100644 --- a/apps/macos/Sources/OpenClaw/SkillsSettings.swift +++ b/apps/macos/Sources/OpenClaw/SkillsSettings.swift @@ -95,7 +95,8 @@ struct SkillsSettings: View { skillKey: skill.skillKey, skillName: skill.name, envKey: envKey, - isPrimary: isPrimary) + isPrimary: isPrimary, + homepage: skill.homepage) }) } if !self.model.skills.isEmpty, self.filteredSkills.isEmpty { @@ -258,8 +259,12 @@ private struct SkillRow: View { guard let raw = self.skill.homepage?.trimmingCharacters(in: .whitespacesAndNewlines) else { return nil } - guard !raw.isEmpty else { return nil } - return URL(string: raw) + guard !raw.isEmpty, let url = URL(string: raw), + let scheme = url.scheme?.lowercased(), + scheme == "http" || scheme == "https" else { + return nil + } + return url } private var enabledBinding: Binding { @@ -428,6 +433,7 @@ private struct EnvEditorState: Identifiable { let skillName: String let envKey: String let isPrimary: Bool + let homepage: String? var id: String { "\(self.skillKey)::\(self.envKey)" @@ -447,8 +453,15 @@ private struct EnvEditorView: View { Text(self.subtitle) .font(.subheadline) .foregroundStyle(.secondary) + if let homepageUrl = self.homepageUrl { + Link("Get your key β†’", destination: homepageUrl) + .font(.caption) + } SecureField(self.editor.envKey, text: self.$value) .textFieldStyle(.roundedBorder) + Text("Saved to openclaw.json under skills.entries.\(self.editor.skillKey)") + .font(.caption2) + .foregroundStyle(.tertiary) HStack { Button("Cancel") { self.dismiss() } Spacer() @@ -464,6 +477,18 @@ private struct EnvEditorView: View { .frame(width: 420) } + private var homepageUrl: URL? { + guard let raw = self.editor.homepage?.trimmingCharacters(in: .whitespacesAndNewlines) else { + return nil + } + guard !raw.isEmpty, let url = URL(string: raw), + let scheme = url.scheme?.lowercased(), + scheme == "http" || scheme == "https" else { + return nil + } + return url + } + private var title: String { self.editor.isPrimary ? "Set API Key" : "Set Environment Variable" } @@ -539,12 +564,12 @@ final class SkillsSettingsModel { _ = try await GatewayConnection.shared.skillsUpdate( skillKey: skillKey, apiKey: value) - self.statusMessage = "Saved API key" + self.statusMessage = "Saved API key β€” stored in openclaw.json (skills.entries.\(skillKey))" } else { _ = try await GatewayConnection.shared.skillsUpdate( skillKey: skillKey, env: [envKey: value]) - self.statusMessage = "Saved \(envKey)" + self.statusMessage = "Saved \(envKey) β€” stored in openclaw.json (skills.entries.\(skillKey).env)" } } catch { self.statusMessage = error.localizedDescription @@ -608,7 +633,8 @@ extension SkillsSettings { skillKey: "test", skillName: "Test Skill", envKey: "API_KEY", - isPrimary: true), + isPrimary: true, + homepage: "https://example.com"), onSave: { _ in }) _ = editor.body } diff --git a/package.json b/package.json index d837e265dcb..b56c780ea9a 100644 --- a/package.json +++ b/package.json @@ -834,7 +834,7 @@ "engines": { "node": ">=22.16.0" }, - "packageManager": "pnpm@10.23.0", + "packageManager": "pnpm@10.32.1", "pnpm": { "minimumReleaseAge": 2880, "overrides": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c3df3ada37..7e7df05fd1a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -718,6 +718,9 @@ importers: ui: dependencies: + '@create-markdown/preview': + specifier: ^2.0.0 + version: 2.0.0(@create-markdown/core@2.0.0)(shiki@3.23.0) '@noble/ed25519': specifier: 3.0.1 version: 3.0.1 @@ -1066,6 +1069,25 @@ packages: resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} engines: {node: '>=0.1.90'} + '@create-markdown/core@2.0.0': + resolution: {integrity: sha512-xOmhoiDSa82EzjXp3aViQdB+xfCP4E2jEKxJiKJ702sup3p/CTCtL8fZBKQ3BvzASQRpq/xKCRXZZwRrg1DmZQ==} + engines: {node: '>=20.0.0'} + + '@create-markdown/preview@2.0.0': + resolution: {integrity: sha512-3WTGCrCOVBy9wH2X82Oa2ZHJ+eiEqu8AlucckenWVtFSbzRKzkxgci0BRw7IDvsOsTUEMxS9Ltc9/hSUHkdidA==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@create-markdown/core': '>=2.0.0' + mermaid: '>=10.0.0' + shiki: '>=1.0.0' + peerDependenciesMeta: + '@create-markdown/core': + optional: true + mermaid: + optional: true + shiki: + optional: true + '@csstools/color-helpers@6.0.2': resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} engines: {node: '>=20.19.0'} @@ -5632,14 +5654,18 @@ packages: picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pino-abstract-transport@2.0.0: resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} @@ -7529,6 +7555,14 @@ snapshots: '@colors/colors@1.5.0': optional: true + '@create-markdown/core@2.0.0': + optional: true + + '@create-markdown/preview@2.0.0(@create-markdown/core@2.0.0)(shiki@3.23.0)': + optionalDependencies: + '@create-markdown/core': 2.0.0 + shiki: 3.23.0 + '@csstools/color-helpers@6.0.2': {} '@csstools/css-calc@3.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': @@ -11032,9 +11066,9 @@ snapshots: dependencies: reusify: 1.1.0 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 fetch-blob@3.2.0: dependencies: @@ -11978,7 +12012,7 @@ snapshots: micromatch@4.0.8: dependencies: braces: 3.0.3 - picomatch: 2.3.1 + picomatch: 2.3.2 mime-db@1.52.0: {} @@ -12433,10 +12467,12 @@ snapshots: picocolors@1.1.1: {} - picomatch@2.3.1: {} + picomatch@2.3.2: {} picomatch@4.0.3: {} + picomatch@4.0.4: {} + pino-abstract-transport@2.0.0: dependencies: split2: 4.2.0 @@ -13284,8 +13320,8 @@ snapshots: tinyglobby@0.2.15: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 tinypool@2.1.0: {} @@ -13475,7 +13511,7 @@ snapshots: vite@8.0.1(@types/node@25.5.0)(esbuild@0.27.3)(jiti@2.6.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 - picomatch: 4.0.3 + picomatch: 4.0.4 postcss: 8.5.8 rolldown: 1.0.0-rc.10 tinyglobby: 0.2.15 @@ -13501,7 +13537,7 @@ snapshots: magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 + picomatch: 4.0.4 std-env: 4.0.0 tinybench: 2.9.0 tinyexec: 1.0.2 diff --git a/skills/coding-agent/SKILL.md b/skills/coding-agent/SKILL.md index 50db2c14570..6e4d9cc0f2d 100644 --- a/skills/coding-agent/SKILL.md +++ b/skills/coding-agent/SKILL.md @@ -3,7 +3,28 @@ name: coding-agent description: 'Delegate coding tasks to Codex, Claude Code, or Pi agents via background process. Use when: (1) building/creating new features or apps, (2) reviewing PRs (spawn in temp dir), (3) refactoring large codebases, (4) iterative coding that needs file exploration. NOT for: simple one-liner fixes (just edit), reading code (use read tool), thread-bound ACP harness requests in chat (for example spawn/run Codex or Claude Code in a Discord thread; use sessions_spawn with runtime:"acp"), or any work in ~/clawd workspace (never spawn agents here). Claude Code: use --print --permission-mode bypassPermissions (no PTY). Codex/Pi/OpenCode: pty:true required.' metadata: { - "openclaw": { "emoji": "🧩", "requires": { "anyBins": ["claude", "codex", "opencode", "pi"] } }, + "openclaw": + { + "emoji": "🧩", + "requires": { "anyBins": ["claude", "codex", "opencode", "pi"] }, + "install": + [ + { + "id": "node-claude", + "kind": "node", + "package": "@anthropic-ai/claude-code", + "bins": ["claude"], + "label": "Install Claude Code CLI (npm)", + }, + { + "id": "node-codex", + "kind": "node", + "package": "@openai/codex", + "bins": ["codex"], + "label": "Install Codex CLI (npm)", + }, + ], + }, } --- diff --git a/skills/gh-issues/SKILL.md b/skills/gh-issues/SKILL.md index 002ad93f92a..6a2b2fb28bc 100644 --- a/skills/gh-issues/SKILL.md +++ b/skills/gh-issues/SKILL.md @@ -3,7 +3,23 @@ name: gh-issues description: "Fetch GitHub issues, spawn sub-agents to implement fixes and open PRs, then monitor and address PR review comments. Usage: /gh-issues [owner/repo] [--label bug] [--limit 5] [--milestone v1.0] [--assignee @me] [--fork user/repo] [--watch] [--interval 5] [--reviews-only] [--cron] [--dry-run] [--model glm-5] [--notify-channel -1002381931352]" user-invocable: true metadata: - { "openclaw": { "requires": { "bins": ["curl", "git", "gh"] }, "primaryEnv": "GH_TOKEN" } } + { + "openclaw": + { + "requires": { "bins": ["curl", "git", "gh"] }, + "primaryEnv": "GH_TOKEN", + "install": + [ + { + "id": "brew", + "kind": "brew", + "formula": "gh", + "bins": ["gh"], + "label": "Install GitHub CLI (brew)", + }, + ], + }, + } --- # gh-issues β€” Auto-fix GitHub Issues with Parallel Sub-agents diff --git a/skills/openai-whisper-api/SKILL.md b/skills/openai-whisper-api/SKILL.md index c961f132f4c..99e54340ded 100644 --- a/skills/openai-whisper-api/SKILL.md +++ b/skills/openai-whisper-api/SKILL.md @@ -9,6 +9,16 @@ metadata: "emoji": "🌐", "requires": { "bins": ["curl"], "env": ["OPENAI_API_KEY"] }, "primaryEnv": "OPENAI_API_KEY", + "install": + [ + { + "id": "brew", + "kind": "brew", + "formula": "curl", + "bins": ["curl"], + "label": "Install curl (brew)", + }, + ], }, } --- diff --git a/skills/session-logs/SKILL.md b/skills/session-logs/SKILL.md index 5fd4a5b8cac..b581bd629ac 100644 --- a/skills/session-logs/SKILL.md +++ b/skills/session-logs/SKILL.md @@ -1,7 +1,31 @@ --- name: session-logs description: Search and analyze your own session logs (older/parent conversations) using jq. -metadata: { "openclaw": { "emoji": "πŸ“œ", "requires": { "bins": ["jq", "rg"] } } } +metadata: + { + "openclaw": + { + "emoji": "πŸ“œ", + "requires": { "bins": ["jq", "rg"] }, + "install": + [ + { + "id": "brew-jq", + "kind": "brew", + "formula": "jq", + "bins": ["jq"], + "label": "Install jq (brew)", + }, + { + "id": "brew-rg", + "kind": "brew", + "formula": "ripgrep", + "bins": ["rg"], + "label": "Install ripgrep (brew)", + }, + ], + }, + } --- # session-logs diff --git a/skills/tmux/SKILL.md b/skills/tmux/SKILL.md index fa589c0e2b9..2bc2b57d1cc 100644 --- a/skills/tmux/SKILL.md +++ b/skills/tmux/SKILL.md @@ -2,7 +2,24 @@ name: tmux description: Remote-control tmux sessions for interactive CLIs by sending keystrokes and scraping pane output. metadata: - { "openclaw": { "emoji": "🧡", "os": ["darwin", "linux"], "requires": { "bins": ["tmux"] } } } + { + "openclaw": + { + "emoji": "🧡", + "os": ["darwin", "linux"], + "requires": { "bins": ["tmux"] }, + "install": + [ + { + "id": "brew", + "kind": "brew", + "formula": "tmux", + "bins": ["tmux"], + "label": "Install tmux (brew)", + }, + ], + }, + } --- # tmux Session Control diff --git a/skills/trello/SKILL.md b/skills/trello/SKILL.md index 3428be880f7..eca899605a7 100644 --- a/skills/trello/SKILL.md +++ b/skills/trello/SKILL.md @@ -5,7 +5,20 @@ homepage: https://developer.atlassian.com/cloud/trello/rest/ metadata: { "openclaw": - { "emoji": "πŸ“‹", "requires": { "bins": ["jq"], "env": ["TRELLO_API_KEY", "TRELLO_TOKEN"] } }, + { + "emoji": "πŸ“‹", + "requires": { "bins": ["jq"], "env": ["TRELLO_API_KEY", "TRELLO_TOKEN"] }, + "install": + [ + { + "id": "brew", + "kind": "brew", + "formula": "jq", + "bins": ["jq"], + "label": "Install jq (brew)", + }, + ], + }, } --- diff --git a/skills/weather/SKILL.md b/skills/weather/SKILL.md index 8d463be0b6a..1348e9d910e 100644 --- a/skills/weather/SKILL.md +++ b/skills/weather/SKILL.md @@ -2,7 +2,24 @@ name: weather description: "Get current weather and forecasts via wttr.in or Open-Meteo. Use when: user asks about weather, temperature, or forecasts for any location. NOT for: historical weather data, severe weather alerts, or detailed meteorological analysis. No API key needed." homepage: https://wttr.in/:help -metadata: { "openclaw": { "emoji": "β˜”", "requires": { "bins": ["curl"] } } } +metadata: + { + "openclaw": + { + "emoji": "β˜”", + "requires": { "bins": ["curl"] }, + "install": + [ + { + "id": "brew", + "kind": "brew", + "formula": "curl", + "bins": ["curl"], + "label": "Install curl (brew)", + }, + ], + }, + } --- # Weather Skill diff --git a/src/cli/skills-cli.format.ts b/src/cli/skills-cli.format.ts index 92af375111c..94c870301ef 100644 --- a/src/cli/skills-cli.format.ts +++ b/src/cli/skills-cli.format.ts @@ -36,7 +36,7 @@ function formatSkillStatus(skill: SkillStatusEntry): string { if (skill.blockedByAllowlist) { return theme.warn("🚫 blocked"); } - return theme.error("βœ— missing"); + return theme.warn("β–³ needs setup"); } function normalizeSkillEmoji(emoji?: string): string { @@ -192,7 +192,7 @@ export function formatSkillInfo( ? theme.warn("⏸ Disabled") : skill.blockedByAllowlist ? theme.warn("🚫 Blocked by allowlist") - : theme.error("βœ— Missing requirements"); + : theme.warn("β–³ Needs setup"); lines.push(`${emoji} ${theme.heading(skill.name)} ${status}`); lines.push(""); @@ -265,6 +265,23 @@ export function formatSkillInfo( } } + if (skill.primaryEnv && skill.missing.env.includes(skill.primaryEnv)) { + lines.push(""); + lines.push(theme.heading("API key setup:")); + if (skill.homepage) { + lines.push(` Get your key: ${skill.homepage}`); + } + lines.push( + ` Save via UI: ${theme.muted("Control UI β†’ Skills β†’ ")}${skill.name}${theme.muted(" β†’ Save key")}`, + ); + lines.push( + ` Save via CLI: ${formatCliCommand(`openclaw config set skills.entries.${skill.skillKey}.apiKey YOUR_KEY`)}`, + ); + lines.push( + ` Stored in: ${theme.muted("~/.openclaw/openclaw.json")} ${theme.muted(`(skills.entries.${skill.skillKey}.apiKey)`)}`, + ); + } + return appendClawHubHint(lines.join("\n"), opts.json); } diff --git a/src/cli/skills-cli.test.ts b/src/cli/skills-cli.test.ts index cb48d798a0d..9f344ed662d 100644 --- a/src/cli/skills-cli.test.ts +++ b/src/cli/skills-cli.test.ts @@ -90,7 +90,7 @@ describe("skills-cli", () => { ]); const output = formatSkillsList(report, { verbose: true }); expect(output).toContain("needs-stuff"); - expect(output).toContain("missing"); + expect(output).toContain("needs setup"); expect(output).toContain("anyBins"); expect(output).toContain("os:"); }); diff --git a/ui/package.json b/ui/package.json index 659a5663659..f9205c022ff 100644 --- a/ui/package.json +++ b/ui/package.json @@ -9,6 +9,7 @@ "test": "vitest run --config vitest.config.ts" }, "dependencies": { + "@create-markdown/preview": "^2.0.0", "@noble/ed25519": "3.0.1", "dompurify": "^3.3.3", "lit": "^3.3.2", diff --git a/ui/src/styles.css b/ui/src/styles.css index 2ed83bca3f9..6e27717f8ea 100644 --- a/ui/src/styles.css +++ b/ui/src/styles.css @@ -5,3 +5,13 @@ @import "./styles/chat.css"; @import "./styles/config.css"; @import "./styles/usage.css"; +@import "@create-markdown/preview/themes/system.css"; + +.cm-preview { + --cm-mono: var(--mono); + --cm-link: var(--accent); + --cm-info: var(--info); + --cm-warning: var(--warn); + --cm-success: var(--ok); + --cm-danger: var(--danger); +} diff --git a/ui/src/styles/base.css b/ui/src/styles/base.css index 35fbffdf10b..3e90aca90ab 100644 --- a/ui/src/styles/base.css +++ b/ui/src/styles/base.css @@ -68,7 +68,7 @@ /* Focus */ --focus: rgba(255, 92, 92, 0.2); - --focus-ring: 0 0 0 2px var(--bg), 0 0 0 3px color-mix(in srgb, var(--ring) 60%, transparent); + --focus-ring: 0 0 0 2px var(--bg), 0 0 0 3px color-mix(in srgb, var(--ring) 80%, transparent); --focus-glow: 0 0 0 2px var(--bg), 0 0 0 3px var(--ring), 0 0 16px var(--accent-glow); /* Grid */ @@ -172,7 +172,7 @@ --info: #2563eb; --focus: rgba(220, 38, 38, 0.15); - --focus-ring: 0 0 0 2px var(--bg), 0 0 0 3px color-mix(in srgb, var(--ring) 50%, transparent); + --focus-ring: 0 0 0 2px var(--bg), 0 0 0 3px color-mix(in srgb, var(--ring) 70%, transparent); --focus-glow: 0 0 0 2px var(--bg), 0 0 0 3px var(--ring), 0 0 12px var(--accent-glow); --grid-line: rgba(0, 0, 0, 0.04); @@ -236,7 +236,7 @@ --text-strong: #f5f5f7; --chat-text: #e0e0e2; --muted: #7a7a80; - --muted-strong: #5a5a62; + --muted-strong: #6e6e76; --muted-foreground: #7a7a80; /* Borders β€” whisper-thin, barely visible */ @@ -371,7 +371,7 @@ --text-strong: #f0e4da; --chat-text: #d8c8b8; --muted: #9a8878; - --muted-strong: #7a6858; + --muted-strong: #8a7868; --muted-foreground: #9a8878; --border: #302418; @@ -467,7 +467,7 @@ body { body { margin: 0; - font: 400 13.5px/1.55 var(--font-body); + font: 400 14px/1.55 var(--font-body); letter-spacing: -0.01em; background: var(--bg); color: var(--text); @@ -475,6 +475,12 @@ body { -moz-osx-font-smoothing: grayscale; } +@media (min-width: 1600px) { + body { + font-size: 15px; + } +} + /* Theme transition */ @keyframes theme-circle-transition { 0% { @@ -517,11 +523,12 @@ openclaw-app { a { color: var(--accent); - text-decoration: none; + text-decoration: underline; + text-underline-offset: 2px; } a:hover { - text-decoration: underline; + text-decoration-thickness: 2px; } button, @@ -691,6 +698,17 @@ select { animation-delay: 250ms; } +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } +} + /* Focus visible styles */ :focus-visible { outline: none; diff --git a/ui/src/styles/chat/grouped.css b/ui/src/styles/chat/grouped.css index 3834d21984c..ac795ce2f4c 100644 --- a/ui/src/styles/chat/grouped.css +++ b/ui/src/styles/chat/grouped.css @@ -66,7 +66,9 @@ background: none; border: none; cursor: pointer; - padding: 2px; + padding: 4px; + min-width: 24px; + min-height: 24px; border-radius: var(--radius-sm, 4px); color: var(--muted); opacity: 0; diff --git a/ui/src/styles/chat/layout.css b/ui/src/styles/chat/layout.css index 4cdc71b8bd3..26b833b77f5 100644 --- a/ui/src/styles/chat/layout.css +++ b/ui/src/styles/chat/layout.css @@ -227,8 +227,8 @@ position: absolute; top: 4px; right: 4px; - width: 20px; - height: 20px; + width: 24px; + height: 24px; border-radius: 50%; border: none; background: rgba(0, 0, 0, 0.7); @@ -702,8 +702,8 @@ position: absolute; top: 2px; right: 2px; - width: 18px; - height: 18px; + width: 24px; + height: 24px; border-radius: 50%; border: none; background: rgba(0, 0, 0, 0.6); diff --git a/ui/src/styles/chat/sidebar.css b/ui/src/styles/chat/sidebar.css index fb94e453390..79ea1e47690 100644 --- a/ui/src/styles/chat/sidebar.css +++ b/ui/src/styles/chat/sidebar.css @@ -79,9 +79,207 @@ .sidebar-markdown { font-size: 14px; - line-height: 1.5; + line-height: 1.6; + color: var(--text); } +/* ── Headings ── */ + +.sidebar-markdown :where(h1) { + font-size: 1.65em; + font-weight: 700; + letter-spacing: -0.025em; + margin: 1.6em 0 0.6em; + padding-bottom: 0.35em; + border-bottom: 1px solid var(--border); + line-height: 1.25; +} + +.sidebar-markdown :where(h2) { + font-size: 1.35em; + font-weight: 650; + letter-spacing: -0.02em; + margin: 1.4em 0 0.5em; + padding-bottom: 0.25em; + border-bottom: 1px solid var(--border); + line-height: 1.3; +} + +.sidebar-markdown :where(h3) { + font-size: 1.15em; + font-weight: 600; + letter-spacing: -0.01em; + margin: 1.2em 0 0.4em; + line-height: 1.35; +} + +.sidebar-markdown :where(h4, h5, h6) { + font-size: 1em; + font-weight: 600; + margin: 1em 0 0.35em; + line-height: 1.4; + color: var(--text-strong); +} + +.sidebar-markdown > :where(h1, h2, h3, h4, h5, h6):first-child { + margin-top: 0; +} + +/* ── Paragraphs & spacing ── */ + +.sidebar-markdown :where(p, ul, ol, pre, blockquote, table, details) { + margin: 0; +} + +.sidebar-markdown :where(p + p, p + ul, p + ol, p + pre, p + blockquote, p + table, p + details) { + margin-top: 0.75em; +} + +.sidebar-markdown :where(h1 + p, h2 + p, h3 + p, h4 + p) { + margin-top: 0; +} + +/* ── Lists ── */ + +.sidebar-markdown :where(ul, ol) { + padding-left: 1.6em; +} + +.sidebar-markdown :where(li + li) { + margin-top: 0.3em; +} + +.sidebar-markdown :where(li > p) { + margin-top: 0.25em; +} + +.sidebar-markdown :where(li > ul, li > ol) { + margin-top: 0.25em; +} + +/* ── Links ── */ + +.sidebar-markdown :where(a) { + color: var(--accent); + text-decoration: underline; + text-underline-offset: 2px; + text-decoration-color: color-mix(in srgb, var(--accent) 40%, transparent); + transition: text-decoration-color var(--duration-fast) ease; +} + +.sidebar-markdown :where(a:hover) { + text-decoration-color: var(--accent); +} + +/* ── Inline code ── */ + +.sidebar-markdown code { + font-family: var(--mono); + font-size: 0.88em; +} + +.sidebar-markdown :where(:not(pre) > code) { + padding: 0.15em 0.4em; + border-radius: var(--radius-sm); + background: rgba(0, 0, 0, 0.12); + border: 1px solid var(--border); +} + +:root[data-theme-mode="light"] .sidebar-markdown :where(:not(pre) > code) { + background: rgba(0, 0, 0, 0.06); +} + +/* ── Code blocks ── */ + +.sidebar-markdown pre { + background: rgba(0, 0, 0, 0.12); + border: 1px solid var(--border); + border-radius: var(--radius-md); + padding: 14px 16px; + overflow-x: auto; + margin-top: 0.75em; +} + +:root[data-theme-mode="light"] .sidebar-markdown pre { + background: rgba(0, 0, 0, 0.04); +} + +.sidebar-markdown :where(pre code) { + background: none; + border: none; + padding: 0; + font-size: 12.5px; + line-height: 1.55; +} + +/* ── Blockquotes ── */ + +.sidebar-markdown :where(blockquote) { + border-left: 3px solid var(--border-strong); + padding: 8px 14px; + margin-left: 0; + margin-top: 0.75em; + color: var(--muted); + background: rgba(255, 255, 255, 0.02); + border-radius: 0 var(--radius-sm) var(--radius-sm) 0; +} + +:root[data-theme-mode="light"] .sidebar-markdown :where(blockquote) { + background: rgba(0, 0, 0, 0.025); +} + +.sidebar-markdown :where(blockquote blockquote) { + margin-top: 8px; +} + +/* ── Tables ── */ + +.sidebar-markdown :where(table) { + margin-top: 0.75em; + border-collapse: collapse; + width: 100%; + font-size: 13px; + display: block; + overflow-x: auto; +} + +.sidebar-markdown :where(th, td) { + border: 1px solid var(--border); + padding: 8px 12px; + text-align: left; + vertical-align: top; +} + +.sidebar-markdown :where(th) { + font-weight: 600; + font-size: 12px; + color: var(--muted); + background: var(--secondary); + text-transform: uppercase; + letter-spacing: 0.03em; +} + +.sidebar-markdown :where(tbody tr:hover) { + background: var(--bg-hover); +} + +/* ── Horizontal rules ── */ + +.sidebar-markdown :where(hr) { + border: none; + border-top: 1px solid var(--border); + margin: 1.5em 0; +} + +/* ── Bold / italic ── */ + +.sidebar-markdown :where(strong) { + font-weight: 600; + color: var(--text-strong); +} + +/* ── Images ── */ + .sidebar-markdown .markdown-inline-image { display: block; max-width: 100%; @@ -92,18 +290,34 @@ border-radius: var(--radius-md); background: color-mix(in srgb, var(--secondary) 70%, transparent); object-fit: contain; + margin-top: 0.75em; } -.sidebar-markdown pre { - background: rgba(0, 0, 0, 0.12); - border-radius: var(--radius-sm); - padding: 12px; - overflow-x: auto; +/* ── Details / summary ── */ + +.sidebar-markdown :where(details) { + margin-top: 0.75em; + border: 1px solid var(--border); + border-radius: var(--radius-md); + background: var(--bg-elevated); + overflow: hidden; } -.sidebar-markdown code { - font-family: var(--mono); +.sidebar-markdown :where(summary) { + cursor: pointer; + padding: 8px 12px; + font-weight: 500; font-size: 13px; + user-select: none; + transition: background var(--duration-fast) ease; +} + +.sidebar-markdown :where(summary:hover) { + background: var(--bg-hover); +} + +.sidebar-markdown :where(details[open] > :not(summary)) { + padding: 0 12px 10px; } /* Mobile: Full-screen modal */ diff --git a/ui/src/styles/components.css b/ui/src/styles/components.css index e1b9cfc382b..896334fa93e 100644 --- a/ui/src/styles/components.css +++ b/ui/src/styles/components.css @@ -165,7 +165,9 @@ align-items: center; justify-content: center; margin-left: 8px; - padding: 2px; + padding: 4px; + min-width: 24px; + min-height: 24px; background: none; border: none; cursor: pointer; @@ -521,6 +523,69 @@ animation: none; } +.statusDot.muted { + background: var(--muted); + box-shadow: none; + animation: none; + opacity: 0.5; +} + +/* =========================================== + Skill Toggle Switch + =========================================== */ + +.skill-toggle-wrap { + display: inline-flex; + align-items: center; + cursor: pointer; +} + +.skill-toggle { + appearance: none; + width: 36px; + height: 20px; + border-radius: var(--radius-full); + background: var(--border); + position: relative; + cursor: pointer; + border: none; + outline: none; + transition: + background var(--duration-fast) var(--ease-out), + box-shadow var(--duration-fast) var(--ease-out); + flex-shrink: 0; +} + +.skill-toggle::after { + content: ""; + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + border-radius: var(--radius-full); + background: white; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); + transition: transform var(--duration-fast) var(--ease-out); +} + +.skill-toggle:checked { + background: var(--accent); +} + +.skill-toggle:checked::after { + transform: translateX(16px); +} + +.skill-toggle:focus-visible { + box-shadow: var(--focus-ring); +} + +.skill-toggle:disabled { + opacity: 0.4; + cursor: not-allowed; +} + /* =========================================== Buttons - Tactile with personality =========================================== */ @@ -906,9 +971,6 @@ .field textarea[aria-invalid="true"], .field select[aria-invalid="true"] { border-color: var(--danger); - box-shadow: - inset 0 1px 0 var(--card-highlight), - 0 0 0 1px rgba(239, 68, 68, 0.2); } .cron-form-status { @@ -2928,23 +2990,6 @@ td.data-table-key-col { min-width: 0; } -.agents-toolbar-label { - font-size: 12px; - font-weight: 600; - color: var(--muted); - text-transform: uppercase; - letter-spacing: 0.04em; - flex-shrink: 0; -} - -.agents-control-row { - display: flex; - align-items: center; - gap: 8px; - flex: 1; - min-width: 0; -} - .agents-control-select { flex: 1; min-width: 0; @@ -2979,80 +3024,32 @@ td.data-table-key-col { box-shadow: var(--focus-ring); } -.agents-control-actions { +.agents-toolbar-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; + margin-left: auto; } .agents-refresh-btn { white-space: nowrap; } -.agent-actions-wrap { - position: relative; -} - -.agent-actions-toggle { - width: 28px; - height: 28px; - display: flex; - align-items: center; - justify-content: center; - border: 1px solid var(--border); - border-radius: var(--radius-sm); - background: var(--bg-elevated); - color: var(--muted); - font-size: 14px; - cursor: pointer; - transition: - background var(--duration-fast) ease, - border-color var(--duration-fast) ease; -} - -.agent-actions-toggle:hover { - background: var(--bg-hover); - border-color: var(--border-strong); -} - -.agent-actions-menu { - position: absolute; - top: calc(100% + 4px); - right: 0; - z-index: 10; - min-width: 160px; - padding: 4px; - border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--bg-elevated); - box-shadow: var(--shadow-md); - display: grid; - gap: 1px; -} - -.agent-actions-menu button { - display: block; - width: 100%; - padding: 7px 10px; - border: none; - border-radius: var(--radius-sm); +.btn--ghost { background: transparent; - color: var(--text); - font-size: 12px; - text-align: left; - cursor: pointer; - transition: background var(--duration-fast) ease; -} - -.agent-actions-menu button:hover:not(:disabled) { - background: var(--bg-hover); -} - -.agent-actions-menu button:disabled { + border-color: transparent; color: var(--muted); - cursor: not-allowed; +} + +.btn--ghost:hover:not(:disabled) { + background: var(--bg-hover); + color: var(--text); +} + +.btn--ghost:disabled { opacity: 0.5; + cursor: not-allowed; } .agents-main { @@ -3197,6 +3194,19 @@ td.data-table-key-col { opacity: 0.7; } +.agent-tab--missing { + opacity: 0.5; +} + +.agent-tab-badge { + margin-left: 4px; + font-size: 9px; + font-weight: 700; + text-transform: uppercase; + color: var(--warn); + opacity: 0.8; +} + .agents-overview-grid { display: grid; gap: 12px; @@ -3273,15 +3283,16 @@ td.data-table-key-col { .agent-chip-input:focus-within { border-color: var(--accent); - box-shadow: var(--focus-ring); } -.agent-chip-input input { +.agent-chip-input input, +.agent-chip-input input:focus { flex: 1; min-width: 120px; border: none; background: transparent; outline: none; + box-shadow: none; font-size: 13px; padding: 0; } @@ -3292,58 +3303,6 @@ td.data-table-key-col { min-width: 200px; } -.agent-files-grid { - display: grid; - grid-template-columns: minmax(180px, 240px) minmax(0, 1fr); - gap: 14px; -} - -.agent-files-list { - display: grid; - gap: 8px; -} - -.agent-file-row { - display: flex; - justify-content: space-between; - align-items: center; - gap: 12px; - width: 100%; - text-align: left; - border: 1px solid var(--border); - border-radius: var(--radius-md); - background: var(--card); - padding: 10px 12px; - cursor: pointer; - transition: border-color var(--duration-fast) ease; -} - -.agent-file-row:hover { - border-color: var(--border-strong); -} - -.agent-file-row.active { - border-color: var(--accent); - box-shadow: var(--focus-ring); -} - -.agent-file-name { - font-weight: 600; -} - -.agent-file-meta { - color: var(--muted); - font-size: 12px; - margin-top: 4px; -} - -.agent-files-editor { - border: 1px solid var(--border); - border-radius: var(--radius-lg); - padding: 16px; - background: var(--card); -} - .agent-file-field { min-height: clamp(320px, 56vh, 720px); } @@ -3365,10 +3324,6 @@ td.data-table-key-col { flex-wrap: wrap; } -.agent-file-title { - font-weight: 600; -} - .agent-file-sub { color: var(--muted); font-size: 12px; @@ -3716,10 +3671,6 @@ td.data-table-key-col { justify-items: start; } - .agent-files-grid { - grid-template-columns: 1fr; - } - .agent-tools-list { grid-template-columns: 1fr; } @@ -3736,8 +3687,9 @@ td.data-table-key-col { max-width: none; } - .agents-toolbar-label { - display: none; + .agents-toolbar-actions { + margin-left: 0; + justify-content: flex-end; } } @@ -4354,6 +4306,111 @@ details[open] > .ov-expandable-toggle::after { transform: rotate(-90deg); } +/* =========================================== + Markdown Preview Dialog + =========================================== */ + +.md-preview-dialog { + border: none; + background: transparent; + padding: 0; + max-width: none; + max-height: none; + width: 100vw; + height: 100vh; + overflow: hidden; +} + +.md-preview-dialog::backdrop { + background: rgba(0, 0, 0, 0.65); + backdrop-filter: blur(6px); +} + +.md-preview-dialog__panel { + width: min(780px, calc(100vw - 48px)); + max-height: calc(100vh - 64px); + margin: 32px auto; + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-lg); + display: flex; + flex-direction: column; + overflow: hidden; + animation: scale-in 0.2s var(--ease-out); + transition: + width 0.2s var(--ease-out), + max-height 0.2s var(--ease-out), + margin 0.2s var(--ease-out); +} + +.md-preview-dialog__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 14px 20px; + border-bottom: 1px solid var(--border); + background: var(--card); + flex-shrink: 0; +} + +.md-preview-dialog__title { + font-size: 13px; + font-weight: 600; + color: var(--text); + letter-spacing: -0.01em; +} + +.md-preview-dialog__actions { + display: flex; + align-items: center; + gap: 6px; + flex-shrink: 0; +} + +.md-preview-dialog__body { + flex: 1; + overflow-y: auto; + padding: 16px 20px 24px; +} + +.md-preview-dialog__panel.fullscreen { + width: calc(100vw - 32px); + max-width: none; + max-height: calc(100vh - 32px); + margin: 16px auto; +} + +.md-preview-expand-btn .when-fullscreen { + display: none; +} + +.md-preview-expand-btn.is-fullscreen .when-normal { + display: none; +} + +.md-preview-expand-btn.is-fullscreen .when-fullscreen { + display: inline; +} + +@media (max-width: 640px) { + .md-preview-dialog__panel { + width: calc(100vw - 16px); + max-height: calc(100vh - 32px); + margin: 16px auto; + border-radius: var(--radius-md); + } + + .md-preview-dialog__header { + padding: 12px 16px; + } + + .md-preview-dialog__body { + padding: 14px 12px 20px; + } +} + @media (max-width: 600px) { .ov-cards { grid-template-columns: repeat(2, 1fr); diff --git a/ui/src/styles/config.css b/ui/src/styles/config.css index 51881599f6f..391a387a4f0 100644 --- a/ui/src/styles/config.css +++ b/ui/src/styles/config.css @@ -103,8 +103,8 @@ right: 8px; top: 50%; transform: translateY(-50%); - width: 22px; - height: 22px; + width: 24px; + height: 24px; border: none; border-radius: var(--radius-full); background: var(--bg-hover); @@ -869,6 +869,21 @@ border: 1px solid rgba(239, 68, 68, 0.3); } +.cfg-field--error .cfg-input, +.cfg-field--error .cfg-textarea, +.cfg-field--error .cfg-select, +.cfg-field--error .cfg-number { + border-color: transparent; + box-shadow: none; +} + +.cfg-field--error .cfg-input:focus, +.cfg-field--error .cfg-textarea:focus, +.cfg-field--error .cfg-select:focus { + border-color: var(--border); + box-shadow: none; +} + .cfg-field__label { font-size: 12.5px; font-weight: 600; diff --git a/ui/src/styles/usage.css b/ui/src/styles/usage.css index 95b043c4d96..7879c35a012 100644 --- a/ui/src/styles/usage.css +++ b/ui/src/styles/usage.css @@ -28,16 +28,17 @@ } .usage-page-title { - font-size: 24px; + font-size: 22px; font-weight: 700; - letter-spacing: -0.02em; + letter-spacing: -0.025em; color: var(--text-strong); } .usage-page-subtitle { max-width: 720px; color: var(--muted); - font-size: 14px; + font-size: 13px; + line-height: 1.5; } .usage-section-title { @@ -70,13 +71,13 @@ .usage-header { display: grid; - gap: 14px; + gap: 16px; } .usage-header-row { display: flex; flex-wrap: wrap; - align-items: flex-start; + align-items: center; justify-content: space-between; gap: 12px; } @@ -93,20 +94,21 @@ flex-wrap: wrap; align-items: center; justify-content: flex-end; - gap: 8px; + gap: 6px; } .usage-refresh-indicator, .usage-loading-badge { display: inline-flex; align-items: center; - gap: 8px; - padding: 6px 10px; + gap: 6px; + padding: 5px 10px; border-radius: var(--radius-full); - background: var(--accent-subtle); + background: color-mix(in srgb, var(--accent) 10%, var(--bg-muted)); color: var(--accent); - font-size: 12px; + font-size: 11px; font-weight: 600; + letter-spacing: 0.01em; } .usage-loading-badge { @@ -169,7 +171,7 @@ flex: 1 1 100%; flex-wrap: wrap; align-items: center; - gap: 10px; + gap: 8px; } .usage-presets, @@ -179,7 +181,35 @@ .usage-filter-actions { display: inline-flex; align-items: center; - gap: 8px; + gap: 6px; +} + +.chart-toggle { + padding: 3px; + background: var(--bg-muted); + border: 1px solid var(--border); + border-radius: var(--radius-md); + gap: 2px; +} + +.chart-toggle .toggle-btn { + border: none; + border-radius: calc(var(--radius-md) - 3px); + background: transparent; + min-height: 28px; + padding: 4px 12px; + font-size: 12px; +} + +.chart-toggle .toggle-btn:hover:not(.active) { + background: var(--bg-hover); + color: var(--text); +} + +.chart-toggle .toggle-btn.active { + background: var(--accent-subtle); + border: none; + box-shadow: 0 1px 2px color-mix(in srgb, var(--accent) 12%, transparent); } .usage-date-range { @@ -195,12 +225,13 @@ .usage-query-input, .usage-filters-inline select, .usage-filters-inline input[type="text"] { - min-height: 36px; + min-height: 32px; border: 1px solid var(--border); border-radius: var(--radius-md); - background: var(--bg); + background: var(--bg-elevated); color: var(--text); font: inherit; + font-size: 12px; transition: border-color 0.18s var(--ease-out), box-shadow 0.18s var(--ease-out), @@ -209,7 +240,12 @@ .usage-date-input, .usage-select { - padding: 7px 10px; + padding: 6px 10px; +} + +.usage-date-input:hover, +.usage-select:hover { + border-color: var(--border-strong); } .usage-date-input:focus, @@ -218,8 +254,8 @@ .usage-filters-inline select:focus, .usage-filters-inline input[type="text"]:focus { outline: none; - border-color: color-mix(in srgb, var(--accent) 65%, var(--border)); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent); + border-color: color-mix(in srgb, var(--accent) 50%, var(--border)); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 12%, transparent); } .usage-separator, @@ -235,22 +271,41 @@ .session-log-meta, .session-logs-header-count { color: var(--muted); + font-size: 11px; +} + +.usage-separator { + font-size: 12px; +} + +.usage-query-hint { + font-size: 11px; + letter-spacing: 0.01em; } .usage-metric-badge { display: inline-flex; align-items: center; - gap: 6px; - padding: 7px 11px; - border: 1px solid color-mix(in srgb, var(--accent) 12%, var(--border)); + gap: 5px; + padding: 5px 10px; + border: 1px solid var(--border); border-radius: var(--radius-full); - background: color-mix(in srgb, var(--accent) 8%, var(--card)); - font-size: 12px; - color: var(--text); + background: var(--bg-elevated); + font-size: 11px; + font-weight: 500; + color: var(--muted); + letter-spacing: 0.01em; + transition: border-color 0.18s var(--ease-out); +} + +.usage-metric-badge:hover { + border-color: var(--border-strong); } .usage-metric-badge strong { color: var(--text-strong); + font-weight: 700; + font-variant-numeric: tabular-nums; } /* Toggle-btn active state layers on .btn for segmented controls */ @@ -259,11 +314,25 @@ border-color: var(--accent); background: var(--accent); color: var(--primary-foreground); - box-shadow: 0 8px 24px color-mix(in srgb, var(--accent) 28%, transparent); + font-weight: 600; + box-shadow: 0 1px 3px color-mix(in srgb, var(--accent) 20%, transparent); +} + +.usage-action-btn.usage-primary-btn:hover:not(:disabled) { + background: var(--accent-hover); + border-color: var(--accent-hover); + box-shadow: 0 2px 8px color-mix(in srgb, var(--accent) 25%, transparent); +} + +.chart-toggle .toggle-btn.active { + border-color: color-mix(in srgb, var(--accent) 30%, var(--border)); + background: var(--accent-subtle); + color: var(--accent); + font-weight: 600; } .usage-export-item:disabled { - opacity: 0.55; + opacity: 0.45; cursor: not-allowed; box-shadow: none; } @@ -271,17 +340,25 @@ .usage-query-section { display: grid; gap: 10px; + padding-top: 2px; + border-top: 1px solid color-mix(in srgb, var(--border) 60%, transparent); } .usage-query-bar { display: grid; grid-template-columns: minmax(0, 1fr) auto; - gap: 10px; + gap: 8px; } .usage-query-input { width: 100%; - padding: 10px 12px; + padding: 8px 12px; + font-size: 12px; +} + +.usage-query-input::placeholder { + color: var(--muted); + opacity: 0.7; } .usage-query-actions { @@ -293,7 +370,7 @@ display: flex; flex-wrap: wrap; align-items: center; - gap: 8px; + gap: 6px; } details.usage-filter-select, @@ -314,39 +391,45 @@ details.usage-filter-select summary::-webkit-details-marker, .usage-filter-select summary { display: inline-flex; align-items: center; - gap: 8px; - min-height: 34px; - padding: 8px 12px; + gap: 6px; + min-height: 30px; + padding: 5px 10px; border: 1px solid var(--border); border-radius: var(--radius-md); - background: var(--bg); + background: var(--bg-elevated); color: var(--text); cursor: pointer; font-size: 12px; + font-weight: 500; transition: border-color 0.18s var(--ease-out), box-shadow 0.18s var(--ease-out), background 0.18s var(--ease-out); } -.usage-filter-select[open] summary, .usage-filter-select summary:hover { - border-color: color-mix(in srgb, var(--accent) 25%, var(--border)); - box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 10%, transparent); + border-color: var(--border-strong); + background: var(--bg-hover); +} + +.usage-filter-select[open] summary { + border-color: color-mix(in srgb, var(--accent) 30%, var(--border)); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 8%, transparent); } .usage-filter-badge { display: inline-flex; align-items: center; justify-content: center; - min-width: 20px; - height: 20px; - padding: 0 6px; + min-width: 18px; + height: 18px; + padding: 0 5px; border-radius: var(--radius-full); - background: var(--accent-subtle); - color: var(--accent); - font-size: 11px; + background: var(--bg-muted); + color: var(--muted); + font-size: 10px; font-weight: 700; + letter-spacing: 0.02em; } .usage-filter-popover, @@ -383,26 +466,28 @@ details.usage-filter-select summary::-webkit-details-marker, grid-template-columns: auto minmax(0, 1fr); gap: 8px; align-items: center; - padding: 8px 10px; + padding: 6px 8px; border-radius: var(--radius-md); - background: color-mix(in srgb, var(--bg-muted) 72%, transparent); + background: transparent; font-size: 12px; + transition: background 0.12s ease; } .usage-filter-option:hover { - background: color-mix(in srgb, var(--accent) 10%, var(--bg-muted)); + background: var(--bg-hover); } .usage-export-item { width: 100%; - min-height: 36px; - padding: 9px 10px; + min-height: 32px; + padding: 7px 10px; border: 1px solid var(--border); border-radius: var(--radius-md); - background: var(--bg); + background: var(--bg-elevated); color: var(--text); text-align: left; font: inherit; + font-size: 12px; cursor: pointer; transition: border-color 0.18s var(--ease-out), @@ -411,8 +496,8 @@ details.usage-filter-select summary::-webkit-details-marker, } .usage-export-item:hover:not(:disabled) { - border-color: color-mix(in srgb, var(--accent) 24%, var(--border)); - background: color-mix(in srgb, var(--accent) 10%, var(--bg)); + border-color: var(--border-strong); + background: var(--bg-hover); color: var(--text-strong); } @@ -431,13 +516,14 @@ details.usage-filter-select summary::-webkit-details-marker, .usage-empty-state__feature { display: inline-flex; align-items: center; - gap: 6px; - padding: 8px 10px; + gap: 5px; + padding: 5px 10px; border-radius: var(--radius-full); - border: 1px solid color-mix(in srgb, var(--accent) 16%, var(--border)); - background: color-mix(in srgb, var(--accent) 8%, var(--card)); + border: 1px solid var(--border); + background: var(--bg-elevated); color: var(--text); - font-size: 12px; + font-size: 11px; + font-weight: 500; } .usage-query-chip button { @@ -453,15 +539,15 @@ details.usage-filter-select summary::-webkit-details-marker, .usage-query-suggestion { cursor: pointer; transition: - transform 0.18s var(--ease-out), border-color 0.18s var(--ease-out), - background 0.18s var(--ease-out); + background 0.18s var(--ease-out), + color 0.18s var(--ease-out); } .usage-query-suggestion:hover { - transform: translateY(-1px); - border-color: color-mix(in srgb, var(--accent) 28%, var(--border)); - background: color-mix(in srgb, var(--accent) 12%, var(--card)); + border-color: var(--border-strong); + background: var(--bg-hover); + color: var(--text-strong); } .usage-callout { @@ -499,28 +585,27 @@ details.usage-filter-select summary::-webkit-details-marker, } .usage-empty-block { - padding: 20px 14px; - border: 1px dashed var(--border); - border-radius: var(--radius-lg); - background: color-mix(in srgb, var(--bg-muted) 62%, transparent); + padding: 18px 14px; + border: 1px dashed color-mix(in srgb, var(--border) 70%, transparent); + border-radius: var(--radius-md); text-align: center; color: var(--muted); - font-size: 13px; + font-size: 12px; } .usage-empty-block--compact { - padding: 12px; + padding: 10px; border-style: solid; } .usage-overview-card { display: grid; - gap: 16px; + gap: 14px; } .usage-overview-layout { display: grid; - gap: 16px; + gap: 14px; } .usage-summary-grid, @@ -529,7 +614,7 @@ details.usage-filter-select summary::-webkit-details-marker, .usage-mosaic-grid, .context-breakdown-grid { display: grid; - gap: 12px; + gap: 10px; } .usage-summary-grid { @@ -540,13 +625,13 @@ details.usage-filter-select summary::-webkit-details-marker, .usage-summary-card, .session-summary-card { - min-height: 118px; - gap: 8px; + min-height: 108px; + gap: 6px; } .usage-summary-card.stat, .session-summary-card.stat { - padding: 16px; + padding: 14px 16px; } .usage-summary-card { @@ -572,9 +657,9 @@ details.usage-filter-select summary::-webkit-details-marker, } .usage-summary-card--hero { - min-height: 168px; - gap: 12px; - padding-block: 18px; + min-height: 148px; + gap: 10px; + padding-block: 16px; } .usage-summary-card--throughput { @@ -598,27 +683,28 @@ details.usage-filter-select summary::-webkit-details-marker, .sessions-sort span, .cost-breakdown-header, .usage-mosaic-section-title { - font-size: 12px; - font-weight: 700; + font-size: 11px; + font-weight: 600; color: var(--muted); text-transform: uppercase; - letter-spacing: 0.05em; + letter-spacing: 0.06em; } .usage-summary-value, .session-summary-value { - font-size: clamp(1.55rem, 2vw, 2rem); - line-height: 1.05; + font-size: clamp(1.4rem, 2vw, 1.85rem); + line-height: 1.1; color: var(--text-strong); + font-variant-numeric: tabular-nums; overflow-wrap: anywhere; } .usage-summary-card--hero .usage-summary-value { - font-size: clamp(2rem, 3vw, 2.8rem); + font-size: clamp(1.8rem, 2.5vw, 2.4rem); } .usage-summary-value--compact { - font-size: clamp(1.2rem, 1.75vw, 1.7rem); + font-size: clamp(1.15rem, 1.5vw, 1.5rem); line-height: 1.15; } @@ -639,39 +725,55 @@ details.usage-filter-select summary::-webkit-details-marker, .usage-list-sub, .usage-error-sub { font-size: 12px; + color: var(--muted); + font-variant-numeric: tabular-nums; } .usage-summary-hint { display: inline-flex; align-items: center; justify-content: center; - width: 18px; - height: 18px; - margin-left: 6px; + width: 16px; + height: 16px; + margin-left: 4px; border-radius: var(--radius-full); - border: 1px solid var(--border); - background: color-mix(in srgb, var(--bg-muted) 82%, transparent); + border: 1px solid color-mix(in srgb, var(--border) 70%, transparent); color: var(--muted); - font-size: 10px; + font-size: 9px; cursor: help; + opacity: 0.7; + transition: opacity 0.15s var(--ease-out); +} + +.usage-summary-hint:hover { + opacity: 1; } .usage-insights-grid { - grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); + grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); } .usage-insights-grid--tight { - margin-top: 12px; + margin-top: 10px; } .usage-insight-card { display: grid; - gap: 10px; - min-height: 168px; + gap: 12px; + align-content: start; + min-height: 148px; padding: 16px; border: 1px solid var(--border); border-radius: var(--radius-lg); - background: color-mix(in srgb, var(--card) 82%, var(--bg-muted)); + background: var(--card); + transition: + border-color 0.18s var(--ease-out), + box-shadow 0.18s var(--ease-out); +} + +.usage-insight-card:hover { + border-color: var(--border-strong); + box-shadow: var(--shadow-sm); } .usage-insight-card--wide { @@ -682,7 +784,7 @@ details.usage-filter-select summary::-webkit-details-marker, .usage-error-list, .context-breakdown-list { display: grid; - gap: 10px; + gap: 0; } .usage-list-item, @@ -690,8 +792,24 @@ details.usage-filter-select summary::-webkit-details-marker, .context-breakdown-item { display: flex; justify-content: space-between; - gap: 10px; + gap: 12px; align-items: baseline; + padding: 8px 0; + border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent); + font-size: 13px; +} + +.usage-list-item:last-child, +.usage-error-row:last-child, +.context-breakdown-item:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.usage-list-item:first-child, +.usage-error-row:first-child, +.context-breakdown-item:first-child { + padding-top: 0; } .usage-list-value { @@ -700,17 +818,20 @@ details.usage-filter-select summary::-webkit-details-marker, justify-content: flex-end; gap: 6px; color: var(--text-strong); + font-weight: 600; + font-variant-numeric: tabular-nums; } .usage-error-rate { - font-size: 16px; + font-size: 15px; font-weight: 700; color: var(--danger); + font-variant-numeric: tabular-nums; } .usage-mosaic { display: grid; - gap: 14px; + gap: 12px; } .usage-mosaic-header, @@ -725,7 +846,7 @@ details.usage-filter-select summary::-webkit-details-marker, flex-wrap: wrap; align-items: center; justify-content: space-between; - gap: 10px; + gap: 8px; } .usage-mosaic-title, @@ -736,13 +857,14 @@ details.usage-filter-select summary::-webkit-details-marker, .usage-mosaic-title, .session-detail-title { - font-size: 16px; + font-size: 15px; font-weight: 700; } .usage-mosaic-total { - font-size: 14px; - font-weight: 700; + font-size: 13px; + font-weight: 600; + font-variant-numeric: tabular-nums; } .usage-mosaic-grid { @@ -751,14 +873,14 @@ details.usage-filter-select summary::-webkit-details-marker, .usage-mosaic-section { display: grid; - gap: 10px; + gap: 8px; } .usage-mosaic-section-title { display: flex; align-items: center; justify-content: space-between; - gap: 10px; + gap: 8px; } .usage-daypart-grid { @@ -1586,10 +1708,13 @@ details.usage-filter-select summary::-webkit-details-marker, } .usage-filter-popover, + .usage-export-popover { + width: min(320px, calc(100vw - 40px)); + } + .usage-export-popover { right: auto; left: 0; - width: min(320px, calc(100vw - 40px)); } .usage-daypart-grid { diff --git a/ui/src/ui/app-render.ts b/ui/src/ui/app-render.ts index ac211d0fabf..2baba7bce39 100644 --- a/ui/src/ui/app-render.ts +++ b/ui/src/ui/app-render.ts @@ -966,6 +966,7 @@ export function renderApp(state: AppViewState) { error: state.toolsCatalogError, result: state.toolsCatalogResult, }, + modelCatalog: state.chatModelCatalog ?? [], onRefresh: async () => { await loadAgents(state); const agentIds = state.agentsList?.agents?.map((entry) => entry.id) ?? []; @@ -1278,16 +1279,21 @@ export function renderApp(state: AppViewState) { report: state.skillsReport, error: state.skillsError, filter: state.skillsFilter, + statusFilter: state.skillsStatusFilter, edits: state.skillEdits, messages: state.skillMessages, busyKey: state.skillsBusyKey, + detailKey: state.skillsDetailKey, onFilterChange: (next) => (state.skillsFilter = next), + onStatusFilterChange: (next) => (state.skillsStatusFilter = next), onRefresh: () => loadSkills(state, { clearMessages: true }), onToggle: (key, enabled) => updateSkillEnabled(state, key, enabled), onEdit: (key, value) => updateSkillEdit(state, key, value), onSaveKey: (key) => saveSkillApiKey(state, key), onInstall: (skillKey, name, installId) => installSkill(state, skillKey, name, installId), + onDetailOpen: (key) => (state.skillsDetailKey = key), + onDetailClose: () => (state.skillsDetailKey = null), }), ) : nothing diff --git a/ui/src/ui/app-view-state.ts b/ui/src/ui/app-view-state.ts index 4eedc881fba..04465575a6f 100644 --- a/ui/src/ui/app-view-state.ts +++ b/ui/src/ui/app-view-state.ts @@ -267,9 +267,11 @@ export type AppViewState = { skillsReport: SkillStatusReport | null; skillsError: string | null; skillsFilter: string; + skillsStatusFilter: "all" | "ready" | "needs-setup" | "disabled"; skillEdits: Record; skillMessages: Record; skillsBusyKey: string | null; + skillsDetailKey: string | null; healthLoading: boolean; healthResult: HealthSummary | null; healthError: string | null; diff --git a/ui/src/ui/app.ts b/ui/src/ui/app.ts index 8092dfc4e00..fe67292eab3 100644 --- a/ui/src/ui/app.ts +++ b/ui/src/ui/app.ts @@ -259,8 +259,7 @@ export class OpenClawApp extends LitElement { @state() toolsCatalogLoading = false; @state() toolsCatalogError: string | null = null; @state() toolsCatalogResult: ToolsCatalogResult | null = null; - @state() agentsPanel: "overview" | "files" | "tools" | "skills" | "channels" | "cron" = - "overview"; + @state() agentsPanel: "overview" | "files" | "tools" | "skills" | "channels" | "cron" = "files"; @state() agentFilesLoading = false; @state() agentFilesError: string | null = null; @state() agentFilesList: AgentsFilesListResult | null = null; @@ -398,9 +397,11 @@ export class OpenClawApp extends LitElement { @state() skillsReport: SkillStatusReport | null = null; @state() skillsError: string | null = null; @state() skillsFilter = ""; + @state() skillsStatusFilter: "all" | "ready" | "needs-setup" | "disabled" = "all"; @state() skillEdits: Record = {}; @state() skillsBusyKey: string | null = null; @state() skillMessages: Record = {}; + @state() skillsDetailKey: string | null = null; @state() healthLoading = false; @state() healthResult: HealthSummary | null = null; diff --git a/ui/src/ui/chat/slash-command-executor.node.test.ts b/ui/src/ui/chat/slash-command-executor.node.test.ts index c574ff79152..06c33c8a57b 100644 --- a/ui/src/ui/chat/slash-command-executor.node.test.ts +++ b/ui/src/ui/chat/slash-command-executor.node.test.ts @@ -279,6 +279,9 @@ describe("executeSlashCommand directives", () => { if (method === "models.list") { return { models: createModelCatalog(OPENAI_GPT5_MINI_MODEL) }; } + if (method === "models.list") { + return { models: [{ id: "gpt-5-mini", name: "gpt-5-mini", provider: "openai" }] }; + } throw new Error(`unexpected method: ${method}`); }); diff --git a/ui/src/ui/controllers/skills.ts b/ui/src/ui/controllers/skills.ts index f243d168742..37b7d2c63a1 100644 --- a/ui/src/ui/controllers/skills.ts +++ b/ui/src/ui/controllers/skills.ts @@ -108,7 +108,7 @@ export async function saveSkillApiKey(state: SkillsState, skillKey: string) { await loadSkills(state); setSkillMessage(state, skillKey, { kind: "success", - message: "API key saved", + message: `API key saved β€” stored in openclaw.json (skills.entries.${skillKey})`, }); } catch (err) { const message = getErrorMessage(err); diff --git a/ui/src/ui/icons.ts b/ui/src/ui/icons.ts index 39815b8aa0b..9cba6e36ef1 100644 --- a/ui/src/ui/icons.ts +++ b/ui/src/ui/icons.ts @@ -448,6 +448,22 @@ export const icons = { `, + maximize: html` + + + + + + + `, + minimize: html` + + + + + + + `, } as const; export type IconName = keyof typeof icons; diff --git a/ui/src/ui/types.ts b/ui/src/ui/types.ts index 8f54d7e776a..cdbc54a9cdd 100644 --- a/ui/src/ui/types.ts +++ b/ui/src/ui/types.ts @@ -645,7 +645,7 @@ export type ModelCatalogEntry = { provider: string; contextWindow?: number; reasoning?: boolean; - input?: Array<"text" | "image">; + input?: Array<"text" | "image" | "document">; }; export type ToolCatalogProfile = diff --git a/ui/src/ui/views/agents-panels-overview.ts b/ui/src/ui/views/agents-panels-overview.ts index 8fa1c4a6b31..b23705b9e08 100644 --- a/ui/src/ui/views/agents-panels-overview.ts +++ b/ui/src/ui/views/agents-panels-overview.ts @@ -1,5 +1,10 @@ import { html, nothing } from "lit"; -import type { AgentIdentityResult, AgentsFilesListResult, AgentsListResult } from "../types.ts"; +import type { + AgentIdentityResult, + AgentsFilesListResult, + AgentsListResult, + ModelCatalogEntry, +} from "../types.ts"; import { buildModelOptions, normalizeModelValue, @@ -23,6 +28,7 @@ export function renderAgentOverview(params: { configLoading: boolean; configSaving: boolean; configDirty: boolean; + modelCatalog: ModelCatalogEntry[]; onConfigReload: () => void; onConfigSave: () => void; onModelChange: (agentId: string, modelId: string | null) => void; @@ -128,14 +134,16 @@ export function renderAgentOverview(params: { > ${ isDefault - ? nothing + ? html` + + ` : html` ` } - ${buildModelOptions(configForm, effectivePrimary ?? undefined)} + ${buildModelOptions(configForm, effectivePrimary ?? undefined, params.modelCatalog)}
diff --git a/ui/src/ui/views/agents-panels-status-files.ts b/ui/src/ui/views/agents-panels-status-files.ts index f1be9715df0..b0918c7783a 100644 --- a/ui/src/ui/views/agents-panels-status-files.ts +++ b/ui/src/ui/views/agents-panels-status-files.ts @@ -1,8 +1,10 @@ +import { applyPreviewTheme } from "@create-markdown/preview"; +import DOMPurify from "dompurify"; import { html, nothing } from "lit"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; +import { marked } from "marked"; import { formatRelativeTimestamp } from "../format.ts"; import { icons } from "../icons.ts"; -import { toSanitizedMarkdownHtml } from "../markdown.ts"; import { formatCronPayload, formatCronSchedule, @@ -10,14 +12,13 @@ import { formatNextRun, } from "../presenter.ts"; import type { - AgentFileEntry, AgentsFilesListResult, ChannelAccountSnapshot, ChannelsStatusSnapshot, CronJob, CronStatus, } from "../types.ts"; -import { formatBytes, type AgentContext } from "./agents-utils.ts"; +import { type AgentContext } from "./agents-utils.ts"; import type { AgentsPanel } from "./agents.ts"; import { resolveChannelExtras as resolveChannelExtrasFromConfig } from "./channel-config-extras.ts"; @@ -387,7 +388,10 @@ export function renderAgentFiles(params: {
${ list - ? html`
Workspace: ${list.workspace}
` + ? html`` : nothing } ${ @@ -402,96 +406,132 @@ export function renderAgentFiles(params: { Load the agent workspace files to edit core instructions. ` - : html` -
-
- ${ - files.length === 0 - ? html` -
No files found.
- ` - : files.map((file) => - renderAgentFileRow(file, active, () => params.onSelectFile(file.name)), - ) - } + : files.length === 0 + ? html` +
No files found.
+ ` + : html` +
+ ${files.map((file) => { + const isActive = active === file.name; + const label = file.name.replace(/\.md$/i, ""); + return html` + + `; + })}
-
- ${ - !activeEntry - ? html` -
Select a file to edit.
- ` - : html` -
-
-
${activeEntry.name}
-
${activeEntry.path}
-
-
- - - -
+ ${ + !activeEntry + ? html` +
Select a file to edit.
+ ` + : html` +
+
+
${activeEntry.path}
- ${ - activeEntry.missing - ? html` -
- This file is missing. Saving will create it in the agent workspace. -
- ` - : nothing - } - - { - const dialog = e.currentTarget as HTMLDialogElement; - if (e.target === dialog) { - dialog.close(); - } - }} - > -
-
-
${activeEntry.name}
+
+ + + +
+
+ ${ + activeEntry.missing + ? html` +
+ This file is missing. Saving will create it in the agent workspace. +
+ ` + : nothing + } + + { + const dialog = e.currentTarget as HTMLDialogElement; + if (e.target === dialog) { + dialog.close(); + } + }} + @close=${(e: Event) => { + const dialog = e.currentTarget as HTMLElement; + dialog + .querySelector(".md-preview-dialog__panel") + ?.classList.remove("fullscreen"); + }} + > +
+
+
${activeEntry.name}
+
+ +
-
-
- ` - } -
-
- ` +
+ ${unsafeHTML(applyPreviewTheme(marked.parse(draft, { gfm: true, breaks: true }) as string, { sanitize: (h: string) => DOMPurify.sanitize(h) }))} +
+
+ + ` + } + ` } `; } - -function renderAgentFileRow(file: AgentFileEntry, active: string | null, onSelect: () => void) { - const status = file.missing - ? "Missing" - : `${formatBytes(file.size)} Β· ${formatRelativeTimestamp(file.updatedAtMs ?? null)}`; - return html` - - `; -} diff --git a/ui/src/ui/views/agents-utils.ts b/ui/src/ui/views/agents-utils.ts index e0c06c41386..5d329ac34df 100644 --- a/ui/src/ui/views/agents-utils.ts +++ b/ui/src/ui/views/agents-utils.ts @@ -1,4 +1,4 @@ -import { html } from "lit"; +import { html, nothing } from "lit"; import { expandToolGroups, normalizeToolName, @@ -8,6 +8,7 @@ import type { AgentIdentityResult, AgentsFilesListResult, AgentsListResult, + ModelCatalogEntry, ToolCatalogProfile, ToolsCatalogResult, } from "../types.ts"; @@ -574,16 +575,38 @@ function resolveConfiguredModels( export function buildModelOptions( configForm: Record | null, current?: string | null, + catalog?: ModelCatalogEntry[], ) { - const options = resolveConfiguredModels(configForm); - const hasCurrent = current ? options.some((option) => option.value === current) : false; - if (current && !hasCurrent) { + const seen = new Set(); + const options: ConfiguredModelOption[] = []; + const addOption = (value: string, label: string) => { + const key = value.toLowerCase(); + if (seen.has(key)) { + return; + } + seen.add(key); + options.push({ value, label }); + }; + + for (const opt of resolveConfiguredModels(configForm)) { + addOption(opt.value, opt.label); + } + + if (catalog) { + for (const entry of catalog) { + const provider = entry.provider?.trim(); + const value = provider ? `${provider}/${entry.id}` : entry.id; + const label = provider ? `${entry.id} Β· ${provider}` : entry.id; + addOption(value, label); + } + } + + if (current && !seen.has(current.toLowerCase())) { options.unshift({ value: current, label: `Current (${current})` }); } + if (options.length === 0) { - return html` - - `; + return nothing; } return options.map((option) => html``); } diff --git a/ui/src/ui/views/agents.test.ts b/ui/src/ui/views/agents.test.ts index f763877937a..64388ee9b9d 100644 --- a/ui/src/ui/views/agents.test.ts +++ b/ui/src/ui/views/agents.test.ts @@ -86,6 +86,7 @@ function createProps(overrides: Partial = {}): AgentsProps { error: null, result: null, }, + modelCatalog: [], onRefresh: () => undefined, onSelectAgent: () => undefined, onSelectPanel: () => undefined, diff --git a/ui/src/ui/views/agents.ts b/ui/src/ui/views/agents.ts index eeb33b8b82c..b6a8bb4098d 100644 --- a/ui/src/ui/views/agents.ts +++ b/ui/src/ui/views/agents.ts @@ -6,6 +6,7 @@ import type { ChannelsStatusSnapshot, CronJob, CronStatus, + ModelCatalogEntry, SkillStatusReport, ToolsCatalogResult, } from "../types.ts"; @@ -81,6 +82,7 @@ export type AgentsProps = { agentIdentityById: Record; agentSkills: AgentSkillsState; toolsCatalog: ToolsCatalogState; + modelCatalog: ModelCatalogEntry[]; onRefresh: () => void; onSelectAgent: (agentId: string) => void; onSelectPanel: (panel: AgentsPanel) => void; @@ -135,72 +137,51 @@ export function renderAgents(props: AgentsProps) {
- Agent -
-
- props.onSelectAgent((e.target as HTMLSelectElement).value)} + > + ${ + agents.length === 0 + ? html` + + ` + : agents.map( + (agent) => html` `, - ) - } - -
-
- ${ - selectedAgent - ? html` -
- - ${ - actionsMenuOpen - ? html` -
- - -
- ` - : nothing - } -
- ` - : nothing + ) } - -
+ +
+
+ ${ + selectedAgent + ? html` + + + ` + : nothing + } +
${ @@ -234,6 +215,7 @@ export function renderAgents(props: AgentsProps) { configLoading: props.config.loading, configSaving: props.config.saving, configDirty: props.config.dirty, + modelCatalog: props.modelCatalog, onConfigReload: props.onConfigReload, onConfigSave: props.onConfigSave, onModelChange: props.onModelChange, @@ -350,8 +332,6 @@ export function renderAgents(props: AgentsProps) { `; } -let actionsMenuOpen = false; - function renderAgentTabs( active: AgentsPanel, onSelect: (panel: AgentsPanel) => void, diff --git a/ui/src/ui/views/chat.ts b/ui/src/ui/views/chat.ts index c8da1dba916..4fb80b99c34 100644 --- a/ui/src/ui/views/chat.ts +++ b/ui/src/ui/views/chat.ts @@ -656,13 +656,14 @@ function renderSearchBar(requestUpdate: () => void): TemplateResult | typeof not { vs.searchQuery = (e.target as HTMLInputElement).value; requestUpdate(); }} /> - ` } - ${ canAbort && (isBusy || props.sending) ? html` - ` @@ -1354,6 +1360,7 @@ export function renderChat(props: ChatProps) { }} ?disabled=${!props.connected || props.sending} title=${isBusy ? "Queue" : "Send"} + aria-label=${isBusy ? "Queue message" : "Send message"} > ${icons.send} diff --git a/ui/src/ui/views/config.ts b/ui/src/ui/views/config.ts index 5351c88581a..cb6cc508ef5 100644 --- a/ui/src/ui/views/config.ts +++ b/ui/src/ui/views/config.ts @@ -887,6 +887,7 @@ export function renderConfig(props: ConfigProps) { type="text" class="config-search__input" placeholder="Search settings..." + aria-label="Search settings" .value=${props.searchQuery} @input=${(e: Event) => props.onSearchChange((e.target as HTMLInputElement).value)} @@ -896,6 +897,7 @@ export function renderConfig(props: ConfigProps) { ? html`
-
+
+ ${STATUS_TABS.map( + (tab) => html` + + `, + )} +
+ +
${groups.map((group) => { - const collapsedByDefault = group.id === "workspace" || group.id === "built-in"; return html` -
+
${group.label} ${group.skills.length} @@ -105,10 +179,50 @@ export function renderSkills(props: SkillsProps) { ` } + + ${detailSkill ? renderSkillDetail(detailSkill, props) : nothing} `; } function renderSkill(skill: SkillStatusEntry, props: SkillsProps) { + const busy = props.busyKey === skill.skillKey; + const dotClass = skillStatusClass(skill); + + return html` +
props.onDetailOpen(skill.skillKey)} + > +
+
+ + ${skill.emoji ? html`${skill.emoji}` : nothing} + ${skill.name} +
+
${clampText(skill.description, 140)}
+
+
+ +
+
+ `; +} + +function renderSkillDetail(skill: SkillStatusEntry, props: SkillsProps) { const busy = props.busyKey === skill.skillKey; const apiKey = props.edits[skill.skillKey] ?? ""; const message = props.messages[skill.skillKey] ?? null; @@ -116,92 +230,124 @@ function renderSkill(skill: SkillStatusEntry, props: SkillsProps) { const showBundledBadge = Boolean(skill.bundled && skill.source !== "openclaw-bundled"); const missing = computeSkillMissing(skill); const reasons = computeSkillReasons(skill); + return html` -
-
-
- ${skill.emoji ? `${skill.emoji} ` : ""}${skill.name} + { + if ((e.target as HTMLElement).classList.contains("md-preview-dialog")) { + props.onDetailClose(); + } + }}> +
+
+
+ + ${skill.emoji ? html`${skill.emoji}` : nothing} + ${skill.name} +
+
-
${clampText(skill.description, 140)}
- ${renderSkillStatusChips({ skill, showBundledBadge })} - ${ - missing.length > 0 - ? html` -
- Missing: ${missing.join(", ")} -
- ` - : nothing - } - ${ - reasons.length > 0 - ? html` -
- Reason: ${reasons.join(", ")} -
- ` - : nothing - } -
-
-
- +
+
+
${skill.description}
+ ${renderSkillStatusChips({ skill, showBundledBadge })} +
+ ${ - canInstall - ? html`` + missing.length > 0 + ? html` +
+
Missing requirements
+
${missing.join(", ")}
+
+ ` : nothing } -
- ${ - message - ? html`
- ${message.message} -
` - : nothing - } - ${ - skill.primaryEnv - ? html` -
- API key - - props.onEdit(skill.skillKey, (e.target as HTMLInputElement).value)} - /> -
- ` + : nothing + } +
+ + ${ + message + ? html`
- Save key - - ` - : nothing - } + ${message.message} +
` + : nothing + } + + ${ + skill.primaryEnv + ? html` +
+ ` + : nothing + } + + +
-
+ `; } diff --git a/ui/src/ui/views/usage-render-overview.ts b/ui/src/ui/views/usage-render-overview.ts index 879951fa1d5..99ea2d19db7 100644 --- a/ui/src/ui/views/usage-render-overview.ts +++ b/ui/src/ui/views/usage-render-overview.ts @@ -178,6 +178,22 @@ function renderDailyChartCompact( const values = daily.map((d) => (isTokenMode ? d.totalTokens : d.totalCost)); const maxValue = Math.max(...values, isTokenMode ? 1 : 0.0001); + // Adaptive scaling: when the spread between largest and smallest non-zero + // values is extreme (>50Γ—), use square-root compression so small bars stay + // visible instead of collapsing to a single pixel. + const nonZero = values.filter((v) => v > 0); + const minNonZero = nonZero.length > 0 ? Math.min(...nonZero) : maxValue; + const spread = maxValue / minNonZero; + const chartAreaPx = 200; + const minBarPx = 6; + const barHeights = values.map((v): number => { + if (v <= 0) { + return 0; + } + const ratio = spread > 50 ? Math.sqrt(v / maxValue) : v / maxValue; + return Math.max(minBarPx, ratio * chartAreaPx); + }); + // Calculate bar width based on number of days const barMaxWidth = daily.length > 30 ? 12 : daily.length > 20 ? 18 : daily.length > 14 ? 24 : 32; const showTotals = daily.length <= 14; @@ -206,8 +222,7 @@ function renderDailyChartCompact(
${daily.map((d, idx) => { - const value = values[idx]; - const heightPct = (value / maxValue) * 100; + const heightPx = barHeights[idx]; const isSelected = selectedDays.includes(d.date); const label = formatDayLabel(d.date); // Shorter label for many days (just day number) @@ -257,7 +272,7 @@ function renderDailyChartCompact( ? html`
${(() => { const total = segments.reduce((sum, seg) => sum + seg.value, 0) || 1; @@ -273,7 +288,7 @@ function renderDailyChartCompact(
` : html` -
+
` } ${showTotals ? html`
${totalLabel}
` : nothing} diff --git a/ui/src/ui/views/usage.ts b/ui/src/ui/views/usage.ts index 1395b358927..957b6a52b8c 100644 --- a/ui/src/ui/views/usage.ts +++ b/ui/src/ui/views/usage.ts @@ -584,6 +584,7 @@ export function renderUsage(props: UsageProps) { type="date" .value=${filters.startDate} title=${t("usage.filters.startDate")} + aria-label=${t("usage.filters.startDate")} @change=${(e: Event) => filterActions.onStartDateChange((e.target as HTMLInputElement).value)} /> @@ -593,6 +594,7 @@ export function renderUsage(props: UsageProps) { type="date" .value=${filters.endDate} title=${t("usage.filters.endDate")} + aria-label=${t("usage.filters.endDate")} @change=${(e: Event) => filterActions.onEndDateChange((e.target as HTMLInputElement).value)} /> @@ -600,6 +602,7 @@ export function renderUsage(props: UsageProps) {