feat(web): add skills & design systems management page in settings (#535)

* feat(web): add skills & design systems management page in settings

Add a new "Library" section in Settings that lets users browse, search,
preview, and enable/disable skills and design systems. Disabled items are
excluded from the create-project picker. Phase 1 — browse/toggle only.

Closes #497

* fix(web): persist empty disabled lists and deduplicate DS preview

Use empty array instead of undefined when all items are re-enabled so
the daemon merge clears the key. Move DS preview panel outside the
category group loop so it renders once, not per group.

* fix(web): address review feedback on library settings

Clear disabled lists on invalid daemon writes, memoize enabled item
filters in App.tsx, and guard preview fetch against rapid-click race
conditions.

* fix(web): hydrate disabled lists from daemon and keep full lists in ProjectView

Merge daemonConfig.disabledSkills/disabledDesignSystems during bootstrap
so the values survive localStorage resets. Pass unfiltered skills and
design systems to ProjectView so existing project metadata resolves
correctly.
This commit is contained in:
Justin Gao 2026-05-05 22:50:25 +08:00 committed by GitHub
parent 4fa2df2ae3
commit cbe2baf596
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 952 additions and 3 deletions

View file

@ -22,6 +22,8 @@ export interface AppConfigPrefs {
agentModels?: Record<string, AgentModelPrefs>;
skillId?: string | null;
designSystemId?: string | null;
disabledSkills?: string[];
disabledDesignSystems?: string[];
}
const ALLOWED_KEYS: ReadonlySet<keyof AppConfigPrefs> = new Set([
@ -30,6 +32,8 @@ const ALLOWED_KEYS: ReadonlySet<keyof AppConfigPrefs> = new Set([
'agentModels',
'skillId',
'designSystemId',
'disabledSkills',
'disabledDesignSystems',
] as const);
function configFile(dataDir: string): string {
@ -84,6 +88,13 @@ function applyConfigValue(
delete target[key];
}
}
if (key === 'disabledSkills' || key === 'disabledDesignSystems') {
if (Array.isArray(value) && value.every((v) => typeof v === 'string')) {
target[key] = value;
} else {
delete target[key];
}
}
}
function filterAllowedKeys(obj: Record<string, unknown>): AppConfigPrefs {

View file

@ -216,6 +216,49 @@ function httpRequest(
});
}
describe('app-config disabled lists', () => {
let dataDir: string;
beforeEach(async () => {
dataDir = await mkdtemp(path.join(tmpdir(), 'od-disabled-'));
});
afterEach(async () => {
await rm(dataDir, { recursive: true, force: true });
});
it('persists disabledSkills as string array', async () => {
await writeAppConfig(dataDir, { disabledSkills: ['skill-a', 'skill-b'] });
const cfg = await readAppConfig(dataDir);
expect(cfg.disabledSkills).toEqual(['skill-a', 'skill-b']);
});
it('persists disabledDesignSystems as string array', async () => {
await writeAppConfig(dataDir, { disabledDesignSystems: ['ds-x'] });
const cfg = await readAppConfig(dataDir);
expect(cfg.disabledDesignSystems).toEqual(['ds-x']);
});
it('drops disabledSkills when not a string array', async () => {
await writeAppConfig(dataDir, { disabledSkills: 'not-array' } as any);
const cfg = await readAppConfig(dataDir);
expect(cfg.disabledSkills).toBeUndefined();
});
it('drops disabledSkills with non-string elements', async () => {
await writeAppConfig(dataDir, { disabledSkills: [1, 2, 3] } as any);
const cfg = await readAppConfig(dataDir);
expect(cfg.disabledSkills).toBeUndefined();
});
it('clears disabledSkills when empty array is sent', async () => {
await writeAppConfig(dataDir, { disabledSkills: ['a'] });
await writeAppConfig(dataDir, { disabledSkills: [] });
const cfg = await readAppConfig(dataDir);
expect(cfg.disabledSkills).toEqual([]);
});
});
describe('app-config origin guard', () => {
let server: http.Server;
let port: number;

View file

@ -1,4 +1,4 @@
import { useCallback, useEffect, useLayoutEffect, useState } from 'react';
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { EntryView } from './components/EntryView';
import type { CreateInput } from './components/NewProjectPanel';
import { PetOverlay } from './components/pet/PetOverlay';
@ -182,6 +182,12 @@ export function App() {
...daemonConfig.agentModels,
};
}
if (daemonConfig.disabledSkills !== undefined) {
next.disabledSkills = daemonConfig.disabledSkills;
}
if (daemonConfig.disabledDesignSystems !== undefined) {
next.disabledDesignSystems = daemonConfig.disabledDesignSystems;
}
}
if (alive) {
@ -520,6 +526,18 @@ export function App() {
void refreshTemplates();
}, [route.kind, refreshTemplates]);
const enabledSkills = useMemo(
() => skills.filter((s) => !(config.disabledSkills ?? []).includes(s.id)),
[skills, config.disabledSkills],
);
const enabledDS = useMemo(
() =>
designSystems.filter(
(d) => !(config.disabledDesignSystems ?? []).includes(d.id),
),
[designSystems, config.disabledDesignSystems],
);
return (
<>
{activeProject ? (
@ -548,8 +566,8 @@ export function App() {
/>
) : (
<EntryView
skills={skills}
designSystems={designSystems}
skills={enabledSkills}
designSystems={enabledDS}
projects={projects}
templates={templates}
promptTemplates={promptTemplates}

View file

@ -0,0 +1,369 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { Dispatch, SetStateAction } from 'react';
import { useT } from '../i18n';
import { Icon } from './Icon';
import type { AppConfig } from '../types';
import type { SkillSummary, DesignSystemSummary } from '@open-design/contracts';
import {
fetchSkills,
fetchDesignSystems,
fetchSkill,
fetchDesignSystem,
} from '../providers/registry';
type Tab = 'skills' | 'design-systems';
interface Props {
cfg: AppConfig;
setCfg: Dispatch<SetStateAction<AppConfig>>;
}
const MODES = [
'prototype',
'deck',
'template',
'design-system',
'image',
'video',
'audio',
] as const;
export function LibrarySection({ cfg, setCfg }: Props) {
const t = useT();
const [tab, setTab] = useState<Tab>('skills');
const [search, setSearch] = useState('');
const [modeFilter, setModeFilter] = useState('all');
const [categoryFilter, setCategoryFilter] = useState('All');
const [skills, setSkills] = useState<SkillSummary[]>([]);
const [designSystems, setDesignSystems] = useState<DesignSystemSummary[]>([]);
const [previewId, setPreviewId] = useState<string | null>(null);
const [previewBody, setPreviewBody] = useState<string | null>(null);
const [previewLoading, setPreviewLoading] = useState(false);
useEffect(() => {
fetchSkills().then(setSkills);
fetchDesignSystems().then(setDesignSystems);
}, []);
const categories = useMemo(() => {
const cats = new Set(designSystems.map((d) => d.category));
return ['All', ...Array.from(cats).sort()];
}, [designSystems]);
const disabledSkills = useMemo(
() => new Set(cfg.disabledSkills ?? []),
[cfg.disabledSkills],
);
const disabledDS = useMemo(
() => new Set(cfg.disabledDesignSystems ?? []),
[cfg.disabledDesignSystems],
);
const filteredSkills = useMemo(() => {
const q = search.toLowerCase();
return skills.filter((s) => {
if (modeFilter !== 'all' && s.mode !== modeFilter) return false;
if (q && !s.name.toLowerCase().includes(q) && !s.description.toLowerCase().includes(q))
return false;
return true;
});
}, [skills, modeFilter, search]);
const filteredDS = useMemo(() => {
const q = search.toLowerCase();
return designSystems.filter((d) => {
if (categoryFilter !== 'All' && d.category !== categoryFilter) return false;
if (q && !d.title.toLowerCase().includes(q) && !d.summary.toLowerCase().includes(q))
return false;
return true;
});
}, [designSystems, categoryFilter, search]);
const groupedSkills = useMemo(() => {
const groups = new Map<string, SkillSummary[]>();
for (const s of filteredSkills) {
const list = groups.get(s.mode) ?? [];
list.push(s);
groups.set(s.mode, list);
}
return groups;
}, [filteredSkills]);
const groupedDS = useMemo(() => {
const groups = new Map<string, DesignSystemSummary[]>();
for (const d of filteredDS) {
const list = groups.get(d.category) ?? [];
list.push(d);
groups.set(d.category, list);
}
return groups;
}, [filteredDS]);
const openPreview = useCallback(
async (id: string) => {
if (previewId === id) {
setPreviewId(null);
setPreviewBody(null);
return;
}
setPreviewId(id);
setPreviewBody(null);
setPreviewLoading(true);
try {
const detail =
tab === 'skills'
? await fetchSkill(id)
: await fetchDesignSystem(id);
setPreviewId((cur) => {
if (cur === id) setPreviewBody(detail?.body ?? null);
return cur;
});
} catch {
setPreviewId((cur) => {
if (cur === id) setPreviewBody(null);
return cur;
});
} finally {
setPreviewId((cur) => {
if (cur === id) setPreviewLoading(false);
return cur;
});
}
},
[previewId, tab],
);
function toggleSkillDisabled(id: string, disabled: boolean) {
setCfg((c) => {
const set = new Set(c.disabledSkills ?? []);
if (disabled) set.add(id);
else set.delete(id);
return { ...c, disabledSkills: [...set] };
});
}
function toggleDSDisabled(id: string, disabled: boolean) {
setCfg((c) => {
const set = new Set(c.disabledDesignSystems ?? []);
if (disabled) set.add(id);
else set.delete(id);
return { ...c, disabledDesignSystems: [...set] };
});
}
return (
<section className="settings-section">
<div className="section-head">
<div>
<h3>{t('settings.library')}</h3>
<p className="hint">{t('settings.libraryHint')}</p>
</div>
</div>
<div className="seg-control" role="tablist">
<button
type="button"
role="tab"
className={`seg-btn${tab === 'skills' ? ' active' : ''}`}
onClick={() => {
setTab('skills');
setModeFilter('all');
setCategoryFilter('All');
setSearch('');
setPreviewId(null);
}}
>
<span className="seg-title">
{t('settings.librarySkills')}
<span className="seg-meta">{skills.length}</span>
</span>
</button>
<button
type="button"
role="tab"
className={`seg-btn${tab === 'design-systems' ? ' active' : ''}`}
onClick={() => {
setTab('design-systems');
setModeFilter('all');
setCategoryFilter('All');
setSearch('');
setPreviewId(null);
}}
>
<span className="seg-title">
{t('settings.libraryDesignSystems')}
<span className="seg-meta">{designSystems.length}</span>
</span>
</button>
</div>
<div className="library-toolbar">
<input
type="search"
className="library-search"
placeholder={t('settings.librarySearch')}
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
{tab === 'skills' ? (
<div className="library-filters">
<button
type="button"
className={`filter-pill${modeFilter === 'all' ? ' active' : ''}`}
onClick={() => setModeFilter('all')}
>
{t('settings.libraryAll')}
</button>
{MODES.map((mode) => {
const count = skills.filter((s) => s.mode === mode).length;
if (count === 0) return null;
return (
<button
key={mode}
type="button"
className={`filter-pill${modeFilter === mode ? ' active' : ''}`}
onClick={() => setModeFilter(mode)}
>
{mode}
<span className="filter-pill-count">{count}</span>
</button>
);
})}
</div>
) : (
<div className="library-filters">
{categories.map((cat) => {
const count =
cat === 'All'
? designSystems.length
: designSystems.filter((d) => d.category === cat).length;
return (
<button
key={cat}
type="button"
className={`filter-pill${categoryFilter === cat ? ' active' : ''}`}
onClick={() => setCategoryFilter(cat)}
>
{cat}
<span className="filter-pill-count">{count}</span>
</button>
);
})}
</div>
)}
</div>
<div className="library-content">
{tab === 'skills' ? (
filteredSkills.length === 0 ? (
<p className="library-empty">{t('settings.libraryNoResults')}</p>
) : (
MODES.filter((m) => groupedSkills.has(m)).map((mode) => (
<div key={mode} className="library-group">
<h4 className="library-group-title">
{mode}{' '}
<span className="library-group-count">{groupedSkills.get(mode)!.length}</span>
</h4>
{groupedSkills.get(mode)!.map((skill) => (
<div
key={skill.id}
className={`library-card${disabledSkills.has(skill.id) ? ' disabled' : ''}`}
>
<div className="library-card-info">
<div className="library-card-title-row">
<span className="library-card-name">{skill.name}</span>
<span className="library-card-badge">{skill.previewType}</span>
</div>
<div className="library-card-desc">{skill.description}</div>
</div>
<button
type="button"
className="library-card-expand"
onClick={() => openPreview(skill.id)}
title={t('settings.libraryPreview')}
>
<Icon
name={previewId === skill.id ? 'close' : 'chevron-right'}
size={14}
/>
</button>
<label className="toggle-switch" title={t('settings.libraryToggleLabel')}>
<input
type="checkbox"
checked={!disabledSkills.has(skill.id)}
onChange={(e) => toggleSkillDisabled(skill.id, !e.target.checked)}
/>
<span className="toggle-slider" />
</label>
{previewId === skill.id && (
<div className="library-preview">
{previewLoading ? (
<p>{t('settings.libraryLoading')}</p>
) : previewBody ? (
<pre className="library-preview-body">{previewBody}</pre>
) : null}
</div>
)}
</div>
))}
</div>
))
)
) : filteredDS.length === 0 ? (
<p className="library-empty">{t('settings.libraryNoResults')}</p>
) : (
<>
{Array.from(groupedDS.entries()).map(([category, items]) => (
<div key={category} className="library-group">
<h4 className="library-group-title">
{category} <span className="library-group-count">{items.length}</span>
</h4>
<div className="ds-grid">
{items.map((ds) => (
<div
key={ds.id}
className={`library-ds-card${disabledDS.has(ds.id) ? ' disabled' : ''}`}
>
<div className="library-ds-card-content" onClick={() => openPreview(ds.id)}>
{ds.swatches && ds.swatches.length > 0 && (
<div className="library-ds-swatches">
{ds.swatches.slice(0, 4).map((c, i) => (
<span
key={i}
className="library-ds-swatch"
style={{ backgroundColor: c }}
/>
))}
</div>
)}
<div className="library-ds-title">{ds.title}</div>
<div className="library-ds-summary">{ds.summary}</div>
</div>
<label className="toggle-switch toggle-switch-sm" title={t('settings.libraryToggleLabel')}>
<input
type="checkbox"
checked={!disabledDS.has(ds.id)}
onChange={(e) => toggleDSDisabled(ds.id, !e.target.checked)}
/>
<span className="toggle-slider" />
</label>
</div>
))}
</div>
</div>
))}
{previewId && filteredDS.some((d) => d.id === previewId) && (
<div className="library-preview">
{previewLoading ? (
<p>{t('settings.libraryLoading')}</p>
) : previewBody ? (
<pre className="library-preview-body">{previewBody}</pre>
) : null}
</div>
)}
</>
)}
</div>
</section>
);
}

View file

@ -19,6 +19,7 @@ import type { AgentInfo, ApiProtocol, ApiProtocolConfig, AppConfig, AppTheme, Ap
import { MEDIA_PROVIDERS } from '../media/models';
import type { MediaProvider } from '../media/models';
import { PetSettings } from './pet/PetSettings';
import { LibrarySection } from './LibrarySection';
import { DEFAULT_NOTIFICATIONS } from '../state/config';
import {
FAILURE_SOUNDS,
@ -38,6 +39,7 @@ export type SettingsSection =
| 'appearance'
| 'notifications'
| 'pet'
| 'library'
| 'about';
interface Props {
@ -507,6 +509,17 @@ export function SettingsDialog({
<small>{t('pet.navHint')}</small>
</span>
</button>
<button
type="button"
className={`settings-nav-item${activeSection === 'library' ? ' active' : ''}`}
onClick={() => setActiveSection('library')}
>
<Icon name="grid" size={18} />
<span>
<strong>{t('settings.library')}</strong>
<small>{t('settings.libraryHint')}</small>
</span>
</button>
<button
type="button"
className={`settings-nav-item${activeSection === 'about' ? ' active' : ''}`}
@ -1011,6 +1024,10 @@ export function SettingsDialog({
<PetSettings cfg={cfg} setCfg={setCfg} />
) : null}
{activeSection === 'library' ? (
<LibrarySection cfg={cfg} setCfg={setCfg} />
) : null}
{activeSection === 'about' ? (
<section className="settings-section">
<div className="section-head">

View file

@ -875,6 +875,19 @@ export const ar: Dict = {
'settings.notifySoundBuzz': 'طنين',
'settings.notifySoundTwoToneDown': 'نغمتان هابطتان',
'settings.notifySoundThud': 'دمدمة',
'settings.library': 'المهارات وأنظمة التصميم',
'settings.libraryHint': 'تصفح ومعاينة وتفعيل/تعطيل مكتبة المحتوى الخاصة بك',
'settings.librarySkills': 'المهارات',
'settings.libraryDesignSystems': 'أنظمة التصميم',
'settings.librarySearch': 'بحث...',
'settings.libraryAll': 'الكل',
'settings.libraryPreview': 'معاينة',
'settings.libraryPreviewClose': 'إغلاق',
'settings.libraryLoading': 'جارٍ التحميل...',
'settings.libraryNoResults': 'لا توجد عناصر تطابق بحثك.',
'settings.libraryEnabled': 'مفعّل',
'settings.libraryDisabled': 'معطّل',
'settings.libraryToggleLabel': 'تبديل',
'notify.successTitle': 'اكتملت المهمة',
'notify.failureTitle': 'فشلت المهمة',
'notify.successBody': 'انتهت جولة.',

View file

@ -829,6 +829,19 @@ export const de: Dict = {
'settings.notifySoundBuzz': 'Summen',
'settings.notifySoundTwoToneDown': 'Zweiton abwärts',
'settings.notifySoundThud': 'Dumpfer Schlag',
'settings.library': 'Fähigkeiten & Designsysteme',
'settings.libraryHint': 'Inhaltsbibliothek durchsuchen, vorschauen und umschalten',
'settings.librarySkills': 'Fähigkeiten',
'settings.libraryDesignSystems': 'Designsysteme',
'settings.librarySearch': 'Suchen...',
'settings.libraryAll': 'Alle',
'settings.libraryPreview': 'Vorschau',
'settings.libraryPreviewClose': 'Schließen',
'settings.libraryLoading': 'Laden...',
'settings.libraryNoResults': 'Keine Elemente entsprechen Ihrer Suche.',
'settings.libraryEnabled': 'Aktiviert',
'settings.libraryDisabled': 'Deaktiviert',
'settings.libraryToggleLabel': 'Umschalten',
'notify.successTitle': 'Aufgabe abgeschlossen',
'notify.failureTitle': 'Aufgabe fehlgeschlagen',
'notify.successBody': 'Eine Runde ist abgeschlossen.',

View file

@ -906,6 +906,19 @@ export const en: Dict = {
'settings.notifySoundBuzz': 'Buzz',
'settings.notifySoundTwoToneDown': 'Two-tone down',
'settings.notifySoundThud': 'Thud',
'settings.library': 'Skills & Design Systems',
'settings.libraryHint': 'Browse, preview, and toggle your content library',
'settings.librarySkills': 'Skills',
'settings.libraryDesignSystems': 'Design Systems',
'settings.librarySearch': 'Search...',
'settings.libraryAll': 'All',
'settings.libraryPreview': 'Preview',
'settings.libraryPreviewClose': 'Close',
'settings.libraryLoading': 'Loading...',
'settings.libraryNoResults': 'No items match your search.',
'settings.libraryEnabled': 'Enabled',
'settings.libraryDisabled': 'Disabled',
'settings.libraryToggleLabel': 'Toggle',
'notify.successTitle': 'Task completed',
'notify.failureTitle': 'Task failed',
'notify.successBody': 'A turn has finished.',

View file

@ -830,6 +830,19 @@ export const esES: Dict = {
'settings.notifySoundBuzz': 'Zumbido',
'settings.notifySoundTwoToneDown': 'Dos tonos descendente',
'settings.notifySoundThud': 'Golpe',
'settings.library': 'Habilidades y sistemas de diseño',
'settings.libraryHint': 'Explorar, previsualizar y activar/desactivar tu biblioteca de contenidos',
'settings.librarySkills': 'Habilidades',
'settings.libraryDesignSystems': 'Sistemas de diseño',
'settings.librarySearch': 'Buscar...',
'settings.libraryAll': 'Todo',
'settings.libraryPreview': 'Vista previa',
'settings.libraryPreviewClose': 'Cerrar',
'settings.libraryLoading': 'Cargando...',
'settings.libraryNoResults': 'Ningún elemento coincide con tu búsqueda.',
'settings.libraryEnabled': 'Activado',
'settings.libraryDisabled': 'Desactivado',
'settings.libraryToggleLabel': 'Alternar',
'notify.successTitle': 'Tarea completada',
'notify.failureTitle': 'La tarea falló',
'notify.successBody': 'Un turno ha terminado.',

View file

@ -907,6 +907,19 @@ export const fa: Dict = {
'settings.notifySoundBuzz': 'وزوز',
'settings.notifySoundTwoToneDown': 'دو نوای پایین‌رونده',
'settings.notifySoundThud': 'تالاپ',
'settings.library': 'مهارت‌ها و سیستم‌های طراحی',
'settings.libraryHint': 'مرور، پیش‌نمایش و فعال/غیرفعال‌سازی کتابخانه محتوای شما',
'settings.librarySkills': 'مهارت‌ها',
'settings.libraryDesignSystems': 'سیستم‌های طراحی',
'settings.librarySearch': 'جستجو...',
'settings.libraryAll': 'همه',
'settings.libraryPreview': 'پیش‌نمایش',
'settings.libraryPreviewClose': 'بستن',
'settings.libraryLoading': 'در حال بارگذاری...',
'settings.libraryNoResults': 'هیچ موردی با جستجوی شما مطابقت ندارد.',
'settings.libraryEnabled': 'فعال',
'settings.libraryDisabled': 'غیرفعال',
'settings.libraryToggleLabel': 'تغییر وضعیت',
'notify.successTitle': 'وظیفه تکمیل شد',
'notify.failureTitle': 'وظیفه ناموفق بود',
'notify.successBody': 'یک نوبت به پایان رسید.',

View file

@ -875,6 +875,19 @@ export const fr: Dict = {
'settings.notifySoundBuzz': 'Buzz',
'settings.notifySoundTwoToneDown': 'Bitonale descendante',
'settings.notifySoundThud': 'Sourd',
'settings.library': 'Compétences et systèmes de design',
'settings.libraryHint': 'Parcourir, prévisualiser et activer/désactiver votre bibliothèque de contenus',
'settings.librarySkills': 'Compétences',
'settings.libraryDesignSystems': 'Systèmes de design',
'settings.librarySearch': 'Rechercher...',
'settings.libraryAll': 'Tout',
'settings.libraryPreview': 'Aperçu',
'settings.libraryPreviewClose': 'Fermer',
'settings.libraryLoading': 'Chargement...',
'settings.libraryNoResults': 'Aucun élément ne correspond à votre recherche.',
'settings.libraryEnabled': 'Activé',
'settings.libraryDisabled': 'Désactivé',
'settings.libraryToggleLabel': 'Basculer',
'notify.successTitle': 'Tâche terminée',
'notify.failureTitle': 'Tâche échouée',
'notify.successBody': 'Un tour est terminé.',

View file

@ -885,6 +885,19 @@ export const hu: Dict = {
'settings.notifySoundBuzz': 'Zümmögés',
'settings.notifySoundTwoToneDown': 'Kétszólamú ereszkedő',
'settings.notifySoundThud': 'Tompa puffanás',
'settings.library': 'Készségek és tervezőrendszerek',
'settings.libraryHint': 'Tartalomkönyvtár böngészése, előnézete és be-/kikapcsolása',
'settings.librarySkills': 'Készségek',
'settings.libraryDesignSystems': 'Tervezőrendszerek',
'settings.librarySearch': 'Keresés...',
'settings.libraryAll': 'Összes',
'settings.libraryPreview': 'Előnézet',
'settings.libraryPreviewClose': 'Bezárás',
'settings.libraryLoading': 'Betöltés...',
'settings.libraryNoResults': 'Nincs a keresésnek megfelelő elem.',
'settings.libraryEnabled': 'Engedélyezve',
'settings.libraryDisabled': 'Letiltva',
'settings.libraryToggleLabel': 'Átváltás',
'notify.successTitle': 'Feladat befejezve',
'notify.failureTitle': 'A feladat meghiúsult',
'notify.successBody': 'Egy kör befejeződött.',

View file

@ -828,6 +828,19 @@ export const ja: Dict = {
'settings.notifySoundBuzz': 'ブザー',
'settings.notifySoundTwoToneDown': '下降2音',
'settings.notifySoundThud': 'ドスン',
'settings.library': 'スキルとデザインシステム',
'settings.libraryHint': 'コンテンツライブラリの閲覧、プレビュー、切り替え',
'settings.librarySkills': 'スキル',
'settings.libraryDesignSystems': 'デザインシステム',
'settings.librarySearch': '検索...',
'settings.libraryAll': 'すべて',
'settings.libraryPreview': 'プレビュー',
'settings.libraryPreviewClose': '閉じる',
'settings.libraryLoading': '読み込み中...',
'settings.libraryNoResults': '検索条件に一致する項目がありません。',
'settings.libraryEnabled': '有効',
'settings.libraryDisabled': '無効',
'settings.libraryToggleLabel': '切り替え',
'notify.successTitle': 'タスクが完了しました',
'notify.failureTitle': 'タスクが失敗しました',
'notify.successBody': '1ターンが終了しました。',

View file

@ -875,6 +875,19 @@ export const ko: Dict = {
'settings.notifySoundBuzz': '버즈',
'settings.notifySoundTwoToneDown': '하강 2음',
'settings.notifySoundThud': '쿵',
'settings.library': '스킬 및 디자인 시스템',
'settings.libraryHint': '콘텐츠 라이브러리 찾아보기, 미리보기 및 전환',
'settings.librarySkills': '스킬',
'settings.libraryDesignSystems': '디자인 시스템',
'settings.librarySearch': '검색...',
'settings.libraryAll': '전체',
'settings.libraryPreview': '미리보기',
'settings.libraryPreviewClose': '닫기',
'settings.libraryLoading': '불러오는 중...',
'settings.libraryNoResults': '검색어와 일치하는 항목이 없습니다.',
'settings.libraryEnabled': '활성화됨',
'settings.libraryDisabled': '비활성화됨',
'settings.libraryToggleLabel': '전환',
'notify.successTitle': '작업 완료',
'notify.failureTitle': '작업 실패',
'notify.successBody': '한 턴이 끝났습니다.',

View file

@ -875,6 +875,19 @@ export const pl: Dict = {
'settings.notifySoundBuzz': 'Brzęczenie',
'settings.notifySoundTwoToneDown': 'Dwuton malejący',
'settings.notifySoundThud': 'Łomot',
'settings.library': 'Umiejętności i systemy projektowe',
'settings.libraryHint': 'Przeglądaj, podglądaj i włączaj/wyłączaj bibliotekę treści',
'settings.librarySkills': 'Umiejętności',
'settings.libraryDesignSystems': 'Systemy projektowe',
'settings.librarySearch': 'Szukaj...',
'settings.libraryAll': 'Wszystkie',
'settings.libraryPreview': 'Podgląd',
'settings.libraryPreviewClose': 'Zamknij',
'settings.libraryLoading': 'Ładowanie...',
'settings.libraryNoResults': 'Brak elementów pasujących do wyszukiwania.',
'settings.libraryEnabled': 'Włączone',
'settings.libraryDisabled': 'Wyłączone',
'settings.libraryToggleLabel': 'Przełącz',
'notify.successTitle': 'Zadanie ukończone',
'notify.failureTitle': 'Zadanie nieudane',
'notify.successBody': 'Tura zakończona.',

View file

@ -905,6 +905,19 @@ export const ptBR: Dict = {
'settings.notifySoundBuzz': 'Zumbido',
'settings.notifySoundTwoToneDown': 'Dois tons descendente',
'settings.notifySoundThud': 'Baque',
'settings.library': 'Habilidades e sistemas de design',
'settings.libraryHint': 'Navegar, visualizar e ativar/desativar sua biblioteca de conteúdos',
'settings.librarySkills': 'Habilidades',
'settings.libraryDesignSystems': 'Sistemas de design',
'settings.librarySearch': 'Pesquisar...',
'settings.libraryAll': 'Todos',
'settings.libraryPreview': 'Visualizar',
'settings.libraryPreviewClose': 'Fechar',
'settings.libraryLoading': 'Carregando...',
'settings.libraryNoResults': 'Nenhum item corresponde à sua pesquisa.',
'settings.libraryEnabled': 'Ativado',
'settings.libraryDisabled': 'Desativado',
'settings.libraryToggleLabel': 'Alternar',
'notify.successTitle': 'Tarefa concluída',
'notify.failureTitle': 'Tarefa falhou',
'notify.successBody': 'Uma rodada foi concluída.',

View file

@ -905,6 +905,19 @@ export const ru: Dict = {
'settings.notifySoundBuzz': 'Жужжание',
'settings.notifySoundTwoToneDown': 'Двухтон вниз',
'settings.notifySoundThud': 'Глухой удар',
'settings.library': 'Навыки и системы дизайна',
'settings.libraryHint': 'Просмотр, предпросмотр и управление библиотекой контента',
'settings.librarySkills': 'Навыки',
'settings.libraryDesignSystems': 'Системы дизайна',
'settings.librarySearch': 'Поиск...',
'settings.libraryAll': 'Все',
'settings.libraryPreview': 'Предпросмотр',
'settings.libraryPreviewClose': 'Закрыть',
'settings.libraryLoading': 'Загрузка...',
'settings.libraryNoResults': 'Ничего не найдено по вашему запросу.',
'settings.libraryEnabled': 'Включено',
'settings.libraryDisabled': 'Отключено',
'settings.libraryToggleLabel': 'Переключить',
'notify.successTitle': 'Задача выполнена',
'notify.failureTitle': 'Задача завершилась с ошибкой',
'notify.successBody': 'Ход завершён.',

View file

@ -874,6 +874,19 @@ export const tr: Dict = {
'settings.notifySoundBuzz': 'Vızıltı',
'settings.notifySoundTwoToneDown': 'Alçalan iki ton',
'settings.notifySoundThud': 'Boğuk vuruş',
'settings.library': 'Beceriler ve tasarım sistemleri',
'settings.libraryHint': 'İçerik kitaplığınıza göz atın, önizleyin ve açıp kapatın',
'settings.librarySkills': 'Beceriler',
'settings.libraryDesignSystems': 'Tasarım sistemleri',
'settings.librarySearch': 'Ara...',
'settings.libraryAll': 'Tümü',
'settings.libraryPreview': 'Önizleme',
'settings.libraryPreviewClose': 'Kapat',
'settings.libraryLoading': 'Yükleniyor...',
'settings.libraryNoResults': 'Aramanızla eşleşen öğe bulunamadı.',
'settings.libraryEnabled': 'Etkin',
'settings.libraryDisabled': 'Devre dışı',
'settings.libraryToggleLabel': 'Değiştir',
'notify.successTitle': 'Görev tamamlandı',
'notify.failureTitle': 'Görev başarısız oldu',
'notify.successBody': 'Bir tur tamamlandı.',

View file

@ -906,6 +906,19 @@ export const uk: Dict = {
'settings.notifySoundBuzz': 'Гудіння',
'settings.notifySoundTwoToneDown': 'Два тони вниз',
'settings.notifySoundThud': 'Глухий звук',
'settings.library': 'Навички та системи дизайну',
'settings.libraryHint': 'Перегляд, попередній перегляд та керування бібліотекою вмісту',
'settings.librarySkills': 'Навички',
'settings.libraryDesignSystems': 'Системи дизайну',
'settings.librarySearch': 'Пошук...',
'settings.libraryAll': 'Усі',
'settings.libraryPreview': 'Попередній перегляд',
'settings.libraryPreviewClose': 'Закрити',
'settings.libraryLoading': 'Завантаження...',
'settings.libraryNoResults': 'Нічого не знайдено за вашим запитом.',
'settings.libraryEnabled': 'Увімкнено',
'settings.libraryDisabled': 'Вимкнено',
'settings.libraryToggleLabel': 'Перемкнути',
'notify.successTitle': 'Завдання завершено',
'notify.failureTitle': 'Завдання не вдалося',
'notify.successBody': 'Черга завершена.',

View file

@ -887,6 +887,19 @@ export const zhCN: Dict = {
'settings.notifySoundBuzz': '蜂鸣',
'settings.notifySoundTwoToneDown': '下行双音',
'settings.notifySoundThud': '低响',
'settings.library': '技能与设计系统',
'settings.libraryHint': '浏览、预览和管理您的内容库',
'settings.librarySkills': '技能',
'settings.libraryDesignSystems': '设计系统',
'settings.librarySearch': '搜索...',
'settings.libraryAll': '全部',
'settings.libraryPreview': '预览',
'settings.libraryPreviewClose': '关闭',
'settings.libraryLoading': '加载中...',
'settings.libraryNoResults': '没有匹配的项目。',
'settings.libraryEnabled': '已启用',
'settings.libraryDisabled': '已禁用',
'settings.libraryToggleLabel': '切换',
'notify.successTitle': '任务已完成',
'notify.failureTitle': '任务失败',
'notify.successBody': '一轮回答已经写完。',

View file

@ -887,6 +887,19 @@ export const zhTW: Dict = {
'settings.notifySoundBuzz': '蜂鳴',
'settings.notifySoundTwoToneDown': '下行雙音',
'settings.notifySoundThud': '低響',
'settings.library': '技能與設計系統',
'settings.libraryHint': '瀏覽、預覽和管理您的內容庫',
'settings.librarySkills': '技能',
'settings.libraryDesignSystems': '設計系統',
'settings.librarySearch': '搜尋...',
'settings.libraryAll': '全部',
'settings.libraryPreview': '預覽',
'settings.libraryPreviewClose': '關閉',
'settings.libraryLoading': '載入中...',
'settings.libraryNoResults': '沒有符合的項目。',
'settings.libraryEnabled': '已啟用',
'settings.libraryDisabled': '已停用',
'settings.libraryToggleLabel': '切換',
'notify.successTitle': '任務已完成',
'notify.failureTitle': '任務失敗',
'notify.successBody': '一輪回答已經寫完。',

View file

@ -149,6 +149,19 @@ export interface Dict {
'settings.runtimePackaged': string;
'settings.runtimeDevelopment': string;
'settings.versionUnavailable': string;
'settings.library': string;
'settings.libraryHint': string;
'settings.librarySkills': string;
'settings.libraryDesignSystems': string;
'settings.librarySearch': string;
'settings.libraryAll': string;
'settings.libraryPreview': string;
'settings.libraryPreviewClose': string;
'settings.libraryLoading': string;
'settings.libraryNoResults': string;
'settings.libraryEnabled': string;
'settings.libraryDisabled': string;
'settings.libraryToggleLabel': string;
// Notifications (settings + system notifications)
'settings.notifications': string;

View file

@ -10185,3 +10185,266 @@ body.entry-resizing { cursor: col-resize; user-select: none; }
[dir="rtl"] .meta {
text-align: right;
}
/* Library section (Skills & Design Systems management) */
.library-toolbar {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 16px;
}
.library-search {
width: 100%;
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: 8px;
font-size: 13px;
background: var(--bg);
color: var(--text);
outline: none;
}
.library-search:focus {
border-color: var(--accent);
}
.library-filters {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.library-content {
display: flex;
flex-direction: column;
gap: 20px;
}
.library-group {
display: flex;
flex-direction: column;
gap: 8px;
}
.library-group-title {
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 6px;
}
.library-group-count {
font-weight: 400;
font-size: 11px;
opacity: 0.6;
}
.library-card {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 10px 12px;
border: 1px solid var(--border);
border-radius: 8px;
flex-wrap: wrap;
}
.library-card.disabled {
opacity: 0.45;
}
.library-card-info {
flex: 1;
min-width: 0;
}
.library-card-title-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.library-card-name {
font-size: 14px;
font-weight: 600;
}
.library-card-badge {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 1px 6px;
border-radius: 4px;
background: var(--accent-tint);
color: var(--accent);
font-weight: 500;
}
.library-card-desc {
font-size: 12px;
color: var(--text-muted);
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.library-card-expand {
background: none;
border: none;
cursor: pointer;
padding: 4px;
color: var(--text-muted);
border-radius: 4px;
}
.library-card-expand:hover {
background: var(--border);
}
.library-ds-card {
position: relative;
display: flex;
flex-direction: column;
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
padding: 12px;
gap: 8px;
}
.library-ds-card.disabled {
opacity: 0.45;
}
.library-ds-card-content {
cursor: pointer;
}
.library-ds-swatches {
display: flex;
gap: 4px;
margin-bottom: 6px;
}
.library-ds-swatch {
width: 18px;
height: 18px;
border-radius: 4px;
border: 1px solid rgba(128, 128, 128, 0.2);
}
.library-ds-title {
font-size: 13px;
font-weight: 600;
margin-bottom: 2px;
}
.library-ds-summary {
font-size: 11px;
color: var(--text-muted);
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.library-ds-card .toggle-switch {
align-self: flex-end;
}
.library-preview {
width: 100%;
padding: 12px;
border-top: 1px solid var(--border);
margin-top: 4px;
}
.library-preview-body {
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
max-height: 300px;
overflow-y: auto;
margin: 0;
color: var(--text-muted);
}
.library-empty {
text-align: center;
color: var(--text-muted);
font-size: 13px;
padding: 32px 0;
}
/* Toggle switch */
.toggle-switch {
position: relative;
display: inline-block;
width: 36px;
height: 20px;
flex-shrink: 0;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--border);
border-radius: 20px;
transition: background-color 0.2s;
}
.toggle-slider::before {
content: '';
position: absolute;
height: 14px;
width: 14px;
left: 3px;
bottom: 3px;
background-color: white;
border-radius: 50%;
transition: transform 0.2s;
}
.toggle-switch input:checked + .toggle-slider {
background-color: var(--accent);
}
.toggle-switch input:checked + .toggle-slider::before {
transform: translateX(16px);
}
.toggle-switch-sm {
width: 30px;
height: 17px;
}
.toggle-switch-sm .toggle-slider::before {
height: 11px;
width: 11px;
}
.toggle-switch-sm input:checked + .toggle-slider::before {
transform: translateX(13px);
}

View file

@ -353,6 +353,8 @@ export async function syncConfigToDaemon(config: AppConfig): Promise<void> {
agentModels: config.agentModels,
skillId: config.skillId,
designSystemId: config.designSystemId,
disabledSkills: config.disabledSkills,
disabledDesignSystems: config.disabledDesignSystems,
};
try {
await fetch('/api/app-config', {

View file

@ -269,6 +269,9 @@ export interface AppConfig {
// configs that pre-date the feature land at `undefined`, which the loader
// normalizes to a safe default (everything off).
notifications?: NotificationsConfig;
// IDs of skills/design-systems the user has explicitly disabled.
disabledSkills?: string[];
disabledDesignSystems?: string[];
}
export interface ComposioSettings {

View file

@ -9,6 +9,8 @@ export interface AppConfigPrefs {
agentModels?: Record<string, AgentModelPrefs>;
skillId?: string | null;
designSystemId?: string | null;
disabledSkills?: string[];
disabledDesignSystems?: string[];
}
export interface AppConfigResponse {