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:
parent
8e00e0ef8c
commit
82fb618c76
12 changed files with 483 additions and 37 deletions
|
|
@ -2,6 +2,7 @@
|
|||
import { getAgentSessions } from '../../stores/agents.svelte';
|
||||
import { getActiveGroup, getEnabledProjects, setActiveProject } from '../../stores/workspace.svelte';
|
||||
import { getHealthAggregates, getAttentionQueue, type ProjectHealth } from '../../stores/health.svelte';
|
||||
import { getTotalConflictCount } from '../../stores/conflicts.svelte';
|
||||
|
||||
let agentSessions = $derived(getAgentSessions());
|
||||
let activeGroup = $derived(getActiveGroup());
|
||||
|
|
@ -15,6 +16,7 @@
|
|||
let health = $derived(getHealthAggregates());
|
||||
let attentionQueue = $derived(getAttentionQueue(5));
|
||||
|
||||
let totalConflicts = $derived(getTotalConflictCount());
|
||||
let showAttention = $state(false);
|
||||
|
||||
function projectName(projectId: string): string {
|
||||
|
|
@ -67,6 +69,12 @@
|
|||
</span>
|
||||
<span class="sep"></span>
|
||||
{/if}
|
||||
{#if totalConflicts > 0}
|
||||
<span class="item state-conflict" title="{totalConflicts} file conflict{totalConflicts > 1 ? 's' : ''} — multiple agents writing same file">
|
||||
⚠ {totalConflicts} conflict{totalConflicts > 1 ? 's' : ''}
|
||||
</span>
|
||||
<span class="sep"></span>
|
||||
{/if}
|
||||
|
||||
<!-- Attention queue toggle -->
|
||||
{#if attentionQueue.length > 0}
|
||||
|
|
@ -173,6 +181,11 @@
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
.state-conflict {
|
||||
color: var(--ctp-red);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { getAgentSession, getTotalCost, type AgentSession } from '../../stores/agents.svelte';
|
||||
import type { AgentMessage, ToolCallContent, CostContent, CompactionContent } from '../../adapters/sdk-messages';
|
||||
import { extractFilePaths } from '../../utils/tool-files';
|
||||
|
||||
interface Props {
|
||||
sessionId: string | null;
|
||||
|
|
@ -195,41 +196,6 @@
|
|||
return Math.ceil(text.length / 4);
|
||||
}
|
||||
|
||||
function extractFilePaths(tc: ToolCallContent): { path: string; op: string }[] {
|
||||
const results: { path: string; op: string }[] = [];
|
||||
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':
|
||||
// Try to extract file paths from bash commands
|
||||
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;
|
||||
}
|
||||
|
||||
function formatTokens(n: number): string {
|
||||
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
||||
|
|
|
|||
|
|
@ -80,6 +80,12 @@
|
|||
<span class="project-id">({project.identifier})</span>
|
||||
</div>
|
||||
<div class="header-info">
|
||||
{#if health && health.fileConflictCount > 0}
|
||||
<span class="info-conflict" title="{health.fileConflictCount} file conflict{health.fileConflictCount > 1 ? 's' : ''} — multiple agents writing same file">
|
||||
⚠ {health.fileConflictCount} conflict{health.fileConflictCount > 1 ? 's' : ''}
|
||||
</span>
|
||||
<span class="info-sep">·</span>
|
||||
{/if}
|
||||
{#if contextPct !== null && contextPct > 0}
|
||||
<span class="info-ctx" style="color: {ctxColor()}" title="Context window usage">ctx {contextPct}%</span>
|
||||
<span class="info-sep">·</span>
|
||||
|
|
@ -224,6 +230,16 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.info-conflict {
|
||||
font-size: 0.6rem;
|
||||
color: var(--ctp-red);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
background: color-mix(in srgb, var(--ctp-red) 12%, transparent);
|
||||
padding: 0.0625rem 0.375rem;
|
||||
border-radius: 0.1875rem;
|
||||
}
|
||||
|
||||
.info-profile {
|
||||
font-size: 0.65rem;
|
||||
color: var(--ctp-blue);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue