feat(electrobun): file management — CodeMirror editor, PDF viewer, CSV table, real file I/O
- CodeEditor: CodeMirror 6 with Catppuccin theme, 15+ languages, Ctrl+S save, dirty tracking, save-on-blur - PdfViewer: pdfjs-dist canvas rendering, zoom 0.5-3x, HiDPI, lazy page load - CsvTable: RFC 4180 parser, delimiter auto-detect, sortable columns, sticky header - FileBrowser: real filesystem via files.list/read/write RPC, lazy dir loading, file type routing (code→editor, pdf→viewer, csv→table, images→display) - 10MB size gate, binary detection, base64 encoding for non-text files
This commit is contained in:
parent
29a3370e79
commit
252fca70df
22 changed files with 8116 additions and 227 deletions
|
|
@ -5,9 +5,12 @@
|
|||
import CommandPalette from './CommandPalette.svelte';
|
||||
import ToastContainer from './ToastContainer.svelte';
|
||||
import NotifDrawer, { type Notification } from './NotifDrawer.svelte';
|
||||
import StatusBar from './StatusBar.svelte';
|
||||
import SearchOverlay from './SearchOverlay.svelte';
|
||||
import { themeStore } from './theme-store.svelte.ts';
|
||||
import { fontStore } from './font-store.svelte.ts';
|
||||
import { keybindingStore } from './keybinding-store.svelte.ts';
|
||||
import { trackProject } from './health-store.svelte.ts';
|
||||
import { appRpc } from './rpc.ts';
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────
|
||||
|
|
@ -137,6 +140,7 @@
|
|||
let settingsOpen = $state(false);
|
||||
let paletteOpen = $state(false);
|
||||
let drawerOpen = $state(false);
|
||||
let searchOpen = $state(false);
|
||||
let sessionStart = $state(Date.now());
|
||||
|
||||
let notifications = $state<Notification[]>([
|
||||
|
|
@ -236,15 +240,8 @@
|
|||
}
|
||||
|
||||
// ── Status bar aggregates ──────────────────────────────────────
|
||||
let runningCount = $derived(PROJECTS.filter(p => p.status === 'running').length);
|
||||
let idleCount = $derived(PROJECTS.filter(p => p.status === 'idle').length);
|
||||
let stalledCount = $derived(PROJECTS.filter(p => p.status === 'stalled').length);
|
||||
let totalCost = $derived(PROJECTS.reduce((s, p) => s + p.costUsd, 0));
|
||||
let totalTokens = $derived(PROJECTS.reduce((s, p) => s + p.tokens, 0));
|
||||
let attentionItems = $derived(PROJECTS.filter(p => p.status === 'stalled' || (p.contextPct ?? 0) >= 75));
|
||||
|
||||
function fmtTokens(n: number): string { return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n); }
|
||||
function fmtCost(n: number): string { return `$${n.toFixed(3)}`; }
|
||||
|
||||
// ── DEBUG: Visual click diagnostics overlay (gated behind DEBUG env) ────
|
||||
const DEBUG_ENABLED = typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('debug');
|
||||
|
|
@ -299,13 +296,29 @@
|
|||
keybindingStore.on('group4', () => setActiveGroup(groups[3]?.id));
|
||||
keybindingStore.on('minimize', () => handleMinimize());
|
||||
|
||||
// Ctrl+Shift+F for search overlay
|
||||
function handleSearchShortcut(e: KeyboardEvent) {
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'F') {
|
||||
e.preventDefault();
|
||||
searchOpen = !searchOpen;
|
||||
}
|
||||
}
|
||||
document.addEventListener('keydown', handleSearchShortcut);
|
||||
|
||||
// Track projects for health monitoring
|
||||
for (const p of PROJECTS) trackProject(p.id);
|
||||
|
||||
const cleanup = keybindingStore.installListener();
|
||||
return cleanup;
|
||||
return () => {
|
||||
cleanup();
|
||||
document.removeEventListener('keydown', handleSearchShortcut);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<SettingsDrawer open={settingsOpen} onClose={() => settingsOpen = false} />
|
||||
<CommandPalette open={paletteOpen} onClose={() => paletteOpen = false} />
|
||||
<SearchOverlay open={searchOpen} onClose={() => searchOpen = false} />
|
||||
<ToastContainer />
|
||||
<NotifDrawer
|
||||
open={drawerOpen}
|
||||
|
|
@ -423,59 +436,14 @@
|
|||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- Status bar -->
|
||||
<footer class="status-bar" role="status" aria-live="polite" aria-label="System status">
|
||||
{#if runningCount > 0}
|
||||
<span class="status-segment">
|
||||
<span class="status-dot-sm green" aria-hidden="true"></span>
|
||||
<span class="status-value">{runningCount}</span>
|
||||
<span>running</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if idleCount > 0}
|
||||
<span class="status-segment">
|
||||
<span class="status-dot-sm gray" aria-hidden="true"></span>
|
||||
<span class="status-value">{idleCount}</span>
|
||||
<span>idle</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if stalledCount > 0}
|
||||
<span class="status-segment">
|
||||
<span class="status-dot-sm orange" aria-hidden="true"></span>
|
||||
<span class="status-value">{stalledCount}</span>
|
||||
<span>stalled</span>
|
||||
</span>
|
||||
{/if}
|
||||
{#if attentionItems.length > 0}
|
||||
<span class="status-segment attn-badge" title="Needs attention: {attentionItems.map(p => p.name).join(', ')}">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" class="attn-icon">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>
|
||||
<span class="status-value">{attentionItems.length}</span>
|
||||
<span>attention</span>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<span class="status-bar-spacer"></span>
|
||||
|
||||
<span class="status-segment" title="Active group">
|
||||
<span class="status-value">{activeGroup?.name}</span>
|
||||
</span>
|
||||
<span class="status-segment" title="Session duration">
|
||||
<span>session</span>
|
||||
<span class="status-value">{sessionDuration}</span>
|
||||
</span>
|
||||
<span class="status-segment" title="Total tokens used">
|
||||
<span>tokens</span>
|
||||
<span class="status-value">{fmtTokens(totalTokens)}</span>
|
||||
</span>
|
||||
<span class="status-segment" title="Total session cost">
|
||||
<span>cost</span>
|
||||
<span class="status-value">{fmtCost(totalCost)}</span>
|
||||
</span>
|
||||
|
||||
<kbd class="palette-hint" title="Open command palette">Ctrl+K</kbd>
|
||||
</footer>
|
||||
<!-- Status bar (health-backed) -->
|
||||
<StatusBar
|
||||
projectCount={filteredProjects.length}
|
||||
{totalTokens}
|
||||
totalCost={totalCost}
|
||||
{sessionDuration}
|
||||
groupName={activeGroup?.name ?? ''}
|
||||
/>
|
||||
|
||||
{#if DEBUG_ENABLED && debugLog.length > 0}
|
||||
<!-- DEBUG: visible click log (enable with ?debug URL param) -->
|
||||
|
|
@ -728,55 +696,5 @@
|
|||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ── Status bar ───────────────────────────────────────────── */
|
||||
.status-bar {
|
||||
height: var(--status-bar-height);
|
||||
background: var(--ctp-crust);
|
||||
border-top: 1px solid var(--ctp-surface0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.875rem;
|
||||
padding: 0 0.625rem;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.6875rem;
|
||||
color: var(--ctp-subtext0);
|
||||
}
|
||||
|
||||
.status-segment {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-dot-sm {
|
||||
width: 0.4375rem;
|
||||
height: 0.4375rem;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-dot-sm.green { background: var(--ctp-green); }
|
||||
.status-dot-sm.gray { background: var(--ctp-overlay0); }
|
||||
.status-dot-sm.orange { background: var(--ctp-peach); }
|
||||
|
||||
.status-value { color: var(--ctp-text); font-weight: 500; }
|
||||
.status-bar-spacer { flex: 1; }
|
||||
|
||||
.attn-badge { color: var(--ctp-yellow); }
|
||||
.attn-icon { width: 0.75rem; height: 0.75rem; stroke: var(--ctp-yellow); }
|
||||
|
||||
.palette-hint {
|
||||
padding: 0.1rem 0.3rem;
|
||||
background: var(--ctp-surface0);
|
||||
border: 1px solid var(--ctp-surface1);
|
||||
border-radius: 0.2rem;
|
||||
font-size: 0.6rem;
|
||||
color: var(--ctp-overlay0);
|
||||
font-family: var(--ui-font-family);
|
||||
cursor: pointer;
|
||||
transition: color 0.1s;
|
||||
}
|
||||
|
||||
.palette-hint:hover { color: var(--ctp-subtext0); }
|
||||
/* Status bar styles are in StatusBar.svelte */
|
||||
</style>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue