feat(conflicts): add file overlap conflict detection (S-1 Phase 1)

Detects when 2+ agent sessions write the same file within a project.
New conflicts.svelte.ts store, shared tool-files.ts utility, dispatcher
integration, health attention scoring (SCORE_FILE_CONFLICT=70), and UI
indicators in ProjectHeader + StatusBar. 170/170 tests pass.
This commit is contained in:
Hibryda 2026-03-11 00:12:10 +01:00
parent 8e00e0ef8c
commit 82fb618c76
12 changed files with 483 additions and 37 deletions

View file

@ -0,0 +1,53 @@
// Extracts file paths from agent tool_call inputs
// Used by ContextTab (all file ops) and conflicts store (write ops only)
import type { ToolCallContent } from '../adapters/sdk-messages';
export interface ToolFileRef {
path: string;
op: 'read' | 'write' | 'glob' | 'grep' | 'bash';
}
/** Extract file paths referenced by a tool call */
export function extractFilePaths(tc: ToolCallContent): ToolFileRef[] {
const results: ToolFileRef[] = [];
const input = tc.input as Record<string, unknown>;
switch (tc.name) {
case 'Read':
case 'read':
if (input?.file_path) results.push({ path: String(input.file_path), op: 'read' });
break;
case 'Write':
case 'write':
if (input?.file_path) results.push({ path: String(input.file_path), op: 'write' });
break;
case 'Edit':
case 'edit':
if (input?.file_path) results.push({ path: String(input.file_path), op: 'write' });
break;
case 'Glob':
case 'glob':
if (input?.pattern) results.push({ path: String(input.pattern), op: 'glob' });
break;
case 'Grep':
case 'grep':
if (input?.path) results.push({ path: String(input.path), op: 'grep' });
break;
case 'Bash':
case 'bash': {
const cmd = String(input?.command ?? '');
const fileMatch = cmd.match(/(?:cat|head|tail|less|vim|nano|code)\s+["']?([^\s"'|;&]+)/);
if (fileMatch) results.push({ path: fileMatch[1], op: 'bash' });
break;
}
}
return results;
}
/** Extract only write-operation file paths (Write, Edit) */
export function extractWritePaths(tc: ToolCallContent): string[] {
return extractFilePaths(tc)
.filter(r => r.op === 'write')
.map(r => r.path);
}