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

@ -0,0 +1,226 @@
<script lang="ts">
import { onMount } from 'svelte';
import { appRpc } from './rpc.ts';
interface Props {
cwd: string;
}
let { cwd }: Props = $props();
interface DocFile {
name: string;
path: string;
}
let files = $state<DocFile[]>([]);
let selectedFile = $state<DocFile | null>(null);
let content = $state('');
let renderedHtml = $state('');
let loading = $state(false);
function expandHome(p: string): string {
if (p.startsWith('~/')) return p.replace('~', '/home/' + (typeof process !== 'undefined' ? process.env.USER : 'user'));
return p;
}
async function loadFiles() {
try {
const resolved = expandHome(cwd);
const res = await appRpc.request['files.list']({ path: resolved });
if (res.error) return;
files = res.entries
.filter((e: { name: string; type: string }) => e.type === 'file' && e.name.endsWith('.md'))
.map((e: { name: string }) => ({ name: e.name, path: `${resolved}/${e.name}` }));
} catch (err) {
console.error('[DocsTab] list error:', err);
}
}
async function selectFile(file: DocFile) {
selectedFile = file;
loading = true;
try {
const res = await appRpc.request['files.read']({ path: file.path });
if (res.error || !res.content) {
content = '';
renderedHtml = `<p class="doc-error">${res.error ?? 'Empty file'}</p>`;
} else {
content = res.content;
renderedHtml = renderMarkdown(content);
}
} catch (err) {
console.error('[DocsTab] read error:', err);
renderedHtml = '<p class="doc-error">Failed to read file</p>';
}
loading = false;
}
/** Simple markdown-to-HTML (no external dep). Handles headers, code blocks, bold, italic, links, lists. */
function renderMarkdown(md: string): string {
let html = md
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
// Code blocks
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, lang, code) =>
`<pre class="doc-code"><code class="lang-${lang}">${code.trim()}</code></pre>`
);
// Inline code
html = html.replace(/`([^`]+)`/g, '<code class="doc-inline-code">$1</code>');
// Headers
html = html.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
html = html.replace(/^### (.+)$/gm, '<h3>$1</h3>');
html = html.replace(/^## (.+)$/gm, '<h2>$1</h2>');
html = html.replace(/^# (.+)$/gm, '<h1>$1</h1>');
// Bold + italic
html = html.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
// Links
html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
// Unordered lists
html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
html = html.replace(/(<li>[\s\S]*?<\/li>)/g, '<ul>$1</ul>');
html = html.replace(/<\/ul>\s*<ul>/g, '');
// Paragraphs (lines not already wrapped)
html = html.replace(/^(?!<[huplo])((?!\s*$).+)$/gm, '<p>$1</p>');
return html;
}
onMount(() => { loadFiles(); });
</script>
<div class="docs-tab">
<div class="docs-sidebar">
<div class="docs-header">Markdown files</div>
{#if files.length === 0}
<div class="docs-empty">No .md files in project</div>
{:else}
{#each files as file}
<button
class="docs-file"
class:active={selectedFile?.path === file.path}
onclick={() => selectFile(file)}
>{file.name}</button>
{/each}
{/if}
</div>
<div class="docs-content">
{#if loading}
<div class="docs-placeholder">Loading...</div>
{:else if !selectedFile}
<div class="docs-placeholder">Select a file to view</div>
{:else}
<div class="docs-rendered">
{@html renderedHtml}
</div>
{/if}
</div>
</div>
<style>
.docs-tab { display: flex; height: 100%; overflow: hidden; }
.docs-sidebar {
width: 10rem;
flex-shrink: 0;
background: var(--ctp-mantle);
border-right: 1px solid var(--ctp-surface0);
display: flex;
flex-direction: column;
overflow-y: auto;
}
.docs-header {
padding: 0.375rem 0.5rem;
font-size: 0.625rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--ctp-overlay0);
border-bottom: 1px solid var(--ctp-surface0);
}
.docs-empty {
padding: 1rem 0.5rem;
font-size: 0.75rem;
color: var(--ctp-overlay0);
font-style: italic;
text-align: center;
}
.docs-file {
display: block;
width: 100%;
text-align: left;
padding: 0.375rem 0.5rem;
background: transparent;
border: none;
color: var(--ctp-subtext0);
font-size: 0.75rem;
cursor: pointer;
font-family: var(--term-font-family);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: background 0.1s, color 0.1s;
}
.docs-file:hover { background: var(--ctp-surface0); color: var(--ctp-text); }
.docs-file.active { background: color-mix(in srgb, var(--ctp-blue) 12%, transparent); color: var(--ctp-blue); }
.docs-content { flex: 1; overflow-y: auto; padding: 0.75rem; min-width: 0; }
.docs-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--ctp-overlay0);
font-size: 0.8125rem;
font-style: italic;
}
.docs-rendered {
font-family: var(--ui-font-family);
font-size: 0.8125rem;
color: var(--ctp-text);
line-height: 1.6;
}
.docs-rendered :global(h1) { font-size: 1.25rem; color: var(--ctp-text); margin: 0.75rem 0 0.375rem; }
.docs-rendered :global(h2) { font-size: 1.0625rem; color: var(--ctp-text); margin: 0.625rem 0 0.3rem; }
.docs-rendered :global(h3) { font-size: 0.9375rem; color: var(--ctp-text); margin: 0.5rem 0 0.25rem; }
.docs-rendered :global(p) { margin: 0.25rem 0; }
.docs-rendered :global(a) { color: var(--ctp-blue); text-decoration: none; }
.docs-rendered :global(a:hover) { text-decoration: underline; }
.docs-rendered :global(strong) { color: var(--ctp-text); }
.docs-rendered :global(ul) { padding-left: 1.25rem; margin: 0.25rem 0; }
.docs-rendered :global(li) { margin: 0.125rem 0; }
.docs-rendered :global(.doc-code) {
background: var(--ctp-surface0);
border: 1px solid var(--ctp-surface1);
border-radius: 0.25rem;
padding: 0.5rem 0.625rem;
overflow-x: auto;
font-family: var(--term-font-family);
font-size: 0.75rem;
line-height: 1.5;
margin: 0.375rem 0;
}
.docs-rendered :global(.doc-inline-code) {
background: var(--ctp-surface0);
padding: 0.1rem 0.25rem;
border-radius: 0.2rem;
font-family: var(--term-font-family);
font-size: 0.75rem;
}
.docs-rendered :global(.doc-error) { color: var(--ctp-red); font-style: italic; }
</style>