feat(electrobun): final 5% — full integration, real data, polish

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.
This commit is contained in:
Hibryda 2026-03-22 02:02:54 +01:00
parent 4826b9dffa
commit 8e756d3523
13 changed files with 1199 additions and 239 deletions

View file

@ -1,4 +1,7 @@
<script lang="ts">
import { onMount } from 'svelte';
import { appRpc } from './rpc.ts';
type TrustLevel = 'human' | 'agent' | 'auto';
interface MemoryFragment {
@ -10,89 +13,133 @@
updatedAt: string;
}
const MEMORIES: MemoryFragment[] = [
{
id: 1,
title: 'Agent Orchestrator — Tech Stack',
body: 'Tauri 2.x + Svelte 5 frontend. Rust backend with rusqlite (WAL mode). Agent sessions via @anthropic-ai/claude-agent-sdk query(). Sidecar uses stdio NDJSON.',
tags: ['agor', 'tech-stack', 'architecture'],
trust: 'human',
updatedAt: '2026-03-20',
},
{
id: 2,
title: 'btmsg SQLite conventions',
body: 'All queries use named column access (row.get("column_name")) — never positional indices. Rust structs use #[serde(rename_all = "camelCase")].',
tags: ['agor', 'database', 'btmsg'],
trust: 'agent',
updatedAt: '2026-03-19',
},
{
id: 3,
title: 'Wake Scheduler — 3 strategies',
body: 'persistent=resume prompt, on-demand=fresh session, smart=threshold-gated on-demand. 6 wake signals from S-3 hybrid tribunal. Pure scorer in wake-scorer.ts (24 tests).',
tags: ['agor', 'wake-scheduler', 'agents'],
trust: 'agent',
updatedAt: '2026-03-18',
},
{
id: 4,
title: 'Svelte 5 runes file extension rule',
body: 'Store files using Svelte 5 runes ($state, $derived) MUST have .svelte.ts extension. Plain .ts compiles but fails at runtime with "rune_outside_svelte".',
tags: ['agor', 'svelte', 'conventions'],
trust: 'auto',
updatedAt: '2026-03-17',
},
];
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">
<span class="memory-count">{MEMORIES.length} fragments</span>
<span class="memory-hint">via Memora</span>
<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">
{#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>
<p class="memory-body">{mem.body}</p>
<div class="memory-footer">
<div class="memory-tags">
{#each mem.tags as tag}
<span class="tag">{tag}</span>
{/each}
{#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>
<span class="memory-date">{mem.updatedAt}</span>
</div>
</article>
{/each}
{#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-tab { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
.memory-header {
display: flex;
align-items: center;
justify-content: space-between;
display: flex; align-items: center; gap: 0.5rem;
padding: 0.375rem 0.625rem;
border-bottom: 1px solid var(--ctp-surface0);
background: var(--ctp-mantle);
@ -100,109 +147,52 @@
font-size: 0.75rem;
}
.memory-count { color: var(--ctp-text); font-weight: 500; }
.memory-hint { color: var(--ctp-overlay0); font-style: italic; }
.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;
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;
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-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;
}
.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;
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); }
.trust-human {
background: color-mix(in srgb, var(--ctp-green) 15%, transparent);
color: var(--ctp-green);
}
.memory-body { margin: 0; font-size: 0.75rem; color: var(--ctp-subtext1); line-height: 1.45; font-family: var(--ui-font-family); }
.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;
}
.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>