feat(pro): add Svelte components for commercial phases

BudgetManager (budget+router), ProjectMemory (persistent memory),
CodeIntelligence (symbols+git+branch policy). Updated pro-bridge.ts
with all new IPC functions.
This commit is contained in:
Hibryda 2026-03-17 03:27:40 +01:00
parent 191b869b43
commit be084c8f17
4 changed files with 826 additions and 0 deletions

View file

@ -0,0 +1,238 @@
// SPDX-License-Identifier: LicenseRef-Commercial
<script lang="ts">
import {
proBudgetGet,
proBudgetSet,
proRouterRecommend,
proRouterSetProfile,
proRouterGetProfile,
type BudgetStatus,
type ModelRecommendation,
} from './pro-bridge';
interface Props {
projectId: string;
}
let { projectId }: Props = $props();
let loading = $state(true);
let error = $state<string | null>(null);
let budget = $state<BudgetStatus | null>(null);
let limitInput = $state('');
let settingLimit = $state(false);
let routerProfile = $state<string>('balanced');
let recommendation = $state<ModelRecommendation | null>(null);
const PROFILES = [
{ id: 'cost_saver', label: 'Cost Saver', desc: 'Smaller models, lower cost', icon: '$' },
{ id: 'balanced', label: 'Balanced', desc: 'Smart routing by task', icon: '~' },
{ id: 'quality_first', label: 'Quality First', desc: 'Best models always', icon: '*' },
] as const;
let barColor = $derived(
!budget ? 'var(--ctp-surface1)' :
budget.percent > 90 ? 'var(--ctp-red)' :
budget.percent > 70 ? 'var(--ctp-yellow)' :
'var(--ctp-green)'
);
async function loadAll() {
loading = true;
error = null;
try {
const [b, profile] = await Promise.all([
proBudgetGet(projectId),
proRouterGetProfile(projectId),
]);
budget = b;
routerProfile = profile;
limitInput = String(b.limit);
recommendation = await proRouterRecommend(projectId, 'default', 4000, 'claude');
} catch (e) {
error = e instanceof Error ? e.message : String(e);
} finally {
loading = false;
}
}
$effect(() => {
void projectId;
loadAll();
});
async function setLimit() {
const val = parseInt(limitInput, 10);
if (isNaN(val) || val <= 0) return;
settingLimit = true;
try {
await proBudgetSet(projectId, val);
budget = await proBudgetGet(projectId);
} catch (e) {
error = e instanceof Error ? e.message : String(e);
} finally {
settingLimit = false;
}
}
async function selectProfile(id: string) {
try {
await proRouterSetProfile(projectId, id);
routerProfile = id;
recommendation = await proRouterRecommend(projectId, 'default', 4000, 'claude');
} catch (e) {
error = e instanceof Error ? e.message : String(e);
}
}
function fmtK(n: number): string {
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
return String(n);
}
</script>
<div class="budget-root">
{#if loading}
<div class="state-msg">Loading budget data...</div>
{:else if error}
<div class="state-msg error">{error}</div>
{:else}
<!-- Budget Section -->
<div class="section-title">Budget Governor</div>
{#if budget}
<div class="budget-bar-wrap">
<div class="budget-bar-bg">
<div class="budget-bar-fill" style:width="{Math.min(budget.percent, 100)}%" style:background={barColor}></div>
</div>
<div class="budget-stats">
<span>{fmtK(budget.used)} / {fmtK(budget.limit)} tokens</span>
<span class="budget-pct" style:color={barColor}>{budget.percent.toFixed(1)}%</span>
</div>
<div class="budget-meta">
<span>Remaining: {fmtK(budget.remaining)}</span>
<span>Resets: {budget.resetDate}</span>
</div>
</div>
<div class="limit-row">
<input
class="limit-input"
type="number"
bind:value={limitInput}
placeholder="Monthly token limit"
/>
<button class="btn" onclick={setLimit} disabled={settingLimit}>
{settingLimit ? 'Setting...' : 'Set Limit'}
</button>
</div>
{/if}
<!-- Router Section -->
<div class="section-title" style:margin-top="0.75rem">Smart Model Router</div>
<div class="profile-cards">
{#each PROFILES as p}
<button
class="profile-card"
class:active={routerProfile === p.id}
onclick={() => selectProfile(p.id)}
>
<span class="profile-icon">{p.icon}</span>
<span class="profile-label">{p.label}</span>
<span class="profile-desc">{p.desc}</span>
</button>
{/each}
</div>
{#if recommendation}
<div class="rec-box">
<span class="rec-label">Recommended:</span>
<span class="rec-model">{recommendation.model}</span>
<span class="rec-reason">{recommendation.reason}</span>
<span class="rec-cost">Cost factor: {recommendation.estimatedCostFactor.toFixed(2)}x</span>
</div>
{/if}
{/if}
</div>
<style>
.budget-root {
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
color: var(--ctp-text);
font-family: var(--ui-font-family, system-ui, sans-serif);
font-size: var(--ui-font-size, 0.8125rem);
overflow-y: auto;
height: 100%;
}
.state-msg { padding: 1.5rem; text-align: center; color: var(--ctp-subtext0); }
.state-msg.error { color: var(--ctp-red); }
.section-title {
font-size: 0.75rem; color: var(--ctp-subtext1);
text-transform: uppercase; letter-spacing: 0.04em;
}
.budget-bar-wrap {
background: var(--ctp-surface0); border-radius: 0.375rem;
border: 1px solid var(--ctp-surface1); padding: 0.625rem;
display: flex; flex-direction: column; gap: 0.375rem;
}
.budget-bar-bg {
height: 0.5rem; background: var(--ctp-surface1);
border-radius: 0.25rem; overflow: hidden;
}
.budget-bar-fill {
height: 100%; border-radius: 0.25rem;
transition: width 0.3s ease, background 0.3s ease;
}
.budget-stats {
display: flex; justify-content: space-between;
font-size: 0.75rem; color: var(--ctp-subtext0);
}
.budget-pct { font-weight: 600; }
.budget-meta {
display: flex; justify-content: space-between;
font-size: 0.6875rem; color: var(--ctp-overlay0);
}
.limit-row { display: flex; gap: 0.375rem; }
.limit-input {
flex: 1; padding: 0.3125rem 0.5rem; border-radius: 0.25rem;
border: 1px solid var(--ctp-surface1); background: var(--ctp-surface0);
color: var(--ctp-text); font-size: 0.75rem;
}
.limit-input:focus { outline: none; border-color: var(--ctp-blue); }
.btn {
padding: 0.3125rem 0.75rem; border-radius: 0.25rem;
border: 1px solid var(--ctp-surface1); background: var(--ctp-blue);
color: var(--ctp-base); cursor: pointer; font-size: 0.75rem; font-weight: 500;
}
.btn:hover { opacity: 0.9; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.profile-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.375rem; }
.profile-card {
display: flex; flex-direction: column; align-items: center;
gap: 0.25rem; padding: 0.5rem; border-radius: 0.375rem;
border: 1px solid var(--ctp-surface1); background: var(--ctp-surface0);
cursor: pointer; transition: border-color 0.15s;
}
.profile-card:hover { border-color: var(--ctp-blue); }
.profile-card.active {
border-color: var(--ctp-blue);
background: color-mix(in srgb, var(--ctp-blue) 10%, var(--ctp-surface0));
}
.profile-icon { font-size: 1.25rem; font-weight: 700; color: var(--ctp-blue); }
.profile-label { font-size: 0.75rem; font-weight: 600; color: var(--ctp-text); }
.profile-desc { font-size: 0.625rem; color: var(--ctp-subtext0); text-align: center; }
.rec-box {
display: flex; flex-wrap: wrap; gap: 0.375rem; align-items: baseline;
padding: 0.5rem 0.625rem; background: var(--ctp-surface0);
border-radius: 0.375rem; border: 1px solid var(--ctp-surface1);
font-size: 0.75rem;
}
.rec-label { color: var(--ctp-subtext0); }
.rec-model { font-family: monospace; color: var(--ctp-blue); font-weight: 600; }
.rec-reason { color: var(--ctp-subtext1); flex-basis: 100%; }
.rec-cost { color: var(--ctp-overlay0); font-size: 0.6875rem; }
</style>

View file

@ -0,0 +1,281 @@
// SPDX-License-Identifier: LicenseRef-Commercial
<script lang="ts">
import {
proGitContext,
proGitInject,
proBranchCheck,
proSymbolsScan,
proSymbolsSearch,
type GitContext,
type PolicyDecision,
type Symbol,
} from './pro-bridge';
interface Props {
projectId: string;
projectPath: string;
}
let { projectId, projectPath }: Props = $props();
let loading = $state(true);
let error = $state<string | null>(null);
// Git
let gitCtx = $state<GitContext | null>(null);
let gitInjected = $state<string | null>(null);
let injecting = $state(false);
// Branch policy
let policy = $state<PolicyDecision | null>(null);
// Symbols
let scanResult = $state<{ filesScanned: number; symbolsFound: number; durationMs: number } | null>(null);
let scanning = $state(false);
let symbolQuery = $state('');
let symbols = $state<Symbol[]>([]);
let searchingSymbols = $state(false);
const KIND_COLORS: Record<string, string> = {
function: 'var(--ctp-blue)', class: 'var(--ctp-mauve)', interface: 'var(--ctp-teal)',
type: 'var(--ctp-green)', const: 'var(--ctp-peach)', variable: 'var(--ctp-yellow)',
enum: 'var(--ctp-pink)', method: 'var(--ctp-sapphire)', struct: 'var(--ctp-flamingo)',
};
async function loadAll() {
loading = true;
error = null;
try {
const [g, p] = await Promise.all([
proGitContext(projectPath),
proBranchCheck(projectPath),
]);
gitCtx = g;
policy = p;
} catch (e) {
error = e instanceof Error ? e.message : String(e);
} finally {
loading = false;
}
}
$effect(() => {
void projectPath;
loadAll();
});
async function injectGit() {
injecting = true;
try {
gitInjected = await proGitInject(projectPath, 4000);
} catch (e) {
error = e instanceof Error ? e.message : String(e);
} finally {
injecting = false;
}
}
async function scanSymbols() {
scanning = true;
try {
scanResult = await proSymbolsScan(projectPath);
} catch (e) {
error = e instanceof Error ? e.message : String(e);
} finally {
scanning = false;
}
}
async function searchSymbols() {
if (!symbolQuery.trim()) { symbols = []; return; }
searchingSymbols = true;
try {
symbols = await proSymbolsSearch(projectPath, symbolQuery.trim());
} catch (e) {
error = e instanceof Error ? e.message : String(e);
} finally {
searchingSymbols = false;
}
}
function shortHash(h: string): string { return h.slice(0, 7); }
function shortPath(p: string): string {
const parts = p.split('/');
return parts.length > 3 ? '.../' + parts.slice(-2).join('/') : p;
}
</script>
<div class="ci-root">
{#if loading}
<div class="state-msg">Loading code intelligence...</div>
{:else if error}
<div class="state-msg error">{error}</div>
{:else}
<!-- Git Context -->
<div class="section">
<div class="section-title">Git Context</div>
{#if gitCtx}
<div class="git-header">
<span class="branch-badge">{gitCtx.branch}</span>
{#if gitCtx.hasUnstaged}<span class="unstaged-badge">unstaged changes</span>{/if}
<button class="btn" onclick={injectGit} disabled={injecting}>
{injecting ? 'Injecting...' : 'Inject'}
</button>
</div>
{#if gitCtx.lastCommits.length > 0}
<div class="commit-list">
{#each gitCtx.lastCommits.slice(0, 5) as c}
<div class="commit-row"><span class="commit-hash">{shortHash(c.hash)}</span><span class="commit-msg">{c.message}</span></div>
{/each}
</div>
{/if}
{#if gitCtx.modifiedFiles.length > 0}
<div class="modified-files">
<span class="file-count">{gitCtx.modifiedFiles.length} modified</span>
{#each gitCtx.modifiedFiles.slice(0, 8) as f}<span class="file-path">{shortPath(f)}</span>{/each}
{#if gitCtx.modifiedFiles.length > 8}<span class="file-more">+{gitCtx.modifiedFiles.length - 8} more</span>{/if}
</div>
{/if}
{#if gitInjected}
<pre class="inject-preview">{gitInjected}</pre>
{/if}
{/if}
</div>
<!-- Branch Policy -->
<div class="section">
<div class="section-title">Branch Policy</div>
{#if policy}
<div class="policy-box" class:allowed={policy.allowed} class:blocked={!policy.allowed}>
<span class="policy-status">{policy.allowed ? 'Allowed' : 'Blocked'}</span>
<span class="policy-branch">{policy.branch}</span>
{#if policy.matchedPolicy}
<span class="policy-match">Policy: {policy.matchedPolicy}</span>
{/if}
<span class="policy-reason">{policy.reason}</span>
</div>
{/if}
</div>
<!-- Symbol Graph -->
<div class="section">
<div class="section-title">Symbol Graph</div>
<div class="symbol-toolbar">
<button class="btn secondary" onclick={scanSymbols} disabled={scanning}>
{scanning ? 'Scanning...' : 'Scan Project'}
</button>
{#if scanResult}
<span class="scan-stats">
{scanResult.filesScanned} files, {scanResult.symbolsFound} symbols ({scanResult.durationMs}ms)
</span>
{/if}
</div>
<div class="search-row">
<input
class="search-input"
type="text"
bind:value={symbolQuery}
placeholder="Search symbols..."
onkeydown={(e: KeyboardEvent) => e.key === 'Enter' && searchSymbols()}
/>
<button class="btn" onclick={searchSymbols} disabled={searchingSymbols}>
{searchingSymbols ? '...' : 'Find'}
</button>
</div>
{#if symbols.length > 0}
<div class="symbol-list">
{#each symbols as sym}
<div class="symbol-row">
<span class="kind-badge" style:background={KIND_COLORS[sym.kind] ?? 'var(--ctp-overlay0)'}>{sym.kind}</span>
<span class="symbol-name">{sym.name}</span>
<span class="symbol-loc">{shortPath(sym.filePath)}:{sym.lineNumber}</span>
</div>
{/each}
</div>
{/if}
</div>
{/if}
</div>
<style>
.ci-root {
padding: 0.75rem; display: flex; flex-direction: column; gap: 0.75rem;
color: var(--ctp-text); font-family: var(--ui-font-family, system-ui, sans-serif);
font-size: var(--ui-font-size, 0.8125rem); overflow-y: auto; height: 100%;
}
.state-msg { padding: 1.5rem; text-align: center; color: var(--ctp-subtext0); }
.state-msg.error { color: var(--ctp-red); }
.section { display: flex; flex-direction: column; gap: 0.375rem; }
.section-title { font-size: 0.75rem; color: var(--ctp-subtext1); text-transform: uppercase; letter-spacing: 0.04em; }
.git-header, .symbol-toolbar { display: flex; align-items: center; gap: 0.375rem; flex-wrap: wrap; }
.branch-badge {
padding: 0.125rem 0.5rem; border-radius: 0.25rem; background: var(--ctp-mauve);
color: var(--ctp-base); font-family: monospace; font-size: 0.75rem; font-weight: 600;
}
.unstaged-badge {
padding: 0.0625rem 0.375rem; border-radius: 0.1875rem; background: var(--ctp-yellow);
color: var(--ctp-base); font-size: 0.625rem; font-weight: 600; text-transform: uppercase;
}
.btn {
padding: 0.3125rem 0.75rem; border-radius: 0.25rem; border: 1px solid var(--ctp-surface1);
background: var(--ctp-blue); color: var(--ctp-base); cursor: pointer; font-size: 0.75rem;
font-weight: 500; margin-left: auto;
}
.btn:hover { opacity: 0.9; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn.secondary { background: var(--ctp-surface0); color: var(--ctp-subtext0); margin-left: 0; }
.btn.secondary:hover { background: var(--ctp-surface1); color: var(--ctp-text); }
.commit-list, .symbol-list {
display: flex; flex-direction: column; gap: 0.125rem; padding: 0.375rem;
background: var(--ctp-surface0); border-radius: 0.375rem; border: 1px solid var(--ctp-surface1);
}
.symbol-list { max-height: 15rem; overflow-y: auto; }
.commit-row { display: flex; gap: 0.375rem; align-items: baseline; font-size: 0.75rem; }
.commit-hash { font-family: monospace; color: var(--ctp-peach); font-size: 0.6875rem; flex-shrink: 0; }
.commit-msg { color: var(--ctp-subtext1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.modified-files { display: flex; flex-wrap: wrap; gap: 0.25rem; align-items: center; font-size: 0.6875rem; }
.file-count { color: var(--ctp-yellow); font-weight: 600; margin-right: 0.25rem; }
.file-path {
padding: 0.0625rem 0.3125rem; border-radius: 0.1875rem; background: var(--ctp-surface0);
color: var(--ctp-subtext0); font-family: monospace; font-size: 0.625rem;
}
.file-more { color: var(--ctp-overlay0); }
.inject-preview {
font-family: monospace; font-size: 0.6875rem; color: var(--ctp-subtext0);
background: var(--ctp-surface0); border-radius: 0.375rem; border: 1px solid var(--ctp-teal);
padding: 0.375rem; white-space: pre-wrap; word-break: break-word; max-height: 8rem; overflow-y: auto; margin: 0;
}
.policy-box {
display: flex; flex-wrap: wrap; gap: 0.375rem; align-items: center;
padding: 0.5rem 0.625rem; border-radius: 0.375rem; border: 1px solid var(--ctp-surface1); font-size: 0.75rem;
}
.policy-box.allowed { background: color-mix(in srgb, var(--ctp-green) 8%, var(--ctp-surface0)); border-color: var(--ctp-green); }
.policy-box.blocked { background: color-mix(in srgb, var(--ctp-red) 8%, var(--ctp-surface0)); border-color: var(--ctp-red); }
.policy-status { font-weight: 700; }
.policy-box.allowed .policy-status { color: var(--ctp-green); }
.policy-box.blocked .policy-status { color: var(--ctp-red); }
.policy-branch { font-family: monospace; color: var(--ctp-mauve); }
.policy-match, .policy-reason, .scan-stats { color: var(--ctp-overlay0); font-size: 0.6875rem; }
.policy-reason { flex-basis: 100%; color: var(--ctp-subtext0); }
.search-row { display: flex; gap: 0.375rem; }
.search-input {
flex: 1; padding: 0.3125rem 0.5rem; border-radius: 0.25rem;
border: 1px solid var(--ctp-surface1); background: var(--ctp-surface0); color: var(--ctp-text); font-size: 0.75rem;
}
.search-input:focus { outline: none; border-color: var(--ctp-blue); }
.symbol-row {
display: flex; gap: 0.375rem; align-items: center; font-size: 0.75rem;
padding: 0.1875rem 0.25rem; border-radius: 0.1875rem;
}
.symbol-row:hover { background: var(--ctp-surface1); }
.kind-badge {
padding: 0.0625rem 0.3125rem; border-radius: 0.1875rem; font-size: 0.5625rem;
font-weight: 600; color: var(--ctp-base); text-transform: uppercase; letter-spacing: 0.03em; flex-shrink: 0;
}
.symbol-name { font-family: monospace; color: var(--ctp-text); font-weight: 500; }
.symbol-loc { font-family: monospace; font-size: 0.625rem; color: var(--ctp-overlay0); margin-left: auto; flex-shrink: 0; }
</style>

View file

@ -0,0 +1,267 @@
// SPDX-License-Identifier: LicenseRef-Commercial
<script lang="ts">
import {
proMemoryList,
proMemorySearch,
proMemoryAdd,
proMemoryDelete,
proMemoryInject,
type MemoryFragment,
} from './pro-bridge';
interface Props {
projectId: string;
}
let { projectId }: Props = $props();
let loading = $state(true);
let error = $state<string | null>(null);
let memories = $state<MemoryFragment[]>([]);
let searchQuery = $state('');
let searching = $state(false);
// Add form
let showAddForm = $state(false);
let addContent = $state('');
let addTags = $state('');
let addSource = $state<'human' | 'agent' | 'auto'>('human');
let adding = $state(false);
// Inject
let injectedPreview = $state<string | null>(null);
let injecting = $state(false);
const TRUST_COLORS: Record<string, string> = {
human: 'var(--ctp-green)',
agent: 'var(--ctp-yellow)',
auto: 'var(--ctp-blue)',
};
async function loadMemories() {
loading = true;
error = null;
try {
memories = await proMemoryList(projectId, 50);
} catch (e) {
error = e instanceof Error ? e.message : String(e);
} finally {
loading = false;
}
}
$effect(() => {
void projectId;
loadMemories();
});
async function doSearch() {
if (!searchQuery.trim()) { loadMemories(); return; }
searching = true;
error = null;
try {
memories = await proMemorySearch(projectId, searchQuery.trim());
} catch (e) {
error = e instanceof Error ? e.message : String(e);
} finally {
searching = false;
}
}
async function addMemory() {
if (!addContent.trim()) return;
adding = true;
try {
await proMemoryAdd(projectId, addContent.trim(), addSource, addTags.trim());
addContent = '';
addTags = '';
showAddForm = false;
await loadMemories();
} catch (e) {
error = e instanceof Error ? e.message : String(e);
} finally {
adding = false;
}
}
async function removeMemory(id: number) {
try {
await proMemoryDelete(id);
memories = memories.filter((m) => m.id !== id);
} catch (e) {
error = e instanceof Error ? e.message : String(e);
}
}
async function injectContext() {
injecting = true;
try {
injectedPreview = await proMemoryInject(projectId, 4000);
} catch (e) {
error = e instanceof Error ? e.message : String(e);
} finally {
injecting = false;
}
}
function fmtDate(ts: number): string {
return new Date(ts * 1000).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
}
</script>
<div class="memory-root">
<div class="toolbar">
<div class="search-row">
<input
class="search-input"
type="text"
placeholder="Search memories..."
bind:value={searchQuery}
onkeydown={(e: KeyboardEvent) => e.key === 'Enter' && doSearch()}
/>
<button class="btn" onclick={doSearch} disabled={searching}>
{searching ? '...' : 'Search'}
</button>
</div>
<div class="action-row">
<button class="btn" onclick={() => (showAddForm = !showAddForm)}>
{showAddForm ? 'Cancel' : '+ Add Memory'}
</button>
<button class="btn secondary" onclick={injectContext} disabled={injecting}>
{injecting ? 'Injecting...' : 'Inject Context'}
</button>
<button class="btn secondary" disabled>Extract from Session</button>
</div>
</div>
{#if showAddForm}
<div class="add-form">
<textarea class="add-textarea" bind:value={addContent} placeholder="Memory content..." rows="3"></textarea>
<div class="add-row">
<input class="add-tags" type="text" bind:value={addTags} placeholder="Tags (comma-separated)" />
<select class="add-select" bind:value={addSource}>
<option value="human">Human</option>
<option value="agent">Agent</option>
<option value="auto">Auto</option>
</select>
<button class="btn" onclick={addMemory} disabled={adding || !addContent.trim()}>
{adding ? 'Adding...' : 'Save'}
</button>
</div>
</div>
{/if}
{#if injectedPreview}
<div class="inject-preview">
<div class="inject-header">
<span class="section-title">Injected Context Preview</span>
<button class="close-btn" onclick={() => (injectedPreview = null)}>x</button>
</div>
<pre class="inject-text">{injectedPreview}</pre>
</div>
{/if}
{#if error}
<div class="state-msg error">{error}</div>
{/if}
{#if loading}
<div class="state-msg">Loading memories...</div>
{:else if memories.length === 0}
<div class="state-msg">No memories found.</div>
{:else}
<div class="memory-list">
{#each memories as mem (mem.id)}
<div class="memory-card">
<div class="card-header">
<span class="trust-badge" style:background={TRUST_COLORS[mem.trust] ?? 'var(--ctp-overlay0)'}>{mem.source}</span>
<span class="card-date">{fmtDate(mem.createdAt)}</span>
<button class="del-btn" onclick={() => removeMemory(mem.id)}>x</button>
</div>
<div class="card-content">{mem.content.length > 200 ? mem.content.slice(0, 200) + '...' : mem.content}</div>
<div class="card-footer">
<div class="confidence-bar-bg">
<div class="confidence-bar-fill" style:width="{mem.confidence * 100}%"></div>
</div>
{#if mem.tags}
<div class="card-tags">
{#each mem.tags.split(',') as tag}
<span class="tag">{tag.trim()}</span>
{/each}
</div>
{/if}
</div>
</div>
{/each}
</div>
{/if}
</div>
<style>
.memory-root {
padding: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem;
color: var(--ctp-text); font-family: var(--ui-font-family, system-ui, sans-serif);
font-size: var(--ui-font-size, 0.8125rem); overflow-y: auto; height: 100%;
}
.toolbar { display: flex; flex-direction: column; gap: 0.375rem; }
.search-row { display: flex; gap: 0.375rem; }
.search-input, .add-tags {
flex: 1; padding: 0.3125rem 0.5rem; border-radius: 0.25rem;
border: 1px solid var(--ctp-surface1); background: var(--ctp-surface0);
color: var(--ctp-text); font-size: 0.75rem;
}
.search-input:focus, .add-tags:focus, .add-textarea:focus { outline: none; border-color: var(--ctp-blue); }
.action-row { display: flex; gap: 0.375rem; flex-wrap: wrap; }
.btn {
padding: 0.3125rem 0.75rem; border-radius: 0.25rem; border: 1px solid var(--ctp-surface1);
background: var(--ctp-blue); color: var(--ctp-base); cursor: pointer; font-size: 0.75rem; font-weight: 500;
}
.btn:hover { opacity: 0.9; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn.secondary { background: var(--ctp-surface0); color: var(--ctp-subtext0); }
.btn.secondary:hover { background: var(--ctp-surface1); color: var(--ctp-text); }
.add-form {
display: flex; flex-direction: column; gap: 0.375rem; padding: 0.5rem;
background: var(--ctp-surface0); border-radius: 0.375rem; border: 1px solid var(--ctp-surface1);
}
.add-textarea {
padding: 0.375rem; border-radius: 0.25rem; border: 1px solid var(--ctp-surface1);
background: var(--ctp-mantle); color: var(--ctp-text); font-size: 0.75rem; resize: vertical; font-family: inherit;
}
.add-row { display: flex; gap: 0.375rem; }
.add-select {
padding: 0.25rem 0.375rem; border-radius: 0.25rem;
border: 1px solid var(--ctp-surface1); background: var(--ctp-mantle); color: var(--ctp-text); font-size: 0.75rem;
}
.inject-preview { padding: 0.5rem; background: var(--ctp-surface0); border-radius: 0.375rem; border: 1px solid var(--ctp-teal); }
.inject-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.375rem; }
.section-title { font-size: 0.75rem; color: var(--ctp-subtext1); text-transform: uppercase; letter-spacing: 0.04em; }
.close-btn { background: none; border: none; color: var(--ctp-overlay0); cursor: pointer; font-size: 0.75rem; padding: 0.125rem 0.25rem; }
.close-btn:hover { color: var(--ctp-text); }
.inject-text {
font-family: monospace; font-size: 0.6875rem; color: var(--ctp-subtext0);
white-space: pre-wrap; word-break: break-word; max-height: 10rem; overflow-y: auto; margin: 0;
}
.state-msg { padding: 1.5rem; text-align: center; color: var(--ctp-subtext0); }
.state-msg.error { color: var(--ctp-red); }
.memory-list { display: flex; flex-direction: column; gap: 0.375rem; }
.memory-card {
padding: 0.5rem 0.625rem; background: var(--ctp-surface0); border-radius: 0.375rem;
border: 1px solid var(--ctp-surface1); display: flex; flex-direction: column; gap: 0.375rem;
}
.memory-card:hover { border-color: var(--ctp-surface2); }
.card-header { display: flex; align-items: center; gap: 0.375rem; }
.trust-badge, .tag, .kind-badge {
padding: 0.0625rem 0.375rem; border-radius: 0.1875rem; font-size: 0.625rem; font-weight: 600;
}
.trust-badge { color: var(--ctp-base); text-transform: uppercase; letter-spacing: 0.03em; }
.card-date { font-size: 0.6875rem; color: var(--ctp-overlay0); margin-left: auto; }
.del-btn { background: none; border: none; color: var(--ctp-overlay0); cursor: pointer; font-size: 0.75rem; padding: 0 0.25rem; }
.del-btn:hover { color: var(--ctp-red); }
.card-content { font-size: 0.75rem; color: var(--ctp-subtext1); line-height: 1.4; }
.card-footer { display: flex; flex-direction: column; gap: 0.25rem; }
.confidence-bar-bg { height: 0.1875rem; background: var(--ctp-surface1); border-radius: 0.125rem; overflow: hidden; }
.confidence-bar-fill { height: 100%; background: var(--ctp-teal); border-radius: 0.125rem; }
.card-tags { display: flex; flex-wrap: wrap; gap: 0.25rem; }
.tag { background: var(--ctp-surface1); color: var(--ctp-subtext0); font-weight: 400; }
</style>

View file

@ -150,3 +150,43 @@ export const proMarketplaceUpdate = (pluginId: string) =>
export const proStatus = () =>
invoke<string>('plugin:agor-pro|pro_status');
// --- Budget Governor ---
export interface BudgetStatus { limit: number; used: number; remaining: number; percent: number; resetDate: string; }
export interface BudgetDecision { allowed: boolean; reason: string; remaining: number; }
export const proBudgetSet = (projectId: string, monthlyLimitTokens: number) => invoke<void>('plugin:agor-pro|pro_budget_set', { projectId, monthlyLimitTokens });
export const proBudgetGet = (projectId: string) => invoke<BudgetStatus>('plugin:agor-pro|pro_budget_get', { projectId });
export const proBudgetCheck = (projectId: string, estimatedTokens: number) => invoke<BudgetDecision>('plugin:agor-pro|pro_budget_check', { projectId, estimatedTokens });
export const proBudgetLogUsage = (projectId: string, sessionId: string, tokensUsed: number) => invoke<void>('plugin:agor-pro|pro_budget_log_usage', { projectId, sessionId, tokensUsed });
export const proBudgetList = () => invoke<Array<BudgetStatus & { projectId: string }>>('plugin:agor-pro|pro_budget_list');
// --- Smart Model Router ---
export interface ModelRecommendation { model: string; reason: string; estimatedCostFactor: number; }
export const proRouterRecommend = (projectId: string, role: string, promptLength: number, provider: string) => invoke<ModelRecommendation>('plugin:agor-pro|pro_router_recommend', { projectId, role, promptLength, provider });
export const proRouterSetProfile = (projectId: string, profile: string) => invoke<void>('plugin:agor-pro|pro_router_set_profile', { projectId, profile });
export const proRouterGetProfile = (projectId: string) => invoke<string>('plugin:agor-pro|pro_router_get_profile', { projectId });
// --- Persistent Memory ---
export interface MemoryFragment { id: number; projectId: string; content: string; source: string; trust: string; confidence: number; createdAt: number; ttlDays: number; tags: string; }
export const proMemoryAdd = (projectId: string, content: string, source: string, tags: string) => invoke<number>('plugin:agor-pro|pro_memory_add', { projectId, content, source, tags });
export const proMemoryList = (projectId: string, limit: number) => invoke<MemoryFragment[]>('plugin:agor-pro|pro_memory_list', { projectId, limit });
export const proMemorySearch = (projectId: string, query: string) => invoke<MemoryFragment[]>('plugin:agor-pro|pro_memory_search', { projectId, query });
export const proMemoryDelete = (id: number) => invoke<void>('plugin:agor-pro|pro_memory_delete', { id });
export const proMemoryInject = (projectId: string, maxTokens: number) => invoke<string>('plugin:agor-pro|pro_memory_inject', { projectId, maxTokens });
// --- Git Context ---
export interface GitContext { branch: string; lastCommits: Array<{hash: string; message: string; author: string; timestamp: number}>; modifiedFiles: string[]; hasUnstaged: boolean; }
export interface PolicyDecision { allowed: boolean; branch: string; matchedPolicy: string | null; reason: string; }
export const proGitContext = (projectPath: string) => invoke<GitContext>('plugin:agor-pro|pro_git_context', { projectPath });
export const proGitInject = (projectPath: string, maxTokens: number) => invoke<string>('plugin:agor-pro|pro_git_inject', { projectPath, maxTokens });
export const proBranchCheck = (projectPath: string) => invoke<PolicyDecision>('plugin:agor-pro|pro_branch_check', { projectPath });
// --- Symbols ---
export interface Symbol { name: string; kind: string; filePath: string; lineNumber: number; }
export const proSymbolsScan = (projectPath: string) => invoke<{filesScanned: number; symbolsFound: number; durationMs: number}>('plugin:agor-pro|pro_symbols_scan', { projectPath });
export const proSymbolsSearch = (projectPath: string, query: string) => invoke<Symbol[]>('plugin:agor-pro|pro_symbols_search', { projectPath, query });