Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion apps/web/src/components/settings/settings-tab-groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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",
Expand Down
23 changes: 23 additions & 0 deletions apps/web/src/i18n/en/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 23 additions & 0 deletions apps/web/src/i18n/pt-br/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
24 changes: 20 additions & 4 deletions apps/web/src/layouts/settings-layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -160,17 +163,30 @@ function useSettingsSidebarGroups(): SettingsNavGroup[] {
key: "build",
label: t("settings.nav.build"),
items: [
{
key: "agents",
label: t("settings.nav.agents"),
icon: <Folder size={14} />,
to: "/$org/settings/agents",
},
{
key: "connections",
label: t("settings.nav.connections"),
icon: <ZapSquare size={14} />,
to: "/$org/settings/connections",
},
{
key: "agents",
label: t("settings.nav.agents"),
icon: <Users03 size={14} />,
to: "/$org/settings/agents",
key: "skills",
label: t("settings.nav.skills"),
icon: <Lightbulb02 size={14} />,
to: "/$org/settings/skills",
group: "skills",
},
{
key: "memory",
label: t("settings.nav.memory"),
icon: <Database01 size={14} />,
to: "/$org/settings/memory",
},
{
key: "automations",
Expand Down
21 changes: 20 additions & 1 deletion apps/web/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
),
Expand Down Expand Up @@ -681,6 +698,8 @@ const settingsWithChildren = settingsLayout.addChildren([
settingsApiKeysRoute,
settingsBucketsRoute,
settingsSyncedReposRoute,
settingsSkillsRoute,
settingsMemoryRoute,
settingsTasksRoute,
settingsMembersRoute,
settingsRolesRoute,
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/routes/orgs/settings/memory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { OrgMemoryPage } from "@/views/settings/memory";

export default function MemoryRoute() {
return <OrgMemoryPage />;
}
5 changes: 5 additions & 0 deletions apps/web/src/routes/orgs/settings/skills.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { OrgSkillsPage } from "@/views/settings/skills";

export default function SkillsRoute() {
return <OrgSkillsPage />;
}
139 changes: 139 additions & 0 deletions apps/web/src/views/settings/memory.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<Page>
<Page.Content>
<Page.Body>
<SettingsPage>
<SettingsSection
title={t("settings.memory.title")}
description={t("settings.memory.description")}
actions={
<Button size="sm" disabled>
<Plus size={16} />
{t("settings.memory.add")}
</Button>
}
>
<p className="px-4 text-xs text-muted-foreground">
{t("settings.memory.mockNotice")}
</p>
<div className="relative px-4">
<SearchSm
size={16}
className="pointer-events-none absolute left-7 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<Input
value={query}
onChange={(e) => setQuery(e.currentTarget.value)}
placeholder={t("settings.memory.searchPlaceholder")}
className="pl-9"
/>
</div>
<SettingsCard>
{memories.length === 0 ? (
<SettingsCardItem
title={t("settings.memory.emptyTitle")}
description={t("settings.memory.emptyDescription")}
/>
) : visible.length === 0 ? (
<SettingsCardItem title={t("settings.memory.noMatches")} />
) : (
visible.map((memory) => (
<SettingsCardItem
key={memory.id}
icon={<Database01 size={16} />}
title={memory.body}
description={`${t("settings.memory.writtenBy", {
source: memory.source,
})} · ${timeAgo(memory.writtenAt)}`}
action={
<Button
variant="ghost"
size="icon-sm"
aria-label={t("settings.memory.remove")}
onClick={() =>
setMemories((prev) =>
prev.filter((m) => m.id !== memory.id),
)
}
>
<Trash03 size={16} />
</Button>
}
/>
))
)}
</SettingsCard>
</SettingsSection>
</SettingsPage>
</Page.Body>
</Page.Content>
</Page>
);
}
Loading
Loading