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:
parent
5e1fd62ed9
commit
f0850f0785
22 changed files with 1389 additions and 25 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue