From 51e6a20daf6c6bb10bbb11f89b2159c0b3205045 Mon Sep 17 00:00:00 2001 From: Till JS Date: Wed, 15 Apr 2026 21:56:02 +0200 Subject: [PATCH] feat(ai-agents): Agents UI + Scene binding + Mission picker (Phase 5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Multi-Agent Workbench becomes visible. User can now create named agents with their own persona (name, avatar, role), policy, memory, and budgets; missions are created against a specific agent; scenes can be bound to an agent so the Workbench tab shows who's at home. New UI: - /ai-agents module (ListView with list/create/detail modes, mirroring the ai-missions pattern). Detail view exposes: * Profile (name, avatar emoji, role) + pause/resume/archive/delete * Behavior (systemPrompt + memory — both encrypted at rest per the registry) * Limits (maxConcurrentMissions, maxTokensPerDay) * Policy editor: global default (auto/propose/deny) + per-module overrides for the usual suspects (todo / calendar / notes / …). Three quick-apply templates: Standard, Cautious, Aggressive. * Clear save path with duplicate-name handling from the store. - Registered under the 'ai' category alongside the existing AI apps. New component: AgentPicker — compact + {#if !required} + + {/if} + {#each agents.value as a (a.id)} + + {/each} + + + + diff --git a/apps/mana/apps/web/src/lib/components/workbench/SceneAppBar.svelte b/apps/mana/apps/web/src/lib/components/workbench/SceneAppBar.svelte index 497d6c70d..d92767b59 100644 --- a/apps/mana/apps/web/src/lib/components/workbench/SceneAppBar.svelte +++ b/apps/mana/apps/web/src/lib/components/workbench/SceneAppBar.svelte @@ -7,6 +7,13 @@ import { tick } from 'svelte'; import type { CarouselPage } from '$lib/components/page-carousel/types'; import type { WorkbenchScene } from '$lib/types/workbench-scenes'; + import { useAgents } from '$lib/data/ai/agents/queries'; + + // Resolve each scene's bound agent → avatar + name. Cheap lookup + // since all active agents are already in memory from the live- + // query. No extra Dexie round-trip per render. + const agents = $derived(useAgents({ state: 'active' })); + const agentById = $derived(new Map(agents.value.map((a) => [a.id, a]))); interface Props { scenes: WorkbenchScene[]; @@ -74,6 +81,7 @@ {@const isActive = scene.id === activeSceneId} {#if isActive && pages.length > 0} + {@const boundAgent = scene.viewingAsAgentId ? agentById.get(scene.viewingAsAgentId) : null}
@@ -108,13 +120,18 @@
{:else} + {@const boundAgent = scene.viewingAsAgentId ? agentById.get(scene.viewingAsAgentId) : null} @@ -219,6 +236,10 @@ text-overflow: ellipsis; white-space: nowrap; } + .scene-agent-avatar { + font-size: 0.875rem; + line-height: 1; + } .scene-count { font-size: 0.9375rem; font-weight: 500; diff --git a/apps/mana/apps/web/src/lib/components/workbench/scenes/BindAgentDialog.svelte b/apps/mana/apps/web/src/lib/components/workbench/scenes/BindAgentDialog.svelte new file mode 100644 index 000000000..41b8c7a3e --- /dev/null +++ b/apps/mana/apps/web/src/lib/components/workbench/scenes/BindAgentDialog.svelte @@ -0,0 +1,123 @@ + + + + + +{#if open && scene} + +{/if} + + diff --git a/apps/mana/apps/web/src/lib/data/ai/missions/store.ts b/apps/mana/apps/web/src/lib/data/ai/missions/store.ts index aeb9eaef5..e155be490 100644 --- a/apps/mana/apps/web/src/lib/data/ai/missions/store.ts +++ b/apps/mana/apps/web/src/lib/data/ai/missions/store.ts @@ -47,6 +47,11 @@ export interface CreateMissionInput { objective: string; inputs?: MissionInputRef[]; cadence: MissionCadence; + /** Owning agent id. Optional — when omitted, the mission inherits + * the legacy default agent via the bootstrap migration. Pass an id + * explicitly from the create UI so new missions land on the right + * agent from the first write. */ + agentId?: string; } export async function createMission(input: CreateMissionInput): Promise { @@ -69,6 +74,7 @@ export async function createMission(input: CreateMissionInput): Promise state: 'active', nextRunAt: nextRunForCadence(cadencePlain, new Date()), iterations: [], + agentId: input.agentId, }; await table().add(mission); return mission; @@ -107,6 +113,7 @@ export interface MissionPatch { objective?: string; inputs?: MissionInputRef[]; cadence?: MissionCadence; + agentId?: string; } export async function updateMission(id: string, patch: MissionPatch): Promise { diff --git a/apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte b/apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte new file mode 100644 index 000000000..0e3c522e9 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/ai-agents/ListView.svelte @@ -0,0 +1,693 @@ + + + +{#if mode === 'list'} +
+
+ +
+ {#if agents.value.length === 0} +

+ Noch keine Agenten. Ein Default-Agent „Mana" wird beim ersten Login automatisch angelegt; + für weitere persona-basierte Agenten klicke auf „Neuer Agent". +

+ {:else} +
    + {#each agents.value as a (a.id)} +
  • + +
  • + {/each} +
+ {/if} +
+{:else if mode === 'create'} +
(e.preventDefault(), handleCreate())}> + + + + + {#if formError} +

{formError}

+ {/if} +
+ +
+
+{:else if selected} +
+ +

+ {selected.avatar ?? '🤖'} + {selected.name} + {selected.state} +

+
+ {#if selected.state === 'active'} + + {:else if selected.state === 'paused'} + + {/if} + {#if selected.state !== 'archived'} + + {/if} + +
+ +
+

Profil

+ + + +
+ +
+

Verhalten

+ + +
+ +
+

Grenzen

+ + +
+ +
+ {#if saveError} + {saveError} + {/if} + +
+ +
+

Policy

+

+ Entscheidet pro Modul was der Agent autonom darf. Tool-spezifische Feinheiten kommen später. +

+ +
+ Template übernehmen + +
+ +
+ Global: wenn kein Modul passt +
+ {#each POLICY_CHOICES as c} + + {/each} +
+
+ + + + + + + + + + + {#each POLICY_MODULES as mod} + {@const current = moduleDecisionOrDefault(selected.policy, mod)} + + + + + + {/each} + +
ModulEntscheidung
{mod} + +
+
+
+{/if} + + diff --git a/apps/mana/apps/web/src/lib/modules/ai-missions/ListView.svelte b/apps/mana/apps/web/src/lib/modules/ai-missions/ListView.svelte index 057d38cea..2cc08e7dc 100644 --- a/apps/mana/apps/web/src/lib/modules/ai-missions/ListView.svelte +++ b/apps/mana/apps/web/src/lib/modules/ai-missions/ListView.svelte @@ -21,6 +21,7 @@ import { productionDeps } from '$lib/data/ai/missions/setup'; import MissionInputPicker from '$lib/components/ai/MissionInputPicker.svelte'; import MissionGrantDialog from '$lib/components/ai/MissionGrantDialog.svelte'; + import AgentPicker from '$lib/components/ai/AgentPicker.svelte'; import AiDebugBlock from '$lib/components/ai/AiDebugBlock.svelte'; import AiProposalInbox from '$lib/components/ai/AiProposalInbox.svelte'; import { isAiDebugEnabled, setAiDebugEnabled } from '$lib/data/ai/missions/debug'; @@ -48,6 +49,7 @@ let formIntervalMin = $state(60); let formDailyHour = $state(9); let formInputs = $state([]); + let formAgentId = $state(undefined); let creating = $state(false); function buildCadence(): MissionCadence { @@ -75,11 +77,13 @@ conceptMarkdown: formConcept, inputs: formInputs, cadence: buildCadence(), + agentId: formAgentId, }); formTitle = ''; formObjective = ''; formConcept = ''; formInputs = []; + formAgentId = undefined; formCadenceKind = 'manual'; selectedId = m.id; mode = 'detail'; @@ -255,6 +259,14 @@ rows="5" > +
+ Agent + (formAgentId = id)} + label="Wer führt aus" + /> +
Inputs (Kontext für die KI) diff --git a/apps/mana/apps/web/src/lib/stores/workbench-scenes.svelte.ts b/apps/mana/apps/web/src/lib/stores/workbench-scenes.svelte.ts index 6eaa92e61..581a2fecc 100644 --- a/apps/mana/apps/web/src/lib/stores/workbench-scenes.svelte.ts +++ b/apps/mana/apps/web/src/lib/stores/workbench-scenes.svelte.ts @@ -99,7 +99,10 @@ function pickActiveId(scenes: WorkbenchScene[], current: string | null): string async function patchScene( id: string, patch: Partial< - Pick + Pick< + LocalWorkbenchScene, + 'name' | 'description' | 'openApps' | 'order' | 'wallpaper' | 'viewingAsAgentId' + > > ) { // Strip Svelte 5 $state proxies — IndexedDB's structured clone can't serialize them. @@ -221,6 +224,12 @@ export const workbenchScenesStore = { await patchScene(id, { description }); }, + /** Bind the scene to an Agent (or clear the binding). Purely a UI + * lens — does not affect which data the open apps can see. */ + async setSceneAgent(id: string, agentId: string | undefined) { + await patchScene(id, { viewingAsAgentId: agentId }); + }, + async duplicateScene(id: string) { const src = scenesState.find((s) => s.id === id); if (!src) return; diff --git a/apps/mana/apps/web/src/lib/types/workbench-scenes.ts b/apps/mana/apps/web/src/lib/types/workbench-scenes.ts index 04aaf7d44..34826d657 100644 --- a/apps/mana/apps/web/src/lib/types/workbench-scenes.ts +++ b/apps/mana/apps/web/src/lib/types/workbench-scenes.ts @@ -32,6 +32,16 @@ export interface WorkbenchScene { order: number; /** Per-scene wallpaper override. When set, takes priority over globalSettings.wallpaper. */ wallpaper?: WallpaperConfig; + /** + * Optional Agent this scene is "viewed as" (Multi-Agent Workbench + * Phase 5). Pure UI lens — does NOT restrict which data the open + * apps see. When set, the scene tab shows the agent's avatar, the + * Workbench timeline defaults to this agent's filter, and the + * mission-create flow pre-selects it. Undefined = neutral scene + * (no agent binding); user can pick one explicitly in scene + * settings. See docs/plans/multi-agent-workbench.md §Phase 5d. + */ + viewingAsAgentId?: string; } /** Dexie row shape (adds the BaseRecord audit fields stamped by hooks). */ diff --git a/apps/mana/apps/web/src/routes/(app)/+page.svelte b/apps/mana/apps/web/src/routes/(app)/+page.svelte index e4b7c8284..f19d195df 100644 --- a/apps/mana/apps/web/src/routes/(app)/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/+page.svelte @@ -4,6 +4,7 @@ import SceneAppBar from '$lib/components/workbench/SceneAppBar.svelte'; import SceneHeader from '$lib/components/workbench/scenes/SceneHeader.svelte'; import ConfirmDialog from '$lib/components/workbench/scenes/ConfirmDialog.svelte'; + import BindAgentDialog from '$lib/components/workbench/scenes/BindAgentDialog.svelte'; import { PageCarousel, type CarouselPage } from '$lib/components/page-carousel'; import { getApp, getAppByDragType, isAppAccessible } from '$lib/app-registry'; import { onMount, onDestroy } from 'svelte'; @@ -13,7 +14,7 @@ import { DragPreview } from '@mana/shared-ui/dnd'; import type { DragType } from '@mana/shared-ui/dnd'; import { ContextMenu, type ContextMenuItem } from '@mana/shared-ui'; - import { Pencil, Copy, Trash, Image } from '@mana/shared-icons'; + import { Pencil, Copy, Trash, Image, Sparkle } from '@mana/shared-icons'; import { goto } from '$app/navigation'; import { _, locale } from 'svelte-i18n'; import { buildContextMenuItems, createWorkbenchContextMenu } from '$lib/context-menu'; @@ -273,6 +274,12 @@ icon: Copy, action: () => handleDuplicateScene(scene.id), }, + { + id: 'bind-agent', + label: scene.viewingAsAgentId ? 'Agent-Bindung ändern…' : 'An Agent binden…', + icon: Sparkle, + action: () => handleRequestBindAgent(scene.id), + }, { id: 'wallpaper', label: 'Hintergrund ändern', @@ -295,6 +302,15 @@ // ── Scene CRUD dialogs ────────────────────────────────── let sceneToDelete = $state<{ id: string; name: string } | null>(null); + let sceneToBindAgent = $state(null); + let bindAgentDialogOpen = $state(false); + + function handleRequestBindAgent(id: string) { + const scene = scenes.find((s) => s.id === id); + if (!scene) return; + sceneToBindAgent = scene; + bindAgentDialogOpen = true; + } function handleRequestRename(id: string) { // Unified rename path: scroll the carousel to the scene header @@ -389,6 +405,8 @@ onConfirm={handleConfirmDeleteScene} onCancel={() => (sceneToDelete = null)} /> + +