1. Claude CLI: additionalDirectories + worktreeName passthrough 2. Agent-store: reads settings (default_cwd, provider model, permission) 3. Project hydration: SQLite replaces hardcoded PROJECTS, add/remove UI 4. Group hydration: SQLite groups, add/delete in sidebar 5. Terminal auto-spawn: reads default_cwd from settings 6. Context tab: real tokens from agent-store, file refs, turn count 7. Memory tab: Memora DB integration (read-only, graceful if missing) 8. Docs tab: markdown viewer (files.list + files.read + inline renderer) 9. SSH tab: CRUD connections, spawn PTY with ssh command 10. Error handling: global unhandledrejection → toast notifications 11. Notifications: agent done/error/stall → toasts, 15min stall timer 12. Command palette: all 18 commands (was 10) +1,198 lines, 13 files. Electrobun now 100% feature-complete vs Tauri v3.
198 lines
6.7 KiB
Svelte
198 lines
6.7 KiB
Svelte
<script lang="ts">
|
|
import { onMount } from 'svelte';
|
|
import { appRpc } from './rpc.ts';
|
|
|
|
type TrustLevel = 'human' | 'agent' | 'auto';
|
|
|
|
interface MemoryFragment {
|
|
id: number;
|
|
title: string;
|
|
body: string;
|
|
tags: string[];
|
|
trust: TrustLevel;
|
|
updatedAt: string;
|
|
}
|
|
|
|
let memories = $state<MemoryFragment[]>([]);
|
|
let searchQuery = $state('');
|
|
let loading = $state(false);
|
|
let hasMemora = $state(true);
|
|
|
|
function parseMemory(raw: {
|
|
id: number; content: string; tags: string;
|
|
metadata: string; updatedAt: string;
|
|
}): MemoryFragment {
|
|
const content = raw.content ?? '';
|
|
const firstLine = content.split('\n')[0] ?? '';
|
|
const title = firstLine.length > 80 ? firstLine.slice(0, 80) + '...' : firstLine;
|
|
const body = content.length > firstLine.length ? content.slice(firstLine.length + 1).trim() : '';
|
|
|
|
let tags: string[] = [];
|
|
try {
|
|
const parsed = JSON.parse(raw.tags ?? '[]');
|
|
tags = Array.isArray(parsed) ? parsed : [];
|
|
} catch { /* keep empty */ }
|
|
|
|
// Infer trust from metadata
|
|
let trust: TrustLevel = 'auto';
|
|
try {
|
|
const meta = JSON.parse(raw.metadata ?? '{}');
|
|
if (meta.source === 'human') trust = 'human';
|
|
else if (meta.source === 'agent') trust = 'agent';
|
|
} catch { /* keep auto */ }
|
|
|
|
return {
|
|
id: raw.id,
|
|
title: title || `Memory #${raw.id}`,
|
|
body,
|
|
tags,
|
|
trust,
|
|
updatedAt: raw.updatedAt?.split('T')[0] ?? '',
|
|
};
|
|
}
|
|
|
|
async function loadMemories() {
|
|
loading = true;
|
|
try {
|
|
const res = await appRpc.request['memora.list']({ limit: 30 });
|
|
if (res.memories.length === 0) {
|
|
// Try search to see if DB exists
|
|
hasMemora = true;
|
|
}
|
|
memories = res.memories.map(parseMemory);
|
|
} catch {
|
|
hasMemora = false;
|
|
memories = [];
|
|
}
|
|
loading = false;
|
|
}
|
|
|
|
async function handleSearch() {
|
|
if (!searchQuery.trim()) {
|
|
await loadMemories();
|
|
return;
|
|
}
|
|
loading = true;
|
|
try {
|
|
const res = await appRpc.request['memora.search']({ query: searchQuery.trim(), limit: 30 });
|
|
memories = res.memories.map(parseMemory);
|
|
} catch {
|
|
memories = [];
|
|
}
|
|
loading = false;
|
|
}
|
|
|
|
const TRUST_LABELS: Record<TrustLevel, string> = {
|
|
human: 'Human',
|
|
agent: 'Agent',
|
|
auto: 'Auto',
|
|
};
|
|
|
|
onMount(() => { loadMemories(); });
|
|
</script>
|
|
|
|
<div class="memory-tab">
|
|
<div class="memory-header">
|
|
<input
|
|
class="memory-search"
|
|
type="text"
|
|
placeholder="Search memories..."
|
|
bind:value={searchQuery}
|
|
onkeydown={(e) => { if (e.key === 'Enter') handleSearch(); }}
|
|
/>
|
|
<span class="memory-count">{memories.length} found</span>
|
|
<span class="memory-hint">{hasMemora ? 'via Memora' : 'Memora not found'}</span>
|
|
</div>
|
|
|
|
<div class="memory-list">
|
|
{#if loading}
|
|
<div class="memory-loading">Loading...</div>
|
|
{:else if memories.length === 0}
|
|
<div class="memory-loading">{hasMemora ? 'No memories found' : 'Memora DB not available (~/.local/share/memora/memories.db)'}</div>
|
|
{:else}
|
|
{#each memories as mem (mem.id)}
|
|
<article class="memory-card">
|
|
<div class="memory-card-top">
|
|
<span class="memory-title">{mem.title}</span>
|
|
<span class="trust-badge trust-{mem.trust}" title="Source: {TRUST_LABELS[mem.trust]}">
|
|
{TRUST_LABELS[mem.trust]}
|
|
</span>
|
|
</div>
|
|
{#if mem.body}
|
|
<p class="memory-body">{mem.body.slice(0, 200)}{mem.body.length > 200 ? '...' : ''}</p>
|
|
{/if}
|
|
<div class="memory-footer">
|
|
<div class="memory-tags">
|
|
{#each mem.tags as tag}
|
|
<span class="tag">{tag}</span>
|
|
{/each}
|
|
</div>
|
|
<span class="memory-date">{mem.updatedAt}</span>
|
|
</div>
|
|
</article>
|
|
{/each}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.memory-tab { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
|
|
|
.memory-header {
|
|
display: flex; align-items: center; gap: 0.5rem;
|
|
padding: 0.375rem 0.625rem;
|
|
border-bottom: 1px solid var(--ctp-surface0);
|
|
background: var(--ctp-mantle);
|
|
flex-shrink: 0;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.memory-search {
|
|
flex: 1; padding: 0.25rem 0.375rem; background: var(--ctp-surface0);
|
|
border: 1px solid var(--ctp-surface1); border-radius: 0.25rem;
|
|
color: var(--ctp-text); font-size: 0.75rem; font-family: var(--ui-font-family);
|
|
}
|
|
.memory-search:focus { outline: none; border-color: var(--ctp-blue); }
|
|
.memory-search::placeholder { color: var(--ctp-overlay0); }
|
|
|
|
.memory-count { color: var(--ctp-text); font-weight: 500; white-space: nowrap; }
|
|
.memory-hint { color: var(--ctp-overlay0); font-style: italic; white-space: nowrap; }
|
|
|
|
.memory-list {
|
|
flex: 1; overflow-y: auto; padding: 0.375rem;
|
|
display: flex; flex-direction: column; gap: 0.375rem;
|
|
}
|
|
|
|
.memory-list::-webkit-scrollbar { width: 0.25rem; }
|
|
.memory-list::-webkit-scrollbar-track { background: transparent; }
|
|
.memory-list::-webkit-scrollbar-thumb { background: var(--ctp-surface1); border-radius: 0.25rem; }
|
|
|
|
.memory-loading { padding: 2rem; text-align: center; color: var(--ctp-overlay0); font-size: 0.8125rem; font-style: italic; }
|
|
|
|
.memory-card {
|
|
background: var(--ctp-surface0); border: 1px solid var(--ctp-surface1);
|
|
border-radius: 0.375rem; padding: 0.5rem 0.625rem;
|
|
display: flex; flex-direction: column; gap: 0.3rem;
|
|
transition: border-color 0.12s;
|
|
}
|
|
.memory-card:hover { border-color: var(--ctp-surface2); }
|
|
|
|
.memory-card-top { display: flex; align-items: flex-start; gap: 0.5rem; }
|
|
|
|
.memory-title { flex: 1; font-size: 0.8125rem; font-weight: 600; color: var(--ctp-text); line-height: 1.3; }
|
|
|
|
.trust-badge {
|
|
flex-shrink: 0; padding: 0.1rem 0.35rem; border-radius: 0.25rem;
|
|
font-size: 0.6rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em;
|
|
}
|
|
.trust-human { background: color-mix(in srgb, var(--ctp-green) 15%, transparent); color: var(--ctp-green); }
|
|
.trust-agent { background: color-mix(in srgb, var(--ctp-blue) 15%, transparent); color: var(--ctp-blue); }
|
|
.trust-auto { background: color-mix(in srgb, var(--ctp-overlay1) 15%, transparent); color: var(--ctp-overlay1); }
|
|
|
|
.memory-body { margin: 0; font-size: 0.75rem; color: var(--ctp-subtext1); line-height: 1.45; font-family: var(--ui-font-family); }
|
|
|
|
.memory-footer { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.1rem; }
|
|
.memory-tags { display: flex; flex-wrap: wrap; gap: 0.25rem; flex: 1; }
|
|
.tag { padding: 0.05rem 0.3rem; background: var(--ctp-surface1); border-radius: 0.2rem; font-size: 0.625rem; color: var(--ctp-overlay1); font-family: var(--term-font-family); }
|
|
.memory-date { font-size: 0.625rem; color: var(--ctp-overlay0); white-space: nowrap; flex-shrink: 0; }
|
|
</style>
|