diff --git a/apps/mana/apps/web/src/lib/companion/goals/seed.ts b/apps/mana/apps/web/src/lib/companion/goals/seed.ts new file mode 100644 index 000000000..ec3ec13ca --- /dev/null +++ b/apps/mana/apps/web/src/lib/companion/goals/seed.ts @@ -0,0 +1,62 @@ +/** + * Goals module seed handler — applied by the workbench-template + * applicator when a template includes `seeds.goals`. + * + * Idempotency: matched by exact-title. A non-deleted goal with the + * same title is treated as "already seeded". Users can rename the + * goal freely without breaking idempotency because we only check on + * apply, not on update. + */ + +import { db } from '$lib/data/database'; +import { registerSeedHandler, type SeedOutcome } from '$lib/data/ai/agents/seed-registry'; +import { goalStore } from './store'; +import type { LocalGoal, GoalMetric, GoalTarget } from './types'; + +interface GoalSeed { + title: string; + description?: string; + moduleId: string; + metric: GoalMetric; + target: GoalTarget; +} + +registerSeedHandler({ + moduleName: 'goals', + async apply(items) { + const outcomes: SeedOutcome[] = []; + const existing = await db.table('companionGoals').toArray(); + const existingTitles = new Set( + existing.filter((g) => g.status !== 'abandoned').map((g) => g.title) + ); + + for (const item of items) { + const seed = item.data as GoalSeed; + + if (existingTitles.has(seed.title)) { + outcomes.push({ stableId: item.stableId, outcome: 'skipped-exists' }); + continue; + } + + try { + await goalStore.create({ + title: seed.title, + description: seed.description, + moduleId: seed.moduleId, + metric: seed.metric, + target: seed.target, + }); + existingTitles.add(seed.title); + outcomes.push({ stableId: item.stableId, outcome: 'created' }); + } catch (err) { + outcomes.push({ + stableId: item.stableId, + outcome: 'failed', + error: err instanceof Error ? err.message : String(err), + }); + } + } + + return outcomes; + }, +}); diff --git a/apps/mana/apps/web/src/lib/data/ai/missions/setup.ts b/apps/mana/apps/web/src/lib/data/ai/missions/setup.ts index ce34a4066..a055d74ee 100644 --- a/apps/mana/apps/web/src/lib/data/ai/missions/setup.ts +++ b/apps/mana/apps/web/src/lib/data/ai/missions/setup.ts @@ -27,6 +27,8 @@ import { runAgentsBootstrap } from '../agents/bootstrap'; // handler to a rarely-used module slows down the hot path otherwise. // See docs/plans/workbench-templates.md §T1. import '$lib/modules/meditate/seed'; +import '$lib/modules/habits/seed'; +import '$lib/companion/goals/seed'; import type { AiPlanInput, AiPlanOutput } from './planner/types'; /** Default interval between tick scans. One minute is fine for foreground use. */ diff --git a/apps/mana/apps/web/src/lib/modules/habits/seed.ts b/apps/mana/apps/web/src/lib/modules/habits/seed.ts new file mode 100644 index 000000000..672743e5f --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/habits/seed.ts @@ -0,0 +1,60 @@ +/** + * Habits module seed handler — applied by the workbench-template + * applicator when a template includes `seeds.habits`. + * + * Idempotency: matched by exact-title. A non-deleted habit with the + * same title is treated as "already seeded" — user may have already + * applied the template or typed a matching habit themselves. Either + * way we don't want to create a duplicate. + */ + +import { habitTable } from './collections'; +import type { LocalHabit } from './types'; +import { registerSeedHandler, type SeedOutcome } from '$lib/data/ai/agents/seed-registry'; +import { habitsStore } from './stores/habits.svelte'; + +interface HabitSeed { + title: string; + icon: string; + color: string; + targetPerDay?: number | null; + defaultDuration?: number | null; +} + +registerSeedHandler({ + moduleName: 'habits', + async apply(items) { + const outcomes: SeedOutcome[] = []; + const existing = (await habitTable.toArray()) as LocalHabit[]; + const existingTitles = new Set(existing.filter((h) => !h.deletedAt).map((h) => h.title)); + + for (const item of items) { + const seed = item.data as HabitSeed; + + if (existingTitles.has(seed.title)) { + outcomes.push({ stableId: item.stableId, outcome: 'skipped-exists' }); + continue; + } + + try { + await habitsStore.createHabit({ + title: seed.title, + icon: seed.icon, + color: seed.color, + targetPerDay: seed.targetPerDay ?? null, + defaultDuration: seed.defaultDuration ?? null, + }); + existingTitles.add(seed.title); // guard against intra-batch duplicates + outcomes.push({ stableId: item.stableId, outcome: 'created' }); + } catch (err) { + outcomes.push({ + stableId: item.stableId, + outcome: 'failed', + error: err instanceof Error ? err.message : String(err), + }); + } + } + + return outcomes; + }, +}); diff --git a/apps/mana/apps/web/src/routes/(app)/agents/templates/+page.svelte b/apps/mana/apps/web/src/routes/(app)/agents/templates/+page.svelte index 1128cf64c..e8c8b68da 100644 --- a/apps/mana/apps/web/src/routes/(app)/agents/templates/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/agents/templates/+page.svelte @@ -15,6 +15,13 @@ import { applyTemplate } from '$lib/data/ai/agents/apply-template'; let selected = $state(null); + const agentTemplates = ALL_TEMPLATES.filter( + (t) => t.category === 'ai' || t.category === 'delight' + ); + const workbenchTemplates = ALL_TEMPLATES.filter( + (t) => t.category !== 'ai' && t.category !== 'delight' + ); + let applying = $state(false); let result = $state<{ agentName?: string; @@ -93,50 +100,72 @@ - Agent-Templates — Mana + Templates — Mana +{#snippet templateCard(t: AgentTemplate)} + +{/snippet} +
-

