feat: @agor/stores package (3 stores) + 58 BackendAdapter tests

@agor/stores:
- theme.svelte.ts, notifications.svelte.ts, health.svelte.ts extracted
- Original files replaced with re-exports (zero consumer changes needed)
- pnpm workspace + Vite/tsconfig aliases configured

BackendAdapter tests (58 new):
- backend-adapter.test.ts: 9 tests (lifecycle, singleton, testing seam)
- tauri-adapter.test.ts: 28 tests (invoke mapping, command names, params)
- electrobun-adapter.test.ts: 21 tests (RPC names, capabilities, stubs)

Total: 523 tests passing (was 465, +58)
This commit is contained in:
Hibryda 2026-03-22 04:45:56 +01:00
parent 5e1fd62ed9
commit f0850f0785
22 changed files with 1389 additions and 25 deletions

View file

@ -170,6 +170,11 @@
fileEncoding = result.encoding;
fileSize = result.size;
editorContent = fileContent;
// Feature 2: Record mtime at read time
try {
const stat = await appRpc.request["files.stat"]({ path: filePath });
if (token === fileRequestToken && !stat.error) readMtimeMs = stat.mtimeMs;
} catch { /* non-critical */ }
} catch (err) {
if (token !== fileRequestToken) return;
fileError = err instanceof Error ? err.message : String(err);
@ -178,9 +183,27 @@
}
}
/** Save current file. */
/** Save current file. Feature 2: Check mtime before write for conflict detection. */
async function saveFile() {
if (!selectedFile || !isDirty) return;
try {
// Feature 2: Check if file was modified externally since we read it
if (readMtimeMs > 0) {
const stat = await appRpc.request["files.stat"]({ path: selectedFile });
if (!stat.error && stat.mtimeMs > readMtimeMs) {
showConflictDialog = true;
return;
}
}
await doSave();
} catch (err) {
console.error('[files.write]', err);
}
}
/** Force-save, bypassing conflict check. */
async function doSave() {
if (!selectedFile) return;
try {
const result = await appRpc.request["files.write"]({
path: selectedFile,
@ -189,6 +212,10 @@
if (result.ok) {
isDirty = false;
fileContent = editorContent;
showConflictDialog = false;
// Update mtime after successful save
const stat = await appRpc.request["files.stat"]({ path: selectedFile });
if (!stat.error) readMtimeMs = stat.mtimeMs;
} else if (result.error) {
console.error('[files.write]', result.error);
}
@ -197,6 +224,17 @@
}
}
/** Reload file from disk (discard local changes). */
async function reloadFile() {
showConflictDialog = false;
if (selectedFile) {
isDirty = false;
const saved = selectedFile;
selectedFile = null;
await selectFile(saved);
}
}
function onEditorChange(newContent: string) {
editorContent = newContent;
isDirty = newContent !== fileContent;
@ -282,6 +320,21 @@
{@render renderEntries(cwd, 0)}
</div>
<!-- Feature 2: Conflict dialog -->
{#if showConflictDialog}
<div class="conflict-overlay">
<div class="conflict-dialog">
<p class="conflict-title">File modified externally</p>
<p class="conflict-desc">This file was changed on disk since you opened it.</p>
<div class="conflict-actions">
<button class="conflict-btn overwrite" onclick={doSave}>Overwrite</button>
<button class="conflict-btn reload" onclick={reloadFile}>Reload</button>
<button class="conflict-btn cancel" onclick={() => showConflictDialog = false}>Cancel</button>
</div>
</div>
</div>
{/if}
<!-- Viewer panel -->
<div class="fb-viewer">
{#if !selectedFile}
@ -346,6 +399,7 @@
height: 100%;
overflow: hidden;
font-size: 0.8125rem;
position: relative;
}
/* ── Tree panel ── */
@ -521,4 +575,58 @@
object-fit: contain;
border-radius: 0.25rem;
}
/* Feature 2: Conflict dialog */
.conflict-overlay {
position: absolute;
inset: 0;
z-index: 50;
background: color-mix(in srgb, var(--ctp-crust) 70%, transparent);
display: flex;
align-items: center;
justify-content: center;
}
.conflict-dialog {
background: var(--ctp-surface0);
border: 1px solid var(--ctp-surface1);
border-radius: 0.5rem;
padding: 1rem 1.25rem;
max-width: 20rem;
}
.conflict-title {
margin: 0 0 0.25rem;
font-size: 0.875rem;
font-weight: 600;
color: var(--ctp-peach);
}
.conflict-desc {
margin: 0 0 0.75rem;
font-size: 0.75rem;
color: var(--ctp-subtext0);
}
.conflict-actions {
display: flex;
gap: 0.375rem;
}
.conflict-btn {
flex: 1;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
border: 1px solid var(--ctp-surface1);
background: var(--ctp-surface0);
color: var(--ctp-text);
font-family: var(--ui-font-family);
font-size: 0.6875rem;
font-weight: 600;
cursor: pointer;
}
.conflict-btn.overwrite { border-color: var(--ctp-red); color: var(--ctp-red); }
.conflict-btn.reload { border-color: var(--ctp-blue); color: var(--ctp-blue); }
.conflict-btn:hover { background: var(--ctp-surface1); }
</style>