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
226
ui-electrobun/src/mainview/DocsTab.svelte
Normal file
226
ui-electrobun/src/mainview/DocsTab.svelte
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
|
||||
// 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue