From 02fb7de6a8227cccfb255e54a1455b221613531a Mon Sep 17 00:00:00 2001 From: iamdoubz <> Date: Tue, 7 Jul 2026 00:36:20 -0500 Subject: [PATCH] feat(briefs): Feature briefs UI in SummaryPanel (T10.6, M1.5) --- src/lib/views/SummaryPanel.svelte | 296 ++++++++++++++++++++++++++++++ 1 file changed, 296 insertions(+) diff --git a/src/lib/views/SummaryPanel.svelte b/src/lib/views/SummaryPanel.svelte index 9d71653..a62d63f 100644 --- a/src/lib/views/SummaryPanel.svelte +++ b/src/lib/views/SummaryPanel.svelte @@ -9,6 +9,8 @@ errorMessage, type ActionItem, type CalendarEventDetail, + type FeatureBrief, + type FeatureBriefInfo, type LlmStatus, } from "../api"; import { renderMarkdown } from "../markdown"; @@ -22,6 +24,9 @@ Mic2, Bell, UploadCloud, + FileText, + Copy, + Check, } from "@lucide/svelte"; onMount(() => calendar.load()); @@ -250,6 +255,120 @@ generatingTags = false; } } + + // ---- Feature briefs (Phase 10 M1, ADR-0011, FR-MCP-4) ---- + // Agent-ready specs distilled from the meeting via the configured LLM + // provider. `exposed` (MCP scope control, FR-MCP-3) lives only on the list + // row (FeatureBriefInfo) — the full brief the viewer shows doesn't carry it. + let briefs = $state([]); + let selectedBriefId = $state(null); + let selectedBrief = $state(null); + let briefTargetRepo = $state(""); + let creatingBrief = $state(false); + let briefError = $state(null); + let loadingBriefsForId: string | null = null; + + $effect(() => { + const m = meetings.selected; + if (!m) { + briefs = []; + selectedBriefId = null; + selectedBrief = null; + loadingBriefsForId = null; + return; + } + if (m.id === loadingBriefsForId) return; + loadingBriefsForId = m.id; + selectedBriefId = null; + selectedBrief = null; + api + .listFeatureBriefs(m.id) + .then((list) => (briefs = list)) + .catch(() => (briefs = [])); + }); + + async function createBrief() { + const m = meetings.selected; + if (!m) return; + creatingBrief = true; + briefError = null; + try { + const brief = await api.createFeatureBrief(m.id, briefTargetRepo.trim() || undefined); + briefs = [ + { + id: brief.id, + meeting_id: brief.meeting_id, + title: brief.title, + target_repo: brief.target_repo, + exposed: false, + }, + ...briefs, + ]; + selectedBriefId = brief.id; + selectedBrief = brief; + briefTargetRepo = ""; + } catch (e) { + briefError = errorMessage(e); + } finally { + creatingBrief = false; + } + } + + async function openBrief(id: string) { + selectedBriefId = id; + briefError = null; + try { + selectedBrief = await api.getFeatureBrief(id); + } catch (e) { + selectedBrief = null; + briefError = errorMessage(e); + } + } + + async function toggleBriefExposed(brief: FeatureBriefInfo) { + const next = !brief.exposed; + try { + await api.setBriefExposed(brief.id, next); + briefs = briefs.map((b) => (b.id === brief.id ? { ...b, exposed: next } : b)); + } catch (e) { + briefError = errorMessage(e); + } + } + + function briefAsMarkdown(b: FeatureBrief): string { + const criteria = b.acceptance_criteria.length + ? b.acceptance_criteria.map((c) => `- ${c}`).join("\n") + : "- None"; + let md = + `## Title\n${b.title}\n\n` + + `## Problem\n${b.problem}\n\n` + + `## Desired Outcome\n${b.desired_outcome}\n\n` + + `## Acceptance Criteria\n${criteria}`; + if (b.context_excerpts.length) { + md += `\n\n## Context\n${b.context_excerpts.map((e) => `> **${e.speaker}:** ${e.text}`).join("\n\n")}`; + } + return md; + } + + // Brief copy feedback (T10.6, M1.5): a transient checkmark rather than a + // toast — consistent with this panel having no toast system elsewhere. + let copiedFormat = $state<"md" | "json" | null>(null); + let copiedTimer: ReturnType | undefined; + function flashCopied(format: "md" | "json") { + copiedFormat = format; + clearTimeout(copiedTimer); + copiedTimer = setTimeout(() => (copiedFormat = null), 1500); + } + async function copyBriefMarkdown() { + if (!selectedBrief) return; + await navigator.clipboard.writeText(briefAsMarkdown(selectedBrief)); + flashCopied("md"); + } + async function copyBriefJson() { + if (!selectedBrief) return; + await navigator.clipboard.writeText(JSON.stringify(selectedBrief, null, 2)); + flashCopied("json"); + }
@@ -391,6 +510,91 @@

