feat(p8): vue IDE /workspace (arbre + Monaco + diff + terminal)

- backend: garde-fou conflit d'édition par mtime (GET renvoie mtime, PUT refuse en 409 STALE_FILE si baseMtime périmé)
- lib: wt-key (base64url du chemin worktree), diff-parse (parseur pur du diff unifié → hunks)
- composables: useMonaco (import dynamique → chunk isolé), useWorkspaceLayout (largeurs/panneau persistés)
- components/workspace: GitStatusBadge, FileTree+FileTreeNode (lazy fs/list includeFiles), ChangedFilesPanel, DiffViewer (coloration +/-), MonacoEditor (Ctrl+S + bannière conflit reload/overwrite)
- views/WorkspaceView: 3 colonnes desktop redimensionnables + SegmentedControl mobile, header live (GitStatusBadge), watchWorktree → refresh diff/changements, terminal de la session corrélée
- router meta 'ide' + route /workspace/:repoId/:wt ; App.vue plein écran sans AppShell ; WorktreeCard bouton « Ouvrir l'IDE »
- deps: monaco-editor (lazy, vendor-monaco isolé, 0 impact bundle initial) ; vite manualChunks
- i18n EN+FR (workspace/editor/diff/git) ; tests diff-parse + wt-key ; acceptance-p8.mjs (mtime/409 + diff)
This commit is contained in:
2026-06-27 13:47:10 +02:00
parent c8dd539571
commit 75efecf93f
26 changed files with 1142 additions and 6 deletions

View File

@@ -54,6 +54,7 @@ export function registerFileRoutes(app: FastifyInstance, wt: WorktreeManager, db
content: buf.toString('utf-8'),
encoding: 'utf-8',
size: st.size,
mtime: st.mtimeMs,
...(lang ? { language: lang } : {}),
};
return reply.send(res);
@@ -74,10 +75,19 @@ export function registerFileRoutes(app: FastifyInstance, wt: WorktreeManager, db
}
try {
const abs = await wt.assertPathInWorktree(id, body.wt, body.path);
// Garde-fou conflit (P8) : si le client a chargé le fichier (baseMtime) et qu'il a changé
// depuis (ex. Claude a écrit en parallèle), on refuse pour ne pas écraser silencieusement.
if (typeof body.baseMtime === 'number') {
const cur = await stat(abs).catch(() => null);
if (cur && cur.isFile() && cur.mtimeMs !== body.baseMtime) {
return reply.status(409).send({ error: { code: 'STALE_FILE', message: 'File changed on disk since it was loaded' } });
}
}
await mkdir(dirname(abs), { recursive: true }); // dirname reste sous le worktree (abs validé)
await writeFile(abs, body.content, 'utf-8');
const after = await stat(abs);
recordAudit(db, { actor: req.authContext?.tokenId ?? 'unknown', action: 'file.write', resourceId: id, details: { path: body.path } });
const res: WriteFileResponse = { ok: true, size: Buffer.byteLength(body.content, 'utf-8') };
const res: WriteFileResponse = { ok: true, size: Buffer.byteLength(body.content, 'utf-8'), mtime: after.mtimeMs };
return reply.send(res);
} catch (err) {
return sendManagerError(reply, err);