Agent-Templates

+

Templates

- Vorgefertigte AI-Agenten, die sofort loslaufen. Jedes Template legt einen Agent, eine passende - Scene und eine Starter-Mission an — die Mission ist standardmäßig pausiert, damit du bewusst - Play drückst. + Vorgefertigte Setups für deinen Workbench. Wähle ein Template und du hast in einem Klick eine + passende Scene, optionale AI-Agenten und erste Daten in den richtigen Modulen.

-
- {#each ALL_TEMPLATES as t (t.id)} - - {/each} -
+
+
+

🤖 Agent-Templates

+

Benannte AI-Personas mit eigener Policy, Memory und Starter-Mission.

+
+
+ {#each agentTemplates as t (t.id)} + {@render templateCard(t)} + {/each} +
+
+ +
+
+

🎨 Workbench-Templates

+

+ Starter-Kits ohne AI — Scene-Layout + vor-gefüllte Habits, Goals und Module-Daten. Du + arbeitest selbst, das Template nimmt dir die Einrichtung ab. +

+
+
+ {#each workbenchTemplates as t (t.id)} + {@render templateCard(t)} + {/each} +
+
{#if selected}
@@ -350,6 +379,22 @@ max-width: 60ch; line-height: 1.5; } + .section { + display: flex; + flex-direction: column; + gap: 0.875rem; + } + .section-head h2 { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + } + .section-head p { + margin: 0.25rem 0 0; + font-size: 0.875rem; + color: hsl(var(--color-muted-foreground)); + max-width: 60ch; + } .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); diff --git a/packages/shared-ai/src/agents/templates/deep-work.ts b/packages/shared-ai/src/agents/templates/deep-work.ts new file mode 100644 index 000000000..9eaf21a7d --- /dev/null +++ b/packages/shared-ai/src/agents/templates/deep-work.ts @@ -0,0 +1,83 @@ +import type { WorkbenchTemplate } from './types'; + +/** + * Deep Work — work-category Workbench-Template. + * + * Ein Starter-Kit für konzentrierte Arbeitsphasen. Scene mit Todo / + * Calendar / Notes / Times + zwei Habits (Deep-Work-Zeit tracken, 1 + * "wichtigste Aufgabe" pro Tag definieren) + ein Wochenziel für + * Fokus-Stunden. + * + * Kein AI-Agent. Das Template gibt die Struktur vor, der User bringt + * die Arbeit. + */ + +export const deepWorkTemplate: WorkbenchTemplate = { + id: 'deep-work', + version: '1', + label: 'Deep Work', + icon: '💻', + tagline: 'Konzentrierter Arbeitsplatz mit Fokus-Tracking', + description: `Ein Starter-Kit für konzentrierte Arbeit. Legt dir eine Scene mit Todo, Kalender, Notes und Time-Tracking an und seed-et zwei Habits plus ein Wochenziel für Deep-Work-Stunden. + +Was drin ist: + +- **Scene**: Todo · Kalender · Notes · Times — alles für fokussierte Sessions nebeneinander. +- **2 Habits**: + - 🎯 1 wichtigste Aufgabe pro Tag + - ⏱ 4h Deep Work pro Tag +- **1 Wochenziel**: "20h Deep Work pro Woche" — zählt abgeschlossene Time-Tracking-Sessions. + +Kein Agent. Das Setup ist da; der Rest ist deine Disziplin.`, + category: 'work', + color: '#1F2937', + scene: { + name: 'Deep Work', + description: 'Fokus, Kalender, Notes, Zeit', + openApps: [ + { appId: 'todo', widthPx: 540 }, + { appId: 'calendar', widthPx: 540 }, + { appId: 'notes', widthPx: 440 }, + { appId: 'times', widthPx: 340 }, + ], + }, + seeds: { + habits: [ + { + stableId: 'template-deepwork:habit:top-task', + data: { + title: '1 wichtigste Aufgabe pro Tag', + icon: '🎯', + color: '#1F2937', + targetPerDay: 1, + defaultDuration: null, + }, + }, + { + stableId: 'template-deepwork:habit:deep-work-hours', + data: { + title: '4h Deep Work pro Tag', + icon: '⏱', + color: '#374151', + targetPerDay: 4, + defaultDuration: 60, + }, + }, + ], + goals: [ + { + stableId: 'template-deepwork:goal:weekly-focus', + data: { + title: '20h Deep Work pro Woche', + description: 'Summiert Time-Tracking-Sessions im Times-Modul.', + moduleId: 'times', + metric: { + source: 'event_count', + eventType: 'TimeSessionCompleted', + }, + target: { value: 20, period: 'week', comparison: 'gte' }, + }, + }, + ], + }, +}; diff --git a/packages/shared-ai/src/agents/templates/fitness.ts b/packages/shared-ai/src/agents/templates/fitness.ts new file mode 100644 index 000000000..590b07f03 --- /dev/null +++ b/packages/shared-ai/src/agents/templates/fitness.ts @@ -0,0 +1,93 @@ +import type { WorkbenchTemplate } from './types'; + +/** + * Fitness — wellness-category Workbench-Template. + * + * Scene für Körper-Arbeit + drei Grund-Habits + ein Wochenziel. Keine + * AI. Der User trackt selbst; das Template nimmt ihm nur die initiale + * Einrichtung ab. + * + * Zielgruppe: jemand der "ich will regelmäßig trainieren" sagt und + * nicht erst 15 Minuten Setup-Zeit investieren will. 1-Klick-Setup. + */ + +export const fitnessTemplate: WorkbenchTemplate = { + id: 'fitness', + version: '1', + label: 'Fitness', + icon: '🏋️', + tagline: 'Workout-Workspace mit Basis-Habits und Wochenziel', + description: `Ein Starter-Kit für regelmäßige Bewegung. Legt dir eine Scene mit Body, Habits, Stretch und Sleep an und seed-et drei Habits und ein Wochenziel — dann kannst du direkt loslegen. + +Was drin ist: + +- **Scene**: Body · Habits · Stretch · Sleep — alles was du zum Tracken brauchst, nebeneinander. +- **3 Habits** mit sinnvollen Default-Icons: + - 🏃 Täglich 30min Bewegung + - 🏋️ 3× Woche Training + - 💧 2L Wasser täglich +- **1 Wochenziel**: "3 Workouts pro Woche" — der Goal-Tracker zählt TaskCompleted-Events aus dem Body-Modul. + +Kein Agent. Keine Automation. Du trainierst, die Workbench zählt.`, + category: 'wellness', + color: '#EF4444', + scene: { + name: 'Fitness', + description: 'Bewegung, Kraft, Schlaf', + openApps: [ + { appId: 'body', widthPx: 540 }, + { appId: 'habits', widthPx: 440 }, + { appId: 'stretch', widthPx: 340 }, + { appId: 'sleep', widthPx: 340 }, + ], + }, + seeds: { + habits: [ + { + stableId: 'template-fitness:habit:daily-movement', + data: { + title: 'Täglich 30min Bewegung', + icon: '🏃', + color: '#EF4444', + targetPerDay: 1, + defaultDuration: 30, + }, + }, + { + stableId: 'template-fitness:habit:weekly-training', + data: { + title: '3× Woche Training', + icon: '🏋️', + color: '#F97316', + targetPerDay: null, + defaultDuration: 60, + }, + }, + { + stableId: 'template-fitness:habit:hydration', + data: { + title: '2L Wasser täglich', + icon: '💧', + color: '#0EA5E9', + targetPerDay: 8, + defaultDuration: null, + }, + }, + ], + goals: [ + { + stableId: 'template-fitness:goal:weekly-workouts', + data: { + title: '3 Workouts pro Woche', + description: 'Zählt abgeschlossene Workouts im Body-Modul.', + moduleId: 'body', + metric: { + source: 'event_count', + eventType: 'TaskCompleted', + }, + target: { value: 3, period: 'week', comparison: 'gte' }, + }, + }, + ], + }, +}; diff --git a/packages/shared-ai/src/agents/templates/index.ts b/packages/shared-ai/src/agents/templates/index.ts index f4a266165..a5205cae1 100644 --- a/packages/shared-ai/src/agents/templates/index.ts +++ b/packages/shared-ai/src/agents/templates/index.ts @@ -11,6 +11,8 @@ import { researchTemplate } from './research'; import { contextTemplate } from './context'; import { todayTemplate } from './today'; import { calmnessTemplate } from './calmness'; +import { fitnessTemplate } from './fitness'; +import { deepWorkTemplate } from './deep-work'; export type { // Generalised names (T1 of workbench-templates plan): @@ -34,9 +36,18 @@ export const ALL_TEMPLATES = [ contextTemplate, todayTemplate, calmnessTemplate, + fitnessTemplate, + deepWorkTemplate, ] as const; -export { researchTemplate, contextTemplate, todayTemplate, calmnessTemplate }; +export { + researchTemplate, + contextTemplate, + todayTemplate, + calmnessTemplate, + fitnessTemplate, + deepWorkTemplate, +}; /** Lookup helper — returns the template matching the given id, or * undefined. Useful for deep-links `/agents/templates?pick=research`. */