Feature chore bug 005 #19

Merged
iamdoubz merged 106 commits from feature_chore_bug_005 into main 2026-07-07 22:02:41 -05:00
Showing only changes of commit 1ade08a60e - Show all commits
+207 -1
View File
@@ -25,6 +25,9 @@
ChevronRight,
RotateCcw,
Info,
Bot,
Copy,
KeyRound,
} from "@lucide/svelte";
import {
OLLAMA_OPTIONS,
@@ -37,7 +40,7 @@
let { onClose }: { onClose: () => void } = $props();
let section = $state<
"recording" | "hardware" | "storage" | "calendar" | "sync" | "ai" | "privacy" | "about"
"recording" | "hardware" | "storage" | "calendar" | "sync" | "ai" | "mcp" | "privacy" | "about"
>("recording");
// ---- About (version + build commit + source) ----
@@ -83,6 +86,47 @@
}
}
// ---- MCP server (Phase 10b, ADR-0011) ----
let mcpTransport = $state<"http" | "stdio">(settings.settings.mcp_transport === "stdio" ? "stdio" : "http");
let mcpPort = $state(settings.settings.mcp_port);
let mcpExpose = $state<"none" | "selected" | "all">(
(settings.settings.mcp_expose as "none" | "selected" | "all") ?? "none",
);
let mcpExposeRecordings = $state(settings.settings.mcp_expose_recordings);
let mcpCopied = $state<"endpoint" | "token" | null>(null);
$effect(() => {
// Re-sync the form when settings (re)load, same pattern as the LLM form above.
mcpTransport = settings.settings.mcp_transport === "stdio" ? "stdio" : "http";
mcpPort = settings.settings.mcp_port;
mcpExpose = (settings.settings.mcp_expose as "none" | "selected" | "all") ?? "none";
mcpExposeRecordings = settings.settings.mcp_expose_recordings;
});
async function toggleMcpEnabled(enabled: boolean) {
await settings.setMcpEnabled(enabled, mcpTransport, mcpPort);
}
async function saveMcpScope() {
await settings.setMcpScope(mcpExpose, mcpExposeRecordings);
}
async function copyToClipboard(text: string, what: "endpoint" | "token") {
try {
await navigator.clipboard.writeText(text);
mcpCopied = what;
setTimeout(() => {
if (mcpCopied === what) mcpCopied = null;
}, 2000);
} catch {
/* clipboard API unavailable — the value is still selectable/copyable by hand */
}
}
function relativeTime(atMs: number): string {
const diffSec = Math.round((Date.now() - atMs) / 1000);
if (diffSec < 5) return "just now";
if (diffSec < 60) return `${diffSec}s ago`;
if (diffSec < 3600) return `${Math.round(diffSec / 60)}m ago`;
if (diffSec < 86400) return `${Math.round(diffSec / 3600)}h ago`;
return new Date(atMs).toLocaleString();
}
// ---- Advanced Ollama configuration (sparse; only overrides are stored) ----
const OPTION_GROUPS: OllamaGroup[] = ["sampling", "repetition", "mirostat", "context"];
type OptVal = number | boolean | string[];
@@ -489,6 +533,9 @@
<button class:active={section === "ai"} onclick={() => (section = "ai")}>
<Sparkles size={14} aria-hidden="true" /> AI
</button>
<button class:active={section === "mcp"} onclick={() => (section = "mcp")}>
<Bot size={14} aria-hidden="true" /> MCP server
</button>
<button class:active={section === "privacy"} onclick={() => (section = "privacy")}>
<ShieldCheck size={14} aria-hidden="true" /> Privacy
</button>
@@ -1191,6 +1238,141 @@
</div>
{/if}
</section>
{:else if section === "mcp"}
<section>
<h3>MCP server</h3>
<div class="banner">
<AlertTriangle size={14} aria-hidden="true" />
<span>
This lets your own coding agent (Claude Code, Codex, Copilot, OpenCode, …) pull meeting
context on your local machine. Once connected, <strong>that agent</strong> may forward what
it reads to its own model provider's cloud — outside WhispAssist's control. WhispAssist
itself never sends this data anywhere; the server only listens on this device
(<code>127.0.0.1</code>) and every read is logged below.
</span>
</div>
<label class="row">
<input
type="checkbox"
checked={settings.mcpStatus?.enabled ?? false}
disabled={settings.mcpSaving}
onchange={(e) => toggleMcpEnabled((e.target as HTMLInputElement).checked)}
/>
<span>Enable the MCP server</span>
</label>
<p class="muted">Off by default. Loopback-only, token-gated — nothing is reachable from the network.</p>
<div class="grid">
<label
>Transport
<select bind:value={mcpTransport} disabled={settings.mcpStatus?.enabled}>
<option value="http">Streamable HTTP</option>
<option value="stdio">stdio (agent spawns a process)</option>
</select>
</label>
{#if mcpTransport === "http"}
<label
>Port<input
type="number"
min="1"
max="65535"
bind:value={mcpPort}
disabled={settings.mcpStatus?.enabled}
/></label
>
{/if}
</div>
{#if settings.mcpStatus?.enabled}
<p class="muted small">
To change transport/port, turn the server off first, then back on.
</p>
{/if}
<h4>Scope</h4>
<div class="grid">
<label
>Expose
<select bind:value={mcpExpose} onchange={saveMcpScope}>
<option value="none">None — nothing is shared</option>
<option value="selected">Selected — only feature briefs you've marked shared</option>
<option value="all">All — meetings, transcripts, action items, and shared briefs</option>
</select>
</label>
</div>
<label class="row small">
<input
type="checkbox"
bind:checked={mcpExposeRecordings}
disabled={mcpExpose === "none"}
onchange={saveMcpScope}
/>
<span
>Also allow meetings with a saved recording (off by default — a recorded meeting's
transcript is withheld even in "All" scope until this is on)</span
>
</label>
{#if settings.mcpLastToken}
<div class="mcp-token" aria-live="polite">
<div class="row">
<KeyRound size={14} aria-hidden="true" />
<strong>New auth token — shown once, copy it now</strong>
</div>
<p class="muted small">
This won't be shown again. It's stored in your OS credential store; if you lose it,
turn the server off and back on to mint a new one.
</p>
<div class="row">
<code>{settings.mcpLastToken}</code>
<button
class="link"
onclick={() => copyToClipboard(settings.mcpLastToken ?? "", "token")}
>
<Copy size={13} aria-hidden="true" />
{mcpCopied === "token" ? "Copied" : "Copy"}
</button>
</div>
</div>
{/if}
{#if settings.mcpStatus?.enabled}
<div class="row">
Endpoint<code>{settings.mcpStatus.endpoint}</code>
<button
class="link"
onclick={() => copyToClipboard(settings.mcpStatus?.endpoint ?? "", "endpoint")}
>
<Copy size={13} aria-hidden="true" />
{mcpCopied === "endpoint" ? "Copied" : "Copy"}
</button>
</div>
<p class="muted small">
Point your agent's MCP client config at this
{settings.mcpStatus.transport === "stdio" ? "command" : "URL"}, with the token above as a
bearer credential.
</p>
{/if}
<h4>Access log</h4>
<p class="muted">Every tool read an agent makes, allowed or denied (FR-MCP-5).</p>
{#if settings.mcpAccessLog.length === 0}
<p class="muted">No agent has read anything yet.</p>
{:else}
<ul class="events">
{#each settings.mcpAccessLog as entry, i (entry.at + "-" + i)}
<li>
<code>{entry.tool}</code>
{#if entry.meeting_id}<span class="host">meeting {entry.meeting_id.slice(0, 8)}</span
>{/if}
{#if entry.client}<span class="badge">{entry.client}</span>{/if}
<span class="host">{relativeTime(entry.at)}</span>
</li>
{/each}
</ul>
<button class="link" onclick={() => settings.loadMcpAccessLog()}>Refresh</button>
{/if}
</section>
{:else if section === "privacy"}
<section>
<h3>Privacy</h3>
@@ -1210,6 +1392,18 @@
{settings.privacy.syncEnabled ? "enabled" : "off"}
</span>
</div>
<div class="row">
MCP server<span class="badge" class:busy={settings.mcpStatus?.enabled}>
{settings.mcpStatus?.enabled ? `on · ${settings.mcpStatus.exposeScope}` : "off"}
</span>
</div>
{#if settings.mcpStatus?.enabled}
<p class="muted confirm">
<Check size={14} aria-hidden="true" />
Inbound on loopback only — it adds nothing to the egress list above. A connected agent
may still forward what it reads to its own model provider; see the MCP server tab.
</p>
{/if}
<h4>Egress allowlist</h4>
{#if settings.privacy.allowlistedHosts.length === 0}
@@ -1660,6 +1854,18 @@
ul.hosts li {
padding: 0.2rem 0;
}
.mcp-token {
margin: 0.6rem 0;
padding: 0.6rem 0.75rem;
border: 1px solid var(--warning, #d97706);
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.mcp-token code {
word-break: break-all;
}
/* ---- AI provider: status + advanced config ---- */
.ghost {