diff --git a/apps/web/src/components/settings/settings-tab-groups.ts b/apps/web/src/components/settings/settings-tab-groups.ts index 6697aa8bb2..d501ef590e 100644 --- a/apps/web/src/components/settings/settings-tab-groups.ts +++ b/apps/web/src/components/settings/settings-tab-groups.ts @@ -14,7 +14,12 @@ import type { CapabilityId } from "@/hooks/use-capability"; import type { TranslationKey } from "@/i18n/use-t.ts"; -export type SettingsGroupKey = "connect" | "members" | "billing" | "storage"; +export type SettingsGroupKey = + | "connect" + | "members" + | "billing" + | "storage" + | "skills"; export interface SettingsTabDef { /** Stable id for React keys and analytics — never localized. */ @@ -105,6 +110,21 @@ export const SETTINGS_TAB_GROUPS: Record< to: "/$org/settings/buckets", requires: "file-configs:manage", }, + ], + }, + /** + * Syncing skills from a repo is a skills concern, not a storage one — it is + * how an org keeps the agent's skills current without hand-uploading each. + */ + skills: { + key: "skills", + titleKey: "settings.nav.skills", + tabs: [ + { + key: "skills", + labelKey: "settings.nav.skills", + to: "/$org/settings/skills", + }, { key: "synced-repos", labelKey: "settings.nav.syncedRepos", diff --git a/apps/web/src/i18n/en/settings.ts b/apps/web/src/i18n/en/settings.ts index dde22e4082..abc20cdf2a 100644 --- a/apps/web/src/i18n/en/settings.ts +++ b/apps/web/src/i18n/en/settings.ts @@ -110,6 +110,29 @@ export const settings = { "settings.syncedRepos.rowSubtitle": "Library folder: {volume}", "settings.nav.connections": "Connections", "settings.nav.agents": "Agents", + "settings.nav.skills": "Skills", + "settings.nav.memory": "Memory", + "settings.skills.title": "Skills", + "settings.skills.description": + "Instructions your agents can load on demand. Each skill is a SKILL.md folder in the Library.", + "settings.skills.add": "Add skill", + "settings.skills.viewInLibrary": "View in Library", + "settings.skills.emptyTitle": "No skills yet", + "settings.skills.emptyDescription": + "Add a SKILL.md folder in the Library, or sync a repo of them from the Synced repos tab.", + "settings.memory.title": "Memory", + "settings.memory.description": + "What your agents remember across tasks. Facts written here are available to every run.", + "settings.memory.add": "Add memory", + "settings.memory.searchPlaceholder": "Search memory", + "settings.memory.emptyTitle": "Nothing remembered yet", + "settings.memory.emptyDescription": + "Agents write here as they learn how your team works. You can also add a fact yourself.", + "settings.memory.noMatches": "No memories match that search.", + "settings.memory.mockNotice": + "Preview: these entries are sample data and are not saved yet.", + "settings.memory.remove": "Remove", + "settings.memory.writtenBy": "Written by {source}", "settings.nav.automations": "Automations", "settings.nav.store": "Store", "settings.nav.monitor": "Monitor", diff --git a/apps/web/src/i18n/pt-br/settings.ts b/apps/web/src/i18n/pt-br/settings.ts index b6e41405df..b6e1dd3fea 100644 --- a/apps/web/src/i18n/pt-br/settings.ts +++ b/apps/web/src/i18n/pt-br/settings.ts @@ -116,6 +116,29 @@ export const settings = { "settings.syncedRepos.rowSubtitle": "Pasta da biblioteca: {volume}", "settings.nav.connections": "Conexões", "settings.nav.agents": "Agentes", + "settings.nav.skills": "Skills", + "settings.nav.memory": "Memória", + "settings.skills.title": "Skills", + "settings.skills.description": + "Instruções que seus agentes podem carregar quando precisam. Cada skill é uma pasta com SKILL.md na Biblioteca.", + "settings.skills.add": "Adicionar skill", + "settings.skills.viewInLibrary": "Ver na Biblioteca", + "settings.skills.emptyTitle": "Nenhuma skill ainda", + "settings.skills.emptyDescription": + "Adicione uma pasta com SKILL.md na Biblioteca, ou sincronize um repositório na aba Repos sincronizados.", + "settings.memory.title": "Memória", + "settings.memory.description": + "O que seus agentes lembram entre tarefas. Fatos aqui ficam disponíveis para todas as execuções.", + "settings.memory.add": "Adicionar memória", + "settings.memory.searchPlaceholder": "Buscar na memória", + "settings.memory.emptyTitle": "Nada memorizado ainda", + "settings.memory.emptyDescription": + "Os agentes escrevem aqui conforme aprendem como seu time trabalha. Você também pode adicionar um fato.", + "settings.memory.noMatches": "Nenhuma memória corresponde à busca.", + "settings.memory.mockNotice": + "Prévia: estes registros são dados de exemplo e ainda não são salvos.", + "settings.memory.remove": "Remover", + "settings.memory.writtenBy": "Escrito por {source}", "settings.nav.automations": "Automações", "settings.nav.store": "Loja", "settings.nav.monitor": "Monitoramento", diff --git a/apps/web/src/layouts/settings-layout.tsx b/apps/web/src/layouts/settings-layout.tsx index 0ec3307c1e..6f481a2221 100644 --- a/apps/web/src/layouts/settings-layout.tsx +++ b/apps/web/src/layouts/settings-layout.tsx @@ -49,6 +49,9 @@ import { Key01, HardDrive, LinkExternal01, + Lightbulb02, + Database01, + Folder, } from "@untitledui/icons"; import { useProjectContext } from "@/sdk"; import { useT } from "@/i18n/use-t.ts"; @@ -160,6 +163,12 @@ function useSettingsSidebarGroups(): SettingsNavGroup[] { key: "build", label: t("settings.nav.build"), items: [ + { + key: "agents", + label: t("settings.nav.agents"), + icon: , + to: "/$org/settings/agents", + }, { key: "connections", label: t("settings.nav.connections"), @@ -167,10 +176,17 @@ function useSettingsSidebarGroups(): SettingsNavGroup[] { to: "/$org/settings/connections", }, { - key: "agents", - label: t("settings.nav.agents"), - icon: , - to: "/$org/settings/agents", + key: "skills", + label: t("settings.nav.skills"), + icon: , + to: "/$org/settings/skills", + group: "skills", + }, + { + key: "memory", + label: t("settings.nav.memory"), + icon: , + to: "/$org/settings/memory", }, { key: "automations", diff --git a/apps/web/src/router.tsx b/apps/web/src/router.tsx index 4944874848..74e083f9d4 100644 --- a/apps/web/src/router.tsx +++ b/apps/web/src/router.tsx @@ -563,10 +563,27 @@ const settingsBucketsRoute = createRoute({ ), }); +const settingsSkillsRoute = createRoute({ + getParentRoute: () => settingsLayout, + path: "/skills", + pendingComponent: settingsGroupPendingComponent("skills"), + component: lazyRouteComponent( + () => import("./routes/orgs/settings/skills.tsx"), + ), +}); + +const settingsMemoryRoute = createRoute({ + getParentRoute: () => settingsLayout, + path: "/memory", + component: lazyRouteComponent( + () => import("./routes/orgs/settings/memory.tsx"), + ), +}); + const settingsSyncedReposRoute = createRoute({ getParentRoute: () => settingsLayout, path: "/synced-repos", - pendingComponent: settingsGroupPendingComponent("storage"), + pendingComponent: settingsGroupPendingComponent("skills"), component: lazyRouteComponent( () => import("./routes/orgs/settings/synced-repos.tsx"), ), @@ -681,6 +698,8 @@ const settingsWithChildren = settingsLayout.addChildren([ settingsApiKeysRoute, settingsBucketsRoute, settingsSyncedReposRoute, + settingsSkillsRoute, + settingsMemoryRoute, settingsTasksRoute, settingsMembersRoute, settingsRolesRoute, diff --git a/apps/web/src/routes/orgs/settings/memory.tsx b/apps/web/src/routes/orgs/settings/memory.tsx new file mode 100644 index 0000000000..e6a61aacda --- /dev/null +++ b/apps/web/src/routes/orgs/settings/memory.tsx @@ -0,0 +1,5 @@ +import { OrgMemoryPage } from "@/views/settings/memory"; + +export default function MemoryRoute() { + return ; +} diff --git a/apps/web/src/routes/orgs/settings/skills.tsx b/apps/web/src/routes/orgs/settings/skills.tsx new file mode 100644 index 0000000000..641c817010 --- /dev/null +++ b/apps/web/src/routes/orgs/settings/skills.tsx @@ -0,0 +1,5 @@ +import { OrgSkillsPage } from "@/views/settings/skills"; + +export default function SkillsRoute() { + return ; +} diff --git a/apps/web/src/views/settings/memory.tsx b/apps/web/src/views/settings/memory.tsx new file mode 100644 index 0000000000..6c7bfa3c68 --- /dev/null +++ b/apps/web/src/views/settings/memory.tsx @@ -0,0 +1,139 @@ +/** + * Settings → Memory — what the org's agents remember across tasks. + * + * MOCK: the entries below are sample data. The server keeps memory per run + * (`apps/api/src/api/routes/decopilot/memory.ts`) and exposes no org-level + * read/write yet, so nothing here persists. The notice on the page says so — + * a settings screen that silently forgets is worse than no screen. + */ + +import { useState } from "react"; +import { Database01, Plus, SearchSm, Trash03 } from "@untitledui/icons"; +import { Button } from "@decocms/ui/components/button.tsx"; +import { Input } from "@decocms/ui/components/input.tsx"; +import { Page } from "@/components/page"; +import { + SettingsCard, + SettingsCardItem, + SettingsPage, + SettingsSection, +} from "@/components/settings/settings-section"; +import { timeAgo } from "@/layouts/library/cards"; +import { useT } from "@/i18n/use-t.ts"; + +interface MemoryEntry { + id: string; + body: string; + /** Who wrote it: an agent session, or a person. */ + source: string; + writtenAt: string; +} + +/** Sample data. See the file header: nothing here is persisted. */ +const SAMPLE_MEMORIES: MemoryEntry[] = [ + { + id: "m1", + body: "Storefront PDP changes always need a Playwright before/after screenshot on the card.", + source: "DECO-12", + writtenAt: "2026-08-18T14:20:00Z", + }, + { + id: "m2", + body: "This org uses design system tokens only. Raw palette classes get reverted in review.", + source: "Code reviewer", + writtenAt: "2026-08-15T09:05:00Z", + }, + { + id: "m3", + body: "Publishing to production is gated on the QA agent approving first.", + source: "DECO-07", + writtenAt: "2026-08-11T17:42:00Z", + }, +]; + +export function OrgMemoryPage() { + const t = useT(); + const [query, setQuery] = useState(""); + const [memories, setMemories] = useState(SAMPLE_MEMORIES); + + const needle = query.trim().toLowerCase(); + const visible = needle + ? memories.filter( + (memory) => + memory.body.toLowerCase().includes(needle) || + memory.source.toLowerCase().includes(needle), + ) + : memories; + + return ( + + + + + + + {t("settings.memory.add")} + + } + > +

+ {t("settings.memory.mockNotice")} +

+
+ + setQuery(e.currentTarget.value)} + placeholder={t("settings.memory.searchPlaceholder")} + className="pl-9" + /> +
+ + {memories.length === 0 ? ( + + ) : visible.length === 0 ? ( + + ) : ( + visible.map((memory) => ( + } + title={memory.body} + description={`${t("settings.memory.writtenBy", { + source: memory.source, + })} · ${timeAgo(memory.writtenAt)}`} + action={ + + } + /> + )) + )} + +
+
+
+
+
+ ); +} diff --git a/apps/web/src/views/settings/skills.tsx b/apps/web/src/views/settings/skills.tsx new file mode 100644 index 0000000000..9d7232fd34 --- /dev/null +++ b/apps/web/src/views/settings/skills.tsx @@ -0,0 +1,111 @@ +/** + * Settings → Skills — the skills the org's agents can load. + * + * Skills already exist (org-fs `SKILL.md` folders, surfaced in the Library and + * loadable by the `skill` tool) but there was nowhere to see which ones an org + * has, so nobody knew the setting existed. This is that page: the catalog the + * server resolves, with a route into the Library to add or edit one, and + * Synced repos alongside for keeping them current from a repo. + */ + +import { Link } from "@tanstack/react-router"; +import { Lightbulb02, Plus } from "@untitledui/icons"; +import { Button } from "@decocms/ui/components/button.tsx"; +import { Skeleton } from "@decocms/ui/components/skeleton.tsx"; +import { useQuery } from "@tanstack/react-query"; +import { + SettingsCard, + SettingsCardItem, + SettingsSection, +} from "@/components/settings/settings-section"; +import { SettingsGroupPage } from "@/components/settings/settings-group-page"; +import { + fetchOrgFsSkillCatalog, + type OrgFsSkillCatalogEntry, +} from "@/hooks/use-org-fs"; +import { KEYS } from "@/lib/query-keys"; +import { useProjectContext } from "@/sdk"; +import { useT } from "@/i18n/use-t.ts"; + +export function OrgSkillsPage() { + const t = useT(); + const { org } = useProjectContext(); + const { data: skills, isPending } = useQuery({ + queryKey: KEYS.slashSkills(org.id), + queryFn: () => fetchOrgFsSkillCatalog(org.slug), + }); + + return ( + + + + + {t("settings.skills.add")} + + + } + > + {isPending ? ( + + + + + ) : !skills || skills.length === 0 ? ( + + + + ) : ( + + {skills.map((skill) => ( + + ))} + + )} + + + ); +} + +function SkillRow({ + skill, + orgSlug, +}: { + skill: OrgFsSkillCatalogEntry; + orgSlug: string; +}) { + const t = useT(); + return ( + + + + } + title={skill.name} + description={skill.description ?? skill.path} + action={ + + } + /> + ); +}