{meetings.summaryError}

{/if} +

+ {#if !meetings.selected} +

Select a meeting to create or view feature briefs.

+ {:else} +

+ Distills this meeting into an agent-ready spec (requires a local LLM provider) — hand it to a + coding agent, or serve it over the MCP server once that's on. +

+
+ + +
+ {#if briefError} +

{briefError}

+ {/if} + + {#if briefs.length > 0} +
    + {#each briefs as b (b.id)} +
  • + + +
  • + {/each} +
+ {/if} + + {#if selectedBrief} +
+
+ + +
+

Problem

+

{selectedBrief.problem}

+

Desired outcome

+

{selectedBrief.desired_outcome}

+

Acceptance criteria

+ {#if selectedBrief.acceptance_criteria.length} +
    + {#each selectedBrief.acceptance_criteria as c, i (i)} +
  • {c}
  • + {/each} +
+ {:else} +

None captured.

+ {/if} + {#if selectedBrief.context_excerpts.length} +

Context

+
    + {#each selectedBrief.context_excerpts as e, i (i)} +
  • {e.speaker}: "{e.text}"
  • + {/each} +
+ {/if} +
+ {/if} + {/if} +

{#if !meetings.selected}

Select a meeting to see its action items.

@@ -761,4 +965,96 @@ .err { cursor: help; } + + /* ---- Feature briefs (Phase 10 M1, ADR-0011) ---- */ + .brief-create { + display: flex; + gap: 0.5rem; + align-items: center; + margin: 0.3rem 0; + } + .brief-create input { + flex: 1; + min-width: 0; + } + ul.briefs { + list-style: none; + padding: 0; + margin: 0.4rem 0; + display: flex; + flex-direction: column; + gap: 0.2rem; + } + ul.briefs li { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + border-radius: var(--radius-sm); + border: 1px solid transparent; + } + ul.briefs li.active { + border-color: var(--accent); + background: var(--bg-hover); + } + .brief-title { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.1rem; + background: none; + border: none; + cursor: pointer; + text-align: left; + padding: 0.35rem 0.4rem; + font-size: 0.85rem; + color: var(--fg); + } + .expose { + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0 0.4rem; + cursor: pointer; + } + .brief-viewer { + margin-top: 0.5rem; + padding: 0.6rem 0.7rem; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + } + .brief-actions { + display: flex; + gap: 0.9rem; + margin-bottom: 0.4rem; + } + .brief-actions .link { + display: inline-flex; + align-items: center; + gap: 0.3rem; + } + .brief-text { + font-size: 0.85rem; + line-height: 1.5; + margin: 0.2rem 0 0.4rem; + white-space: pre-wrap; + } + ul.excerpts { + list-style: none; + padding: 0; + margin: 0.2rem 0; + font-size: 0.82rem; + color: var(--muted); + } + ul.excerpts li { + padding: 0.2rem 0; + font-style: italic; + } + ul.excerpts .speaker { + font-style: normal; + font-weight: 600; + color: var(--fg); + }