feat(ai-missions): inline AiProposalInbox in mission detail (cross-module)

Mission detail now renders all pending proposals for that mission,
across every module, directly above the iteration list. No more jumping
between /todo, /news, /calendar to approve what the agent staged.

AiProposalInbox.module is now optional; when omitted, every card grows
a small lowercase module badge (e.g. "news", "todo") so the user knows
where each proposal will land on approve. The existing per-module
inboxes on /todo, /news, /calendar still work — proposals show in both
places via the same live query, so approving in one auto-clears the
other.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-15 21:33:06 +02:00
parent 988c17a678
commit f06ca2c7c3
2 changed files with 31 additions and 4 deletions

View file

@ -23,13 +23,22 @@
import type { Proposal } from '$lib/data/ai/proposals/types';
interface Props {
/** Filter proposals to tools belonging to this module (e.g. 'todo'). */
module: string;
/** Filter proposals to tools belonging to this module (e.g. 'todo').
* Omit when filtering by mission only — the inbox will then render
* every pending proposal across modules and add a module badge to
* each card so the user knows where it'll land on approve. */
module?: string;
/** Filter to proposals from a specific mission. Combine with `module`
* to scope to that mission's proposals for a single module. */
missionId?: string;
}
let { module }: Props = $props();
let { module, missionId }: Props = $props();
const proposals = $derived(useAiProposals({ status: 'pending', module }));
const proposals = $derived(useAiProposals({ status: 'pending', module, missionId }));
/** Show module badge whenever the inbox is cross-module (i.e. the
* caller didn't pin it to a single module). */
const showModuleBadge = $derived(!module);
let busyId = $state<string | null>(null);
/** Proposal whose reject-feedback textarea is currently open. */
@ -92,6 +101,10 @@
<header class="header">
<Sparkle size={16} weight="fill" />
<span class="label">KI schlägt vor</span>
{#if showModuleBadge && p.intent.kind === 'toolCall'}
{@const mod = getTool(p.intent.toolName)?.module ?? '?'}
<span class="module-badge">{mod}</span>
{/if}
</header>
<p class="intent">{formatIntent(p)}</p>
@ -181,6 +194,16 @@
text-transform: uppercase;
letter-spacing: 0.04em;
}
.module-badge {
margin-left: auto;
padding: 0.0625rem 0.375rem;
border-radius: 0.25rem;
background: color-mix(in oklab, var(--color-primary, #6b5bff) 18%, transparent);
color: color-mix(in oklab, var(--color-primary, #6b5bff) 90%, var(--color-fg, #000));
font-size: 0.6875rem;
letter-spacing: 0.02em;
text-transform: lowercase;
}
.intent {
margin: 0.375rem 0 0;

View file

@ -22,6 +22,7 @@
import MissionInputPicker from '$lib/components/ai/MissionInputPicker.svelte';
import MissionGrantDialog from '$lib/components/ai/MissionGrantDialog.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';
import { isMissionGrantsEnabled } from '$lib/api/config';
import type { Mission, MissionCadence, MissionInputRef } from '$lib/data/ai/missions/types';
@ -392,6 +393,9 @@
<MissionGrantDialog mission={selected} bind:open={grantDialogOpen} />
{/if}
<h3 class="section-title">Vorschläge zur Review</h3>
<AiProposalInbox missionId={selected.id} />
<h3 class="section-title">Iterationen</h3>
{#if selected.iterations.length === 0}
<p class="empty">Noch keine Iteration gelaufen.</p>