From 37e39a5ddb4ebd66432e8b99b008dd1b08b518c3 Mon Sep 17 00:00:00 2001 From: Till JS Date: Wed, 15 Apr 2026 13:23:20 +0200 Subject: [PATCH] feat(ai): AI features as top-level workbench apps (not sub-routes) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverts the previous /companion-carousel misstep. The user's model is that Missions / Workbench / Rituals / Policy / Insights / Health each live as their OWN app in the root `/` workbench scene, alongside todo / calendar / notes / etc. — openable from the normal app picker, freely combinable with any other module card. - New modules, each with a ListView.svelte usable inside AppPage's PageShell (no self-wrapping, no shell-control props): `lib/modules/ai-missions/ListView.svelte` `lib/modules/ai-workbench/ListView.svelte` `lib/modules/ai-rituals/ListView.svelte` `lib/modules/ai-policy/ListView.svelte` `lib/modules/ai-insights/ListView.svelte` `lib/modules/ai-health/ListView.svelte` - Registered in `app-registry/apps.ts`: `ai-missions`, `ai-workbench`, `ai-rituals`, `ai-policy`, `ai-insights`, `ai-health`. Each picks a distinct color + icon. - `/companion/+page.svelte` restored to the simple chat it was before. The companion app (chat) remains as its own registered app — unchanged. - Removed: `lib/modules/companion/pages/` + the `workbench-settings.svelte.ts` store (both were the dead-end PageCarousel approach). User model now: ` / ` (workbench root) → [+ App] → any of the 6 AI apps + any module /companion → full-screen chat (unchanged) /todo, /calendar, … → module-inline ghost inbox stays svelte-check clean; webapp AI tests 71/71 green. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../apps/web/src/lib/app-registry/apps.ts | 68 +++ .../src/lib/modules/ai-health/ListView.svelte | 134 ++++++ .../ListView.svelte} | 191 +++----- .../ListView.svelte} | 437 ++++++++---------- .../ListView.svelte} | 143 ++---- .../ListView.svelte} | 131 ++---- .../ListView.svelte} | 184 +++----- .../modules/companion/pages/AiHomePage.svelte | 231 --------- .../modules/companion/pages/ChatPage.svelte | 224 --------- .../modules/companion/pages/HealthPage.svelte | 179 ------- .../modules/companion/pages/PagePicker.svelte | 138 ------ .../lib/modules/companion/pages/page-meta.ts | 99 ---- .../stores/workbench-settings.svelte.ts | 100 ---- .../src/routes/(app)/companion/+page.svelte | 394 ++++++++++++---- 14 files changed, 924 insertions(+), 1729 deletions(-) create mode 100644 apps/mana/apps/web/src/lib/modules/ai-health/ListView.svelte rename apps/mana/apps/web/src/lib/modules/{companion/pages/InsightsPage.svelte => ai-insights/ListView.svelte} (57%) rename apps/mana/apps/web/src/lib/modules/{companion/pages/MissionsPage.svelte => ai-missions/ListView.svelte} (56%) rename apps/mana/apps/web/src/lib/modules/{companion/pages/PolicyPage.svelte => ai-policy/ListView.svelte} (62%) rename apps/mana/apps/web/src/lib/modules/{companion/pages/RitualsPage.svelte => ai-rituals/ListView.svelte} (56%) rename apps/mana/apps/web/src/lib/modules/{companion/pages/WorkbenchPage.svelte => ai-workbench/ListView.svelte} (59%) delete mode 100644 apps/mana/apps/web/src/lib/modules/companion/pages/AiHomePage.svelte delete mode 100644 apps/mana/apps/web/src/lib/modules/companion/pages/ChatPage.svelte delete mode 100644 apps/mana/apps/web/src/lib/modules/companion/pages/HealthPage.svelte delete mode 100644 apps/mana/apps/web/src/lib/modules/companion/pages/PagePicker.svelte delete mode 100644 apps/mana/apps/web/src/lib/modules/companion/pages/page-meta.ts delete mode 100644 apps/mana/apps/web/src/lib/modules/companion/stores/workbench-settings.svelte.ts diff --git a/apps/mana/apps/web/src/lib/app-registry/apps.ts b/apps/mana/apps/web/src/lib/app-registry/apps.ts index b388e5d62..b54e8effa 100644 --- a/apps/mana/apps/web/src/lib/app-registry/apps.ts +++ b/apps/mana/apps/web/src/lib/app-registry/apps.ts @@ -56,6 +56,9 @@ import { Pulse, Robot, Target, + Flag, + Notebook, + Heartbeat, Smiley, Gear, Palette, @@ -988,6 +991,71 @@ registerApp({ }, }); +// ── AI Workbench apps — each feature is its own top-level app so it +// can live alongside other modules in the user's scene. Colors loosely +// group them: green=missions, amber=audit, pink=rituals, orange=policy, +// violet=insights, emerald=health. + +registerApp({ + id: 'ai-missions', + name: 'AI Missions', + color: '#22C55E', + icon: Flag, + views: { + list: { load: () => import('$lib/modules/ai-missions/ListView.svelte') }, + }, +}); + +registerApp({ + id: 'ai-workbench', + name: 'AI Workbench', + color: '#F59E0B', + icon: Notebook, + views: { + list: { load: () => import('$lib/modules/ai-workbench/ListView.svelte') }, + }, +}); + +registerApp({ + id: 'ai-rituals', + name: 'AI Rituale', + color: '#EC4899', + icon: Lightning, + views: { + list: { load: () => import('$lib/modules/ai-rituals/ListView.svelte') }, + }, +}); + +registerApp({ + id: 'ai-policy', + name: 'AI Policy', + color: '#F97316', + icon: Flag, + views: { + list: { load: () => import('$lib/modules/ai-policy/ListView.svelte') }, + }, +}); + +registerApp({ + id: 'ai-insights', + name: 'AI Insights', + color: '#8B5CF6', + icon: Notebook, + views: { + list: { load: () => import('$lib/modules/ai-insights/ListView.svelte') }, + }, +}); + +registerApp({ + id: 'ai-health', + name: 'AI Health', + color: '#10B981', + icon: Heartbeat, + views: { + list: { load: () => import('$lib/modules/ai-health/ListView.svelte') }, + }, +}); + registerApp({ id: 'goals', name: 'Ziele', diff --git a/apps/mana/apps/web/src/lib/modules/ai-health/ListView.svelte b/apps/mana/apps/web/src/lib/modules/ai-health/ListView.svelte new file mode 100644 index 000000000..da2095895 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/ai-health/ListView.svelte @@ -0,0 +1,134 @@ + + + +
+
+

Foreground-Runner (dieser Tab)

+
+
Status
+
+ {#if isMissionTickRunning()} + ● aktiv · 60 s Interval + {:else} + ● nicht aktiv + {/if} +
+ {#if lastRunStats} +
Letzter Manual-Tick
+
+ {lastRunStats.at} · {lastRunStats.plansProduced} Plans · {lastRunStats.errors} Fehler +
+ {/if} +
+ +
+ +
+

Server-Runner (mana-ai)

+

+ Läuft unabhängig vom Browser. Status + Uptime werden von Prometheus gescrapet und auf dem + Status-Dashboard angezeigt. +

+ + status.mana.how öffnen → + +
+ +
+

Datenlage

+

+ Alles was hier angezeigt wird kommt lokal aus IndexedDB. Kein Server-Call außer beim Planner + selbst. +

+
+
+ + diff --git a/apps/mana/apps/web/src/lib/modules/companion/pages/InsightsPage.svelte b/apps/mana/apps/web/src/lib/modules/ai-insights/ListView.svelte similarity index 57% rename from apps/mana/apps/web/src/lib/modules/companion/pages/InsightsPage.svelte rename to apps/mana/apps/web/src/lib/modules/ai-insights/ListView.svelte index 3998ed07f..615131090 100644 --- a/apps/mana/apps/web/src/lib/modules/companion/pages/InsightsPage.svelte +++ b/apps/mana/apps/web/src/lib/modules/ai-insights/ListView.svelte @@ -1,38 +1,13 @@ - -
-
-

Approval-Rate

- {#if approvalRate === null} -

Noch nicht genug Daten.

- {:else} -
{approvalRate}%
-

über alle Missions + alle Iterationen

- {/if} -
+
+
+

Approval-Rate

+ {#if approvalRate === null} +

Noch nicht genug Daten.

+ {:else} +
{approvalRate}%
+

über alle Missions + alle Iterationen

+ {/if} +
-
-

AI-Events / Tag (14 Tage)

-
- {#each perDay as b (b.day)} -
- 0} - > -
+
+

AI-Events / Tag (14 Tage)

+
+ {#each perDay as b (b.day)} +
+ 0} + > +
+ {/each} +
+
+ +
+

Pro Mission

+ {#if missionStats.length === 0} +

Keine Missions angelegt.

+ {:else} +
    + {#each missionStats as m (m.id)} +
  • + {m.title} + + {m.approved}· + {m.rejected}· + {m.awaiting}· + {m.failed} + +
  • {/each} -
-
+ +

+ approved · + rejected · + awaiting · + failed +

+ {/if} + -
-

Pro Mission

- {#if missionStats.length === 0} -

Keine Missions angelegt.

- {:else} -
    - {#each missionStats as m (m.id)} -
  • - {m.title} - - {m.approved} - · - {m.rejected} - · - {m.awaiting} - · - {m.failed} - -
  • - {/each} -
-

- approved · - rejected · - awaiting · - failed -

- {/if} -
- -
-

Häufigstes Feedback

- {#if topFeedback.length === 0} -

Noch keine Freitext-Reviews.

- {:else} -
    - {#each topFeedback as fb} -
  • - {fb.count}× - "{fb.text}" -
  • - {/each} -
- {/if} -
-
- +
+

Häufigstes Feedback

+ {#if topFeedback.length === 0} +

Noch keine Freitext-Reviews.

+ {:else} +
    + {#each topFeedback as fb} +
  • + {fb.count}× + "{fb.text}" +
  • + {/each} +
+ {/if} +
+
diff --git a/apps/mana/apps/web/src/lib/modules/companion/pages/ChatPage.svelte b/apps/mana/apps/web/src/lib/modules/companion/pages/ChatPage.svelte deleted file mode 100644 index 0cdcc4db1..000000000 --- a/apps/mana/apps/web/src/lib/modules/companion/pages/ChatPage.svelte +++ /dev/null @@ -1,224 +0,0 @@ - - - - -
-
- - -
- - {#if showList} -
- {#each conversations.value as c (c.id)} -
- - -
- {/each} - {#if conversations.value.length === 0} -

Noch keine Gespräche — klick auf „Neu".

- {/if} -
- {:else if activeConversation} - {#key activeConversation.id} - - {/key} - {:else} -
- -

Mana Companion

- -
- {/if} -
-
- - diff --git a/apps/mana/apps/web/src/lib/modules/companion/pages/HealthPage.svelte b/apps/mana/apps/web/src/lib/modules/companion/pages/HealthPage.svelte deleted file mode 100644 index 9cb2b8bad..000000000 --- a/apps/mana/apps/web/src/lib/modules/companion/pages/HealthPage.svelte +++ /dev/null @@ -1,179 +0,0 @@ - - - - -
-
-

Foreground-Runner (dieser Tab)

-
-
Status
-
- {#if isMissionTickRunning()} - ● aktiv · 60 s Interval - {:else} - ● nicht aktiv - {/if} -
- {#if lastRunStats} -
Letzter Manual-Tick
-
- {lastRunStats.at} · {lastRunStats.plansProduced} Plans · - {lastRunStats.errors} Fehler -
- {/if} -
- -
- -
-

Server-Runner (mana-ai)

-

- Läuft unabhängig vom Browser. Status + Uptime werden von Prometheus gescrapet und auf dem - Status-Dashboard angezeigt. -

- - status.mana.how öffnen → - -
- -
-

Datenlage (lokal)

-

- Alles in diesem Carousel kommt aus IndexedDB. Kein Server-Call außer beim Planner selbst. -

-
-
-
- - diff --git a/apps/mana/apps/web/src/lib/modules/companion/pages/PagePicker.svelte b/apps/mana/apps/web/src/lib/modules/companion/pages/PagePicker.svelte deleted file mode 100644 index f6cb91bc8..000000000 --- a/apps/mana/apps/web/src/lib/modules/companion/pages/PagePicker.svelte +++ /dev/null @@ -1,138 +0,0 @@ - - - -
-
- Page öffnen - -
- - {#if availableIds.length === 0} -

Alle Pages sind bereits geöffnet.

- {:else} -
    - {#each availableIds as id (id)} - {@const m = COMPANION_PAGE_META[id]} -
  • - -
  • - {/each} -
- {/if} -
- - diff --git a/apps/mana/apps/web/src/lib/modules/companion/pages/page-meta.ts b/apps/mana/apps/web/src/lib/modules/companion/pages/page-meta.ts deleted file mode 100644 index 43394ab0e..000000000 --- a/apps/mana/apps/web/src/lib/modules/companion/pages/page-meta.ts +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Static metadata for every page the Companion carousel can render. - * - * Matches the shape modules like `/todo` use in their - * `PAGE_META: Record` — adapted here to the - * Companion's own page set. Colors map loosely to the activity type - * (data: muted, creative: primary-ish, health: green, policy: orange). - */ - -import { Sparkle, ChatCircle, Flag, Notebook, Lightning, Heartbeat } from '@mana/shared-icons'; -import type { Component } from 'svelte'; -import type { CompanionPageId } from '../stores/workbench-settings.svelte'; - -export interface CompanionPageMeta { - id: CompanionPageId; - title: string; - shortLabel: string; - color: string; - icon: Component; - description: string; -} - -export const COMPANION_PAGE_META: Record = { - home: { - id: 'home', - title: 'Companion', - shortLabel: 'Home', - color: '#6B5BFF', - icon: Sparkle, - description: 'Übersicht, schnelle Einstiege, letzte Aktivität.', - }, - chat: { - id: 'chat', - title: 'Chat', - shortLabel: 'Chat', - color: '#3B82F6', - icon: ChatCircle, - description: 'Gespräch mit der KI.', - }, - missions: { - id: 'missions', - title: 'Missions', - shortLabel: 'Missions', - color: '#22C55E', - icon: Flag, - description: 'Langlebige Aufträge an die KI anlegen, pausieren, ausführen.', - }, - workbench: { - id: 'workbench', - title: 'Workbench', - shortLabel: 'Workbench', - color: '#F59E0B', - icon: Notebook, - description: 'Timeline aller KI-Aktivität; rückgängig machen.', - }, - rituals: { - id: 'rituals', - title: 'Rituale', - shortLabel: 'Rituale', - color: '#EC4899', - icon: Lightning, - description: 'Geführte Routinen (Morgen, Abend, …).', - }, - policy: { - id: 'policy', - title: 'Policy', - shortLabel: 'Policy', - color: '#F97316', - icon: Flag, - description: 'Pro Tool festlegen: auto / propose / deny.', - }, - insights: { - id: 'insights', - title: 'Insights', - shortLabel: 'Insights', - color: '#8B5CF6', - icon: Notebook, - description: 'Approval-Raten, Feedback-Muster, Stats pro Mission.', - }, - health: { - id: 'health', - title: 'Health', - shortLabel: 'Health', - color: '#10B981', - icon: Heartbeat, - description: 'Runner-Status, letzter Tick, LLM-Backend.', - }, -}; - -export const ALL_COMPANION_PAGE_IDS: readonly CompanionPageId[] = [ - 'home', - 'chat', - 'missions', - 'workbench', - 'rituals', - 'policy', - 'insights', - 'health', -]; diff --git a/apps/mana/apps/web/src/lib/modules/companion/stores/workbench-settings.svelte.ts b/apps/mana/apps/web/src/lib/modules/companion/stores/workbench-settings.svelte.ts deleted file mode 100644 index b2e285f5a..000000000 --- a/apps/mana/apps/web/src/lib/modules/companion/stores/workbench-settings.svelte.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * Companion workbench settings — which pages the user currently has - * open + their widths. Persisted to localStorage so layout survives - * reloads; keep it tiny on purpose (no Dexie round-trip for every - * resize). - */ - -export type CompanionPageId = - | 'home' - | 'chat' - | 'missions' - | 'workbench' - | 'rituals' - | 'policy' - | 'insights' - | 'health'; - -export interface CompanionOpenPage { - id: CompanionPageId; - widthPx: number; - heightPx?: number; - maximized?: boolean; -} - -const STORAGE_KEY = 'companion:openPages'; -const DEFAULT_WIDTH = 520; - -/** Fresh users open the home page with shortcuts to everything else. */ -const DEFAULT_OPEN_PAGES: CompanionOpenPage[] = [{ id: 'home', widthPx: DEFAULT_WIDTH }]; - -function loadOpenPages(): CompanionOpenPage[] { - if (typeof localStorage === 'undefined') return DEFAULT_OPEN_PAGES; - try { - const raw = localStorage.getItem(STORAGE_KEY); - if (!raw) return DEFAULT_OPEN_PAGES; - const parsed = JSON.parse(raw); - if (!Array.isArray(parsed) || parsed.length === 0) return DEFAULT_OPEN_PAGES; - return parsed as CompanionOpenPage[]; - } catch { - return DEFAULT_OPEN_PAGES; - } -} - -function persist(pages: CompanionOpenPage[]): void { - if (typeof localStorage === 'undefined') return; - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(pages)); - } catch { - // ignore quota / private-mode failures — layout falls back to default next load - } -} - -function createStore() { - let openPages = $state(loadOpenPages()); - - return { - get openPages() { - return openPages; - }, - openPage(id: CompanionPageId) { - if (openPages.some((p) => p.id === id)) return; - openPages = [...openPages, { id, widthPx: DEFAULT_WIDTH }]; - persist(openPages); - }, - closePage(id: CompanionPageId) { - openPages = openPages.filter((p) => p.id !== id); - persist(openPages); - }, - resize(id: CompanionPageId, widthPx: number, heightPx?: number) { - openPages = openPages.map((p) => (p.id === id ? { ...p, widthPx, heightPx } : p)); - persist(openPages); - }, - toggleMaximized(id: CompanionPageId) { - openPages = openPages.map((p) => (p.id === id ? { ...p, maximized: !p.maximized } : p)); - persist(openPages); - }, - moveLeft(id: CompanionPageId) { - const idx = openPages.findIndex((p) => p.id === id); - if (idx <= 0) return; - const next = [...openPages]; - [next[idx - 1], next[idx]] = [next[idx], next[idx - 1]]; - openPages = next; - persist(openPages); - }, - moveRight(id: CompanionPageId) { - const idx = openPages.findIndex((p) => p.id === id); - if (idx === -1 || idx >= openPages.length - 1) return; - const next = [...openPages]; - [next[idx], next[idx + 1]] = [next[idx + 1], next[idx]]; - openPages = next; - persist(openPages); - }, - reset() { - openPages = [...DEFAULT_OPEN_PAGES]; - persist(openPages); - }, - }; -} - -export const companionWorkbenchSettings = createStore(); diff --git a/apps/mana/apps/web/src/routes/(app)/companion/+page.svelte b/apps/mana/apps/web/src/routes/(app)/companion/+page.svelte index b7ec0f7dc..5c1b18967 100644 --- a/apps/mana/apps/web/src/routes/(app)/companion/+page.svelte +++ b/apps/mana/apps/web/src/routes/(app)/companion/+page.svelte @@ -1,66 +1,39 @@ - @@ -68,51 +41,268 @@ Companion - Mana - (showPicker = !showPicker)} - addLabel="Page hinzufügen" -> - {#snippet page(p)} - {@const pageId = p.id as CompanionPageId} - {@const idx = openPages.findIndex((o) => o.id === pageId)} - {@const first = idx === 0} - {@const last = idx === openPages.length - 1} - {@const shellProps = { - widthPx: p.widthPx, - maximized: p.maximized, - onClose: () => close(pageId), - onMaximize: () => maximize(pageId), - onResize: (w: number, h?: number) => resize(pageId, w, h), - onMoveLeft: first ? undefined : () => moveLeft(pageId), - onMoveRight: last ? undefined : () => moveRight(pageId), - }} +
+ + + + +
+ {#if activeConversation} + {#key activeConversation.id} + + {/key} + {:else} +
+ +

Mana Companion

+

+ Dein persoenlicher Assistent. Frag nach deinem Tag, lass Tasks erstellen oder Getraenke + loggen. +

+ +
{/if} - {/snippet} +
+
- {#snippet picker()} - p.id)} - onPick={pick} - onClose={() => (showPicker = false)} - /> - {/snippet} -
+