From ca412f43c6a90dbee2def6f9c48d224a23b1eed7 Mon Sep 17 00:00:00 2001 From: bashybaranaba Date: Mon, 24 Aug 2026 00:10:47 +0300 Subject: [PATCH] Clean up skills UI and unify scheduled tasks iconography Platform skills store `icon` as a Lucide icon name (`panels-top-left`, `file-text`, `monitor`), but three surfaces rendered that string as raw text inside a 36px chip, so slugs spilled across the card, the agent's skills list, and the skill detail header. Add a SkillIcon that resolves the stored value: a Lucide name renders the real glyph, an emoji renders as a glyph, and anything unrecognised falls back to the Zap mark, so a slug can never reach the layout again. Tidy the skills grid card while there: the icon, title, and description share one aligned row; the boxed ghost "Available to..." button becomes a quiet inline row; the hover lift gives way to a flat border/tint hover matching the tools view; tags truncate; the empty state adopts the house dashed-border pattern; cards are keyboard-reachable with a focus ring. Skills now read as Zap rather than Sparkles across the agent section nav, the Customize tab, both empty states, and the runtime skills note. Sparkles stays on credits, the copilot badge, and the composer's Auto chip, which are not skills. Tasks become "Scheduled tasks" with a single ClipboardClock icon across the sidebar, collapsed rail, agent section nav, global search, landing sidebar, page header, create tooltip, detail back link, and copilot scope label. That icon postdates the pinned lucide-react 0.474, so it is inlined from Lucide's own path data with LucideProps and drops into every `icon:` slot unchanged. Co-Authored-By: Claude Opus 5 --- apps/commons-app/app/studio/[tab]/page.tsx | 4 +- .../app/studio/agents/[agent]/page.tsx | 14 +- .../app/studio/skills/[skillId]/page.tsx | 11 +- .../app/studio/tasks/[taskId]/page.tsx | 2 +- .../agents/runtime-native-tooling.tsx | 4 +- .../copilot/floating-commons-copilot.tsx | 2 +- .../components/customize/customize-tabs.tsx | 4 +- .../components/icons/clipboard-clock.tsx | 40 ++++++ .../components/landing/landing-sidebar.tsx | 4 +- .../components/layout/dashboard-bar.tsx | 14 +- .../components/layout/dashboard-side-bar.tsx | 6 +- .../components/search/global-search.tsx | 8 +- .../components/skills/skill-icon.tsx | 135 ++++++++++++++++++ .../skills/skills-marketplace-view.tsx | 107 +++++++------- 14 files changed, 261 insertions(+), 94 deletions(-) create mode 100644 apps/commons-app/components/icons/clipboard-clock.tsx create mode 100644 apps/commons-app/components/skills/skill-icon.tsx diff --git a/apps/commons-app/app/studio/[tab]/page.tsx b/apps/commons-app/app/studio/[tab]/page.tsx index 4ae1f97c..237cd518 100644 --- a/apps/commons-app/app/studio/[tab]/page.tsx +++ b/apps/commons-app/app/studio/[tab]/page.tsx @@ -228,7 +228,7 @@ const StudioPage: NextPage = () => { case "tools": return "Create new tool"; case "tasks": - return "Create new task"; + return "Create new scheduled task"; case "workflows": return "Create new workflow"; case "skills": @@ -249,7 +249,7 @@ const StudioPage: NextPage = () => { }; case "tasks": return { - title: "Tasks", + title: "Scheduled tasks", description: "Schedule one-off or recurring tasks for your agents to run automatically.", }; diff --git a/apps/commons-app/app/studio/agents/[agent]/page.tsx b/apps/commons-app/app/studio/agents/[agent]/page.tsx index ddcba42b..254a9e5b 100644 --- a/apps/commons-app/app/studio/agents/[agent]/page.tsx +++ b/apps/commons-app/app/studio/agents/[agent]/page.tsx @@ -9,7 +9,6 @@ import { BarChart3, Bot, Brain, - CalendarCheck, Check, CheckCircle2, ChevronRight, @@ -27,13 +26,14 @@ import { Search, Server, Settings2, - Sparkles, TerminalSquare, Wallet, Wrench, XCircle, Zap, } from "lucide-react"; +import { ClipboardClock } from "@/components/icons/clipboard-clock"; +import { SkillIcon } from "@/components/skills/skill-icon"; import { formatDistanceToNow } from "date-fns"; import { Area, @@ -120,9 +120,9 @@ const sections: Array<{ key: SectionKey; label: string; icon: typeof Bot }> = [ { key: "new-session", label: "New session", icon: Plus }, { key: "sessions", label: "Sessions", icon: MessageSquare }, { key: "computer", label: "Computer", icon: Monitor }, - { key: "tasks", label: "Tasks", icon: CalendarCheck }, + { key: "tasks", label: "Scheduled tasks", icon: ClipboardClock }, { key: "tools", label: "Tools", icon: Wrench }, - { key: "skills", label: "Skills", icon: Sparkles }, + { key: "skills", label: "Skills", icon: Zap }, { key: "artifacts", label: "Artifacts", icon: FileText }, { key: "observability", label: "Observability", icon: TerminalSquare }, { key: "usage", label: "Usage", icon: BarChart3 }, @@ -1473,7 +1473,7 @@ function SkillsView({ ) : visibleSkills.length === 0 ? (
- +

{view === "assigned" ? "No skills enabled" : "No skills found"}

@@ -1500,9 +1500,7 @@ function SkillsView({ } className="flex min-w-0 flex-1 items-start gap-3 text-left" > - - {skill.icon || } - + diff --git a/apps/commons-app/app/studio/skills/[skillId]/page.tsx b/apps/commons-app/app/studio/skills/[skillId]/page.tsx index 4593a775..5e07d34f 100644 --- a/apps/commons-app/app/studio/skills/[skillId]/page.tsx +++ b/apps/commons-app/app/studio/skills/[skillId]/page.tsx @@ -2,10 +2,11 @@ import { use, useEffect, useState } from "react"; import { useRouter } from "next/navigation"; -import { ArrowLeft, Globe, Lock, Sparkles } from "lucide-react"; +import { ArrowLeft, Globe, Lock } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; +import { SkillIcon } from "@/components/skills/skill-icon"; import type { Skill } from "@agent-commons/sdk"; export default function SkillDetailPage({ @@ -74,13 +75,7 @@ export default function SkillDetailPage({
- - {skill.icon ? ( - {skill.icon} - ) : ( - - )} - +

{skill.name}

{skill.slug}

diff --git a/apps/commons-app/app/studio/tasks/[taskId]/page.tsx b/apps/commons-app/app/studio/tasks/[taskId]/page.tsx index 092395e6..611afa33 100644 --- a/apps/commons-app/app/studio/tasks/[taskId]/page.tsx +++ b/apps/commons-app/app/studio/tasks/[taskId]/page.tsx @@ -258,7 +258,7 @@ export default function TaskDetailPage({

Task not found.

); diff --git a/apps/commons-app/components/agents/runtime-native-tooling.tsx b/apps/commons-app/components/agents/runtime-native-tooling.tsx index 1ab4ae2a..a1a7fbb4 100644 --- a/apps/commons-app/components/agents/runtime-native-tooling.tsx +++ b/apps/commons-app/components/agents/runtime-native-tooling.tsx @@ -13,9 +13,9 @@ import { Send, Server, ShieldCheck, - Sparkles, Unplug, X, + Zap, } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; @@ -1467,7 +1467,7 @@ export function RuntimeSkillsNote({ const meta = RUNTIME_NATIVE_TOOLING[key]; return (
- +

{meta.skillsNote}

); diff --git a/apps/commons-app/components/copilot/floating-commons-copilot.tsx b/apps/commons-app/components/copilot/floating-commons-copilot.tsx index bfc50fa5..89981251 100644 --- a/apps/commons-app/components/copilot/floating-commons-copilot.tsx +++ b/apps/commons-app/components/copilot/floating-commons-copilot.tsx @@ -47,7 +47,7 @@ const SCOPES = [ ["agents", "Agents"], ["tools", "Tools"], ["skills", "Skills"], - ["tasks", "Tasks"], + ["tasks", "Scheduled tasks"], ] as const; const PANEL_WIDTH_KEY = "copilot-panel-width"; diff --git a/apps/commons-app/components/customize/customize-tabs.tsx b/apps/commons-app/components/customize/customize-tabs.tsx index 2270d942..0fc49a1c 100644 --- a/apps/commons-app/components/customize/customize-tabs.tsx +++ b/apps/commons-app/components/customize/customize-tabs.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; -import { AppWindow, Sparkles } from "lucide-react"; +import { AppWindow, Zap } from "lucide-react"; import { cn } from "@/lib/utils"; const tabs = [ @@ -16,7 +16,7 @@ const tabs = [ label: "Skills", href: "/studio/customize/skills", segment: "/studio/customize/skills", - icon: Sparkles, + icon: Zap, }, ] as const; diff --git a/apps/commons-app/components/icons/clipboard-clock.tsx b/apps/commons-app/components/icons/clipboard-clock.tsx new file mode 100644 index 00000000..68c3a146 --- /dev/null +++ b/apps/commons-app/components/icons/clipboard-clock.tsx @@ -0,0 +1,40 @@ +import { forwardRef } from "react"; +import type { LucideProps } from "lucide-react"; + +/** + * `clipboard-clock` from Lucide, inlined because the pinned lucide-react + * (0.474) predates the icon. Same 24×24 geometry, props, and stroke defaults + * as every other Lucide glyph, so it sizes and colors identically alongside + * them. + * + * This is the single icon for scheduled tasks across the app. + */ +export const ClipboardClock = forwardRef( + ({ size = 24, strokeWidth = 2, absoluteStrokeWidth, ...props }, ref) => ( + + + + + + + + ) +); + +ClipboardClock.displayName = "ClipboardClock"; diff --git a/apps/commons-app/components/landing/landing-sidebar.tsx b/apps/commons-app/components/landing/landing-sidebar.tsx index b61664c5..88a821fb 100644 --- a/apps/commons-app/components/landing/landing-sidebar.tsx +++ b/apps/commons-app/components/landing/landing-sidebar.tsx @@ -2,18 +2,18 @@ import Image from "next/image"; import Link from "next/link"; import { Bot, - BriefcaseBusiness, LibraryBig, MoreHorizontal, Search, Wrench, Workflow, } from "lucide-react"; +import { ClipboardClock } from "@/components/icons/clipboard-clock"; const NAV_ITEMS = [ { label: "Agents", icon: Bot, target: "/studio/agents" }, { label: "Tools", icon: Wrench, target: "/studio/tools" }, - { label: "Tasks", icon: BriefcaseBusiness, target: "/studio/tasks" }, + { label: "Scheduled tasks", icon: ClipboardClock, target: "/studio/tasks" }, { label: "Workflows", icon: Workflow, target: "/studio/workflows" }, { label: "Library", icon: LibraryBig, target: "/library" }, ]; diff --git a/apps/commons-app/components/layout/dashboard-bar.tsx b/apps/commons-app/components/layout/dashboard-bar.tsx index ec6b504d..97658991 100644 --- a/apps/commons-app/components/layout/dashboard-bar.tsx +++ b/apps/commons-app/components/layout/dashboard-bar.tsx @@ -3,14 +3,8 @@ import { FC, ReactNode } from "react"; import { useRouter } from "next/navigation"; -import { - Bot, - BriefcaseBusiness, - LibraryBig, - Wrench, - Workflow, - Settings2, -} from "lucide-react"; +import { Bot, LibraryBig, Wrench, Workflow, Settings2 } from "lucide-react"; +import { ClipboardClock } from "@/components/icons/clipboard-clock"; import Link from "next/link"; import Image from "next/image"; import { SearchTrigger } from "@/components/search/search-trigger"; @@ -35,8 +29,8 @@ export const DashboardBar: FC = ({ { key: "tools", label: "Tools", icon: Wrench, path: "/studio/tools" }, { key: "tasks", - label: "Tasks", - icon: BriefcaseBusiness, + label: "Scheduled tasks", + icon: ClipboardClock, path: "/studio/tasks", }, { diff --git a/apps/commons-app/components/layout/dashboard-side-bar.tsx b/apps/commons-app/components/layout/dashboard-side-bar.tsx index 878ce708..355c6508 100644 --- a/apps/commons-app/components/layout/dashboard-side-bar.tsx +++ b/apps/commons-app/components/layout/dashboard-side-bar.tsx @@ -7,7 +7,6 @@ import Image from "next/image"; import { cn } from "@/lib/utils"; import { Bot, - BriefcaseBusiness, PanelLeft, PanelRight, LibraryBig, @@ -16,6 +15,7 @@ import { Workflow, Settings2, } from "lucide-react"; +import { ClipboardClock } from "@/components/icons/clipboard-clock"; import { ScrollArea } from "@/components/ui/scroll-area"; import { DashboardBar } from "./dashboard-bar"; import { SidebarAccount } from "./sidebar-account"; @@ -160,9 +160,9 @@ export function DashboardSideBar({ username }: { username: string }) { }, { key: "tasks", - icon: BriefcaseBusiness, + icon: ClipboardClock, path: "/studio/tasks", - label: "Tasks", + label: "Scheduled tasks", }, { key: "workflows", diff --git a/apps/commons-app/components/search/global-search.tsx b/apps/commons-app/components/search/global-search.tsx index ffcc0cfc..d4e69b18 100644 --- a/apps/commons-app/components/search/global-search.tsx +++ b/apps/commons-app/components/search/global-search.tsx @@ -15,7 +15,6 @@ import { Bot, MessageSquare, Wrench, - BriefcaseBusiness, Workflow, Earth, LibraryBig, @@ -23,6 +22,7 @@ import { Wallet, Loader2, } from "lucide-react"; +import { ClipboardClock } from "@/components/icons/clipboard-clock"; import { useAuth } from "@/context/AuthContext"; import { normalizePrincipalId } from "@/lib/principal-id"; import { useAgents } from "@/hooks/use-agents"; @@ -38,7 +38,7 @@ interface GlobalSearchProps { const NAV_ITEMS = [ { label: "Agents", path: "/studio/agents", icon: Bot, keywords: "agents" }, { label: "Tools", path: "/studio/tools", icon: Wrench, keywords: "tools integrations" }, - { label: "Tasks", path: "/studio/tasks", icon: BriefcaseBusiness, keywords: "tasks queue" }, + { label: "Scheduled tasks", path: "/studio/tasks", icon: ClipboardClock, keywords: "tasks queue scheduled cron recurring" }, { label: "Workflows", path: "/studio/workflows", icon: Workflow, keywords: "workflows automation" }, { label: "Spaces", path: "/spaces", icon: Earth, keywords: "spaces rooms live" }, { label: "Library", path: "/library", icon: LibraryBig, keywords: "library files documents collections" }, @@ -191,14 +191,14 @@ export function GlobalSearch({ open, onOpenChange }: GlobalSearchProps) { {tasks.length > 0 && ( <> - + {tasks.map((t) => ( go(`/studio/tasks/${t.taskId}`)} > - + {t.title} ))} diff --git a/apps/commons-app/components/skills/skill-icon.tsx b/apps/commons-app/components/skills/skill-icon.tsx new file mode 100644 index 00000000..d92036a4 --- /dev/null +++ b/apps/commons-app/components/skills/skill-icon.tsx @@ -0,0 +1,135 @@ +"use client"; + +import type { ElementType } from "react"; +import { + BarChart3, + Bot, + Brain, + Calendar, + Code2, + Database, + FileSpreadsheet, + FileText, + FileType, + Folder, + Github, + Globe, + Image, + Layers, + Link2, + Mail, + MessageSquare, + Monitor, + PanelsTopLeft, + PenLine, + Presentation, + Search, + Send, + Settings2, + Table2, + Terminal, + Users, + Video, + Workflow, + Wrench, + Zap, +} from "lucide-react"; +import { cn } from "@/lib/utils"; + +/** + * Skills store `icon` as either a Lucide icon name (platform skills seed + * kebab-case names like `panels-top-left`) or a single emoji (what the create + * form asks users for). Anything unrecognised falls back to the Zap mark + * rather than leaking a raw slug into the layout. + */ +const lucideByName: Record = { + "bar-chart-3": BarChart3, + bot: Bot, + brain: Brain, + calendar: Calendar, + "code-2": Code2, + code: Code2, + database: Database, + "file-spreadsheet": FileSpreadsheet, + "file-text": FileText, + "file-type": FileType, + folder: Folder, + github: Github, + globe: Globe, + image: Image, + layers: Layers, + "link-2": Link2, + mail: Mail, + "message-square": MessageSquare, + monitor: Monitor, + "panels-top-left": PanelsTopLeft, + "pen-line": PenLine, + presentation: Presentation, + search: Search, + send: Send, + "settings-2": Settings2, + "table-2": Table2, + terminal: Terminal, + users: Users, + video: Video, + workflow: Workflow, + wrench: Wrench, + zap: Zap, +}; + +/** `PanelsTopLeft` / `panelsTopLeft` / `Panels Top Left` → `panels-top-left`. */ +function toKebabCase(value: string) { + return value + .trim() + .replace(/([a-z0-9])([A-Z])/g, "$1-$2") + .replace(/[\s_]+/g, "-") + .toLowerCase(); +} + +/** Emoji and other pictographs are safe to render as text; slugs are not. */ +function isGlyph(value: string) { + const glyph = value.trim(); + if (!glyph || /[a-zA-Z0-9]/.test(glyph)) return false; + return Array.from(glyph).length <= 2; +} + +const sizeStyles = { + sm: { chip: "h-7 w-7 rounded-md", glyph: "text-sm", lucide: "h-3.5 w-3.5" }, + md: { chip: "h-9 w-9 rounded-lg", glyph: "text-base", lucide: "h-4 w-4" }, + lg: { chip: "h-11 w-11 rounded-xl", glyph: "text-lg", lucide: "h-5 w-5" }, +} as const; + +export function SkillIcon({ + icon, + size = "md", + className, +}: { + icon?: string | null; + size?: keyof typeof sizeStyles; + className?: string; +}) { + const styles = sizeStyles[size]; + const raw = icon?.trim() ?? ""; + const Lucide = raw ? lucideByName[toKebabCase(raw)] : undefined; + const glyph = !Lucide && isGlyph(raw) ? raw : null; + const Fallback = Zap; + + return ( + + {glyph ? ( + {glyph} + ) : Lucide ? ( + + ) : ( + + )} + + ); +} diff --git a/apps/commons-app/components/skills/skills-marketplace-view.tsx b/apps/commons-app/components/skills/skills-marketplace-view.tsx index 93632265..014f3a6c 100644 --- a/apps/commons-app/components/skills/skills-marketplace-view.tsx +++ b/apps/commons-app/components/skills/skills-marketplace-view.tsx @@ -3,7 +3,7 @@ import { useState, useEffect, useRef } from "react"; import { Loader2, - Sparkles, + Zap, Search, Trash2, Globe, @@ -35,6 +35,7 @@ import { import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Switch } from "@/components/ui/switch"; +import { SkillIcon } from "@/components/skills/skill-icon"; import type { Skill } from "@agent-commons/sdk"; interface SkillsMarketplaceViewProps { @@ -115,22 +116,39 @@ function SkillCard({ const enabledAgents = (skill.assignedAgents ?? []).filter( (assignment) => assignment.isEnabled ); + const assignmentLabel = enabledAgents.length + ? `Available to ${enabledAgents + .slice(0, 2) + .map((assignment) => assignment.agentName) + .join(", ")}${ + enabledAgents.length > 2 ? ` +${enabledAgents.length - 2}` : "" + }` + : "Not assigned to an agent"; return (
onOpen(skill.skillId)} + onKeyDown={(event) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + onOpen(skill.skillId); + } + }} > -
- - {skill.icon || ( - - )} - -
+
+ +
+

+ {skill.name} +

+

+ {skill.description} +

+
+
{ event.stopPropagation(); onDelete(skill.skillId); }} - aria-label="Delete skill" + aria-label={`Delete ${skill.name}`} > @@ -152,41 +170,26 @@ function SkillCard({
-

- {skill.name} -

-

- {skill.description} -

- - + + {assignmentLabel} + -
-
+
+
{skill.tags.slice(0, 3).map((tag) => ( {tag} @@ -194,14 +197,14 @@ function SkillCard({ {skill.tags.length > 3 && ( +{skill.tags.length - 3} )}
- - {skill.usageCount} uses + + {skill.usageCount} {skill.usageCount === 1 ? "use" : "uses"}
@@ -612,23 +615,25 @@ export function SkillsMarketplaceView({
) : filtered.length === 0 ? ( -
- -

+

+ +

{search ? "No skills match your search" : tab === "mine" ? "No skills yet" : "No platform skills"}

- {!search && tab === "mine" && ( -

- Create your first skill to add custom capabilities to your agents -

- )} +

+ {search + ? "Try a different name, description, or tag." + : tab === "mine" + ? "Create your first skill to add custom capabilities to your agents." + : "Platform skills will appear here once they are published."} +

) : ( -
+
{filtered.map((skill) => (