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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user