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:
parent
4826b9dffa
commit
8e756d3523
13 changed files with 1199 additions and 239 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue