fix(graph): toggle buttons visually distinct — active gets border + glow, inactive dimmed
This commit is contained in:
parent
b7064759c0
commit
8a9121fc3e
6 changed files with 309 additions and 101 deletions
|
|
@ -178,10 +178,10 @@ function ToolbarToggle({
|
|||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={`flex items-center gap-1 rounded-md px-2 py-1 text-[11px] font-mono transition-colors cursor-pointer
|
||||
className={`flex items-center gap-1.5 rounded-md px-2.5 py-1.5 text-[11px] font-mono transition-all cursor-pointer border
|
||||
${active
|
||||
? 'text-[#aaeeff] bg-[rgba(100,200,255,0.12)]'
|
||||
: 'text-[#66ccff50] hover:text-[#66ccff90]'
|
||||
? 'text-[#aaeeff] bg-[rgba(100,200,255,0.18)] border-[rgba(100,200,255,0.3)] shadow-[0_0_6px_rgba(100,200,255,0.15)]'
|
||||
: 'text-[#66ccff40] bg-transparent border-transparent hover:text-[#66ccff70] hover:border-[rgba(100,200,255,0.1)]'
|
||||
}`}
|
||||
>
|
||||
{icon}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,11 @@ import {
|
|||
TASK_STATUS_LABELS,
|
||||
TASK_STATUS_STYLES,
|
||||
} from '@renderer/utils/memberHelpers';
|
||||
import { buildTaskChangeRequestOptions, deriveTaskSince } from '@renderer/utils/taskChangeRequest';
|
||||
import {
|
||||
buildTaskChangeRequestOptions,
|
||||
buildTaskChangeSignature,
|
||||
deriveTaskSince,
|
||||
} from '@renderer/utils/taskChangeRequest';
|
||||
import { linkifyTaskIdsInMarkdown, parseTaskLinkHref } from '@renderer/utils/taskReferenceUtils';
|
||||
import { isLeadMember } from '@shared/utils/leadDetection';
|
||||
import { getTaskKanbanColumn } from '@shared/utils/reviewState';
|
||||
|
|
@ -78,6 +82,8 @@ import {
|
|||
X,
|
||||
} from 'lucide-react';
|
||||
|
||||
const TASK_CHANGES_AUTO_REFRESH_MS = 20_000;
|
||||
|
||||
import { SourceMessageAttachments } from '../attachments/SourceMessageAttachments';
|
||||
|
||||
import { WorkflowTimeline } from './StatusHistoryTimeline';
|
||||
|
|
@ -154,6 +160,9 @@ export const TaskDetailDialog = ({
|
|||
const [taskChangesFiles, setTaskChangesFiles] = useState<FileChangeSummary[] | null>(null);
|
||||
const [taskChangesLoading, setTaskChangesLoading] = useState(false);
|
||||
const [taskChangesError, setTaskChangesError] = useState<string | null>(null);
|
||||
const loadedTaskChangeSummaryKeyRef = useRef<string | null>(null);
|
||||
const taskChangesLoadInFlightRef = useRef(false);
|
||||
const currentTaskChangeSummaryKeyRef = useRef<string | null>(null);
|
||||
|
||||
// Inline editing: subject
|
||||
const [editingSubject, setEditingSubject] = useState(false);
|
||||
|
|
@ -323,6 +332,17 @@ export const TaskDetailDialog = ({
|
|||
() => (currentTask ? buildTaskChangeRequestOptions(currentTask) : null),
|
||||
[currentTask]
|
||||
);
|
||||
const taskChangeRequestSignature = useMemo(
|
||||
() => (taskChangeRequestOptions ? buildTaskChangeSignature(taskChangeRequestOptions) : null),
|
||||
[taskChangeRequestOptions]
|
||||
);
|
||||
const currentTaskChangeSummaryKey = useMemo(
|
||||
() =>
|
||||
currentTask
|
||||
? `${teamName}:${currentTask.id}:${taskChangeRequestSignature ?? 'default'}`
|
||||
: null,
|
||||
[currentTask, teamName, taskChangeRequestSignature]
|
||||
);
|
||||
const taskChangeSummaryOptions = useMemo(
|
||||
() =>
|
||||
currentTask
|
||||
|
|
@ -335,6 +355,10 @@ export const TaskDetailDialog = ({
|
|||
);
|
||||
const setTaskNeedsClarification = useStore((s) => s.setTaskNeedsClarification);
|
||||
|
||||
useEffect(() => {
|
||||
currentTaskChangeSummaryKeyRef.current = currentTaskChangeSummaryKey;
|
||||
}, [currentTaskChangeSummaryKey]);
|
||||
|
||||
const loadTaskChangeSummary = useCallback(
|
||||
async (forceFresh = false): Promise<TaskChangeSetV2 | null> => {
|
||||
if (
|
||||
|
|
@ -355,71 +379,109 @@ export const TaskDetailDialog = ({
|
|||
[canShowTaskChanges, currentTask, onViewChanges, taskChangeSummaryOptions, teamName, variant]
|
||||
);
|
||||
|
||||
const syncTaskChangeSummaryResult = useCallback(
|
||||
(data: TaskChangeSetV2 | null) => {
|
||||
setTaskChangesFiles(data?.files ?? null);
|
||||
if (currentTask && taskChangeRequestOptions) {
|
||||
recordTaskHasChanges(
|
||||
teamName,
|
||||
currentTask.id,
|
||||
taskChangeRequestOptions,
|
||||
!!data?.files.length
|
||||
);
|
||||
}
|
||||
const nextPresence = data ? resolveTaskChangePresenceFromResult(data) : null;
|
||||
if (currentTask && nextPresence) {
|
||||
setSelectedTeamTaskChangePresence(teamName, currentTask.id, nextPresence);
|
||||
}
|
||||
},
|
||||
[
|
||||
currentTask,
|
||||
recordTaskHasChanges,
|
||||
setSelectedTeamTaskChangePresence,
|
||||
taskChangeRequestOptions,
|
||||
teamName,
|
||||
]
|
||||
);
|
||||
|
||||
const requestTaskChangeSummary = useCallback(
|
||||
async ({
|
||||
forceFresh = false,
|
||||
showSpinner = false,
|
||||
preserveFilesOnError = false,
|
||||
}: {
|
||||
forceFresh?: boolean;
|
||||
showSpinner?: boolean;
|
||||
preserveFilesOnError?: boolean;
|
||||
} = {}): Promise<void> => {
|
||||
const requestKey = currentTaskChangeSummaryKeyRef.current;
|
||||
if (taskChangesLoadInFlightRef.current) return;
|
||||
if (
|
||||
!requestKey ||
|
||||
!currentTask ||
|
||||
variant !== 'team' ||
|
||||
!canShowTaskChanges ||
|
||||
!onViewChanges
|
||||
)
|
||||
return;
|
||||
|
||||
taskChangesLoadInFlightRef.current = true;
|
||||
if (showSpinner) {
|
||||
setTaskChangesLoading(true);
|
||||
}
|
||||
setTaskChangesError(null);
|
||||
|
||||
try {
|
||||
const data = await loadTaskChangeSummary(forceFresh);
|
||||
if (currentTaskChangeSummaryKeyRef.current !== requestKey) {
|
||||
return;
|
||||
}
|
||||
syncTaskChangeSummaryResult(data);
|
||||
} catch (error) {
|
||||
if (currentTaskChangeSummaryKeyRef.current !== requestKey) {
|
||||
return;
|
||||
}
|
||||
if (!preserveFilesOnError) {
|
||||
setTaskChangesFiles(null);
|
||||
}
|
||||
setTaskChangesError(
|
||||
error instanceof Error ? error.message : 'Failed to load task changes summary'
|
||||
);
|
||||
} finally {
|
||||
taskChangesLoadInFlightRef.current = false;
|
||||
if (showSpinner) {
|
||||
setTaskChangesLoading(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
canShowTaskChanges,
|
||||
currentTask,
|
||||
loadTaskChangeSummary,
|
||||
onViewChanges,
|
||||
syncTaskChangeSummaryResult,
|
||||
variant,
|
||||
]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (variant !== 'team') return;
|
||||
if (!open || !currentTask || !canShowTaskChanges || !onViewChanges || !changesSectionOpen)
|
||||
return;
|
||||
|
||||
let cancelled = false;
|
||||
const summaryKey = currentTaskChangeSummaryKey;
|
||||
if (loadedTaskChangeSummaryKeyRef.current === summaryKey) {
|
||||
return;
|
||||
}
|
||||
loadedTaskChangeSummaryKeyRef.current = summaryKey;
|
||||
|
||||
// Show full loading state only when no files are cached yet;
|
||||
// otherwise let the refresh button spinner indicate background reload.
|
||||
if (!taskChangesFiles || taskChangesFiles.length === 0) {
|
||||
setTaskChangesLoading(true);
|
||||
}
|
||||
setTaskChangesError(null);
|
||||
void loadTaskChangeSummary()
|
||||
.then((data) => {
|
||||
if (!cancelled) {
|
||||
setTaskChangesFiles(data?.files ?? null);
|
||||
if (currentTask && taskChangeRequestOptions) {
|
||||
recordTaskHasChanges(
|
||||
teamName,
|
||||
currentTask.id,
|
||||
taskChangeRequestOptions,
|
||||
!!data?.files.length
|
||||
);
|
||||
}
|
||||
const nextPresence = data ? resolveTaskChangePresenceFromResult(data) : null;
|
||||
if (currentTask && nextPresence) {
|
||||
setSelectedTeamTaskChangePresence(teamName, currentTask.id, nextPresence);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!cancelled) {
|
||||
setTaskChangesFiles(null);
|
||||
setTaskChangesError(
|
||||
error instanceof Error ? error.message : 'Failed to load task changes summary'
|
||||
);
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setTaskChangesLoading(false);
|
||||
});
|
||||
|
||||
void loadTaskChangeSummary(true)
|
||||
.then((data) => {
|
||||
if (!cancelled && data) {
|
||||
setTaskChangesFiles(data.files);
|
||||
if (currentTask && taskChangeRequestOptions) {
|
||||
recordTaskHasChanges(
|
||||
teamName,
|
||||
currentTask.id,
|
||||
taskChangeRequestOptions,
|
||||
data.files.length > 0
|
||||
);
|
||||
}
|
||||
const nextPresence = resolveTaskChangePresenceFromResult(data);
|
||||
if (currentTask && nextPresence) {
|
||||
setSelectedTeamTaskChangePresence(teamName, currentTask.id, nextPresence);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => undefined);
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
void requestTaskChangeSummary({
|
||||
forceFresh: false,
|
||||
showSpinner: !taskChangesFiles || taskChangesFiles.length === 0,
|
||||
preserveFilesOnError: false,
|
||||
});
|
||||
}, [
|
||||
changesSectionOpen,
|
||||
open,
|
||||
|
|
@ -427,50 +489,54 @@ export const TaskDetailDialog = ({
|
|||
canShowTaskChanges,
|
||||
teamName,
|
||||
onViewChanges,
|
||||
taskSince,
|
||||
currentTaskChangeSummaryKey,
|
||||
taskChangeRequestSignature,
|
||||
variant,
|
||||
loadTaskChangeSummary,
|
||||
requestTaskChangeSummary,
|
||||
taskChangesFiles,
|
||||
]);
|
||||
|
||||
const handleRefreshChanges = useCallback(() => {
|
||||
if (!currentTask || variant !== 'team' || !canShowTaskChanges || !onViewChanges) return;
|
||||
setTaskChangesLoading(true);
|
||||
setTaskChangesError(null);
|
||||
void loadTaskChangeSummary(true)
|
||||
.then((data) => {
|
||||
setTaskChangesFiles(data?.files ?? null);
|
||||
if (currentTask && taskChangeRequestOptions) {
|
||||
recordTaskHasChanges(
|
||||
teamName,
|
||||
currentTask.id,
|
||||
taskChangeRequestOptions,
|
||||
!!data?.files.length
|
||||
);
|
||||
}
|
||||
const nextPresence = data ? resolveTaskChangePresenceFromResult(data) : null;
|
||||
if (currentTask && nextPresence) {
|
||||
setSelectedTeamTaskChangePresence(teamName, currentTask.id, nextPresence);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setTaskChangesFiles(null);
|
||||
setTaskChangesError(
|
||||
error instanceof Error ? error.message : 'Failed to load task changes summary'
|
||||
);
|
||||
})
|
||||
.finally(() => setTaskChangesLoading(false));
|
||||
useEffect(() => {
|
||||
if (!open || !changesSectionOpen) {
|
||||
loadedTaskChangeSummaryKeyRef.current = null;
|
||||
}
|
||||
}, [open, changesSectionOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (variant !== 'team') return;
|
||||
if (!open || !currentTask || !canShowTaskChanges || !onViewChanges || !changesSectionOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = window.setInterval(() => {
|
||||
void requestTaskChangeSummary({
|
||||
forceFresh: true,
|
||||
showSpinner: false,
|
||||
preserveFilesOnError: true,
|
||||
});
|
||||
}, TASK_CHANGES_AUTO_REFRESH_MS);
|
||||
|
||||
return () => {
|
||||
window.clearInterval(timer);
|
||||
};
|
||||
}, [
|
||||
changesSectionOpen,
|
||||
open,
|
||||
currentTask,
|
||||
canShowTaskChanges,
|
||||
onViewChanges,
|
||||
loadTaskChangeSummary,
|
||||
recordTaskHasChanges,
|
||||
setSelectedTeamTaskChangePresence,
|
||||
taskChangeRequestOptions,
|
||||
teamName,
|
||||
requestTaskChangeSummary,
|
||||
variant,
|
||||
]);
|
||||
|
||||
const handleRefreshChanges = useCallback(() => {
|
||||
void requestTaskChangeSummary({
|
||||
forceFresh: true,
|
||||
showSpinner: true,
|
||||
preserveFilesOnError: false,
|
||||
});
|
||||
}, [requestTaskChangeSummary]);
|
||||
|
||||
const handleDependencyClick = (taskId: string): void => {
|
||||
// Resolve short displayId (e.g. "8ce74455") to full UUID via taskMap,
|
||||
// since kanban cards use the full UUID in data-task-id.
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import { CodeMirrorDiffView } from './CodeMirrorDiffView';
|
|||
import { DiffErrorBoundary } from './DiffErrorBoundary';
|
||||
import { FileSectionPlaceholder } from './FileSectionPlaceholder';
|
||||
import { ReviewDiffContent } from './ReviewDiffContent';
|
||||
import {
|
||||
shouldRenderCodeMirrorReviewDiff,
|
||||
shouldRenderSnippetReviewPreview,
|
||||
} from './reviewDiffSafety';
|
||||
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import type { FileChangeWithContent } from '@shared/types';
|
||||
|
|
@ -47,6 +51,7 @@ export const FileSectionDiff = ({
|
|||
}: FileSectionDiffProps): React.ReactElement => {
|
||||
const localEditorViewRef = useRef<EditorView | null>(null);
|
||||
const sentinelRef = useRef<HTMLDivElement>(null);
|
||||
const canRenderSnippetPreview = shouldRenderSnippetReviewPreview(file.snippets);
|
||||
|
||||
// Notify parent whenever CodeMirrorDiffView creates or destroys its EditorView.
|
||||
// This fires on every editor lifecycle event: initial mount, key-change remount,
|
||||
|
|
@ -87,7 +92,11 @@ export const FileSectionDiff = ({
|
|||
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<ReviewDiffContent file={file} />
|
||||
{canRenderSnippetPreview ? (
|
||||
<ReviewDiffContent file={file} />
|
||||
) : (
|
||||
<OversizedDiffNotice message="Diff preview skipped because the change is too large to render safely." />
|
||||
)}
|
||||
<div ref={sentinelRef} className="h-1 shrink-0" />
|
||||
</div>
|
||||
);
|
||||
|
|
@ -115,18 +124,29 @@ export const FileSectionDiff = ({
|
|||
// pretend it's empty — fall back to snippet-level diff.
|
||||
const canRenderCodeMirror =
|
||||
resolvedModified !== null && (file.isNewFile || resolvedOriginal !== null);
|
||||
const originalForDiff = file.isNewFile ? '' : (resolvedOriginal ?? '');
|
||||
const canRenderCodeMirrorSafely =
|
||||
canRenderCodeMirror &&
|
||||
shouldRenderCodeMirrorReviewDiff(originalForDiff, resolvedModified ?? '');
|
||||
|
||||
if (!canRenderCodeMirror) {
|
||||
if (!canRenderCodeMirrorSafely) {
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
<ReviewDiffContent file={file} />
|
||||
<OversizedDiffNotice
|
||||
message={
|
||||
canRenderCodeMirror && !canRenderSnippetPreview
|
||||
? 'Full diff skipped because it is large enough to risk a renderer out-of-memory crash.'
|
||||
: canRenderCodeMirror
|
||||
? 'Large diff opened in safe preview mode to avoid a renderer out-of-memory crash.'
|
||||
: 'Diff preview skipped because the available change data is too large to render safely.'
|
||||
}
|
||||
/>
|
||||
{canRenderSnippetPreview ? <ReviewDiffContent file={file} /> : null}
|
||||
<div ref={sentinelRef} className="h-1 shrink-0" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const originalForDiff = file.isNewFile ? '' : (resolvedOriginal ?? '');
|
||||
|
||||
return (
|
||||
<div className="overflow-auto">
|
||||
{isMissingOnDisk && (
|
||||
|
|
@ -170,3 +190,11 @@ export const FileSectionDiff = ({
|
|||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function OversizedDiffNotice({ message }: { message: string }): React.ReactElement {
|
||||
return (
|
||||
<div className="border-b border-border bg-amber-500/10 px-4 py-3 text-xs text-amber-300">
|
||||
{message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
59
src/renderer/components/team/review/reviewDiffSafety.ts
Normal file
59
src/renderer/components/team/review/reviewDiffSafety.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import type { SnippetDiff } from '@shared/types/review';
|
||||
|
||||
const MAX_CODEMIRROR_DIFF_COMBINED_BYTES = 2 * 1024 * 1024;
|
||||
const MAX_CODEMIRROR_DIFF_LINE_PRODUCT = 1_000_000;
|
||||
const MAX_SNIPPET_PREVIEW_COMBINED_BYTES = 512 * 1024;
|
||||
const MAX_SNIPPET_PREVIEW_TOTAL_LINES = 4_000;
|
||||
|
||||
function countLinesUpTo(text: string, limit: number): number {
|
||||
let lines = 1;
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
if (text.charCodeAt(i) === 10) {
|
||||
lines++;
|
||||
if (lines > limit) {
|
||||
return lines;
|
||||
}
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
export function shouldRenderCodeMirrorReviewDiff(original: string, modified: string): boolean {
|
||||
const combinedBytes = original.length + modified.length;
|
||||
if (combinedBytes > MAX_CODEMIRROR_DIFF_COMBINED_BYTES) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldLines = countLinesUpTo(original, MAX_CODEMIRROR_DIFF_LINE_PRODUCT + 1);
|
||||
const newLines = countLinesUpTo(modified, MAX_CODEMIRROR_DIFF_LINE_PRODUCT + 1);
|
||||
|
||||
return oldLines * newLines <= MAX_CODEMIRROR_DIFF_LINE_PRODUCT;
|
||||
}
|
||||
|
||||
export function shouldRenderSnippetReviewPreview(snippets: SnippetDiff[]): boolean {
|
||||
let totalBytes = 0;
|
||||
let totalLines = 0;
|
||||
|
||||
for (const snippet of snippets) {
|
||||
if (snippet.isError) {
|
||||
continue;
|
||||
}
|
||||
|
||||
totalBytes += snippet.oldString.length + snippet.newString.length;
|
||||
if (totalBytes > MAX_SNIPPET_PREVIEW_COMBINED_BYTES) {
|
||||
return false;
|
||||
}
|
||||
|
||||
totalLines += countLinesUpTo(snippet.oldString, MAX_SNIPPET_PREVIEW_TOTAL_LINES);
|
||||
if (totalLines > MAX_SNIPPET_PREVIEW_TOTAL_LINES) {
|
||||
return false;
|
||||
}
|
||||
|
||||
totalLines += countLinesUpTo(snippet.newString, MAX_SNIPPET_PREVIEW_TOTAL_LINES);
|
||||
if (totalLines > MAX_SNIPPET_PREVIEW_TOTAL_LINES) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
@ -143,6 +143,8 @@ export function initializeNotificationListeners(): () => void {
|
|||
cleanupFns.push(() => {
|
||||
if (cliStatusTimer) clearTimeout(cliStatusTimer);
|
||||
});
|
||||
// This lightweight renderer-side poll keeps visible in-progress task badges fresh.
|
||||
// It is intentionally independent from the backend log-source tracking feature flag below.
|
||||
const inProgressChangePresencePollTimer = setInterval(() => {
|
||||
void pollVisibleTeamInProgressChangePresence();
|
||||
}, IN_PROGRESS_CHANGE_PRESENCE_POLL_MS);
|
||||
|
|
|
|||
53
test/renderer/components/reviewDiffSafety.test.ts
Normal file
53
test/renderer/components/reviewDiffSafety.test.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
shouldRenderCodeMirrorReviewDiff,
|
||||
shouldRenderSnippetReviewPreview,
|
||||
} from '@renderer/components/team/review/reviewDiffSafety';
|
||||
|
||||
describe('reviewDiffSafety', () => {
|
||||
it('allows regular CodeMirror review diffs', () => {
|
||||
expect(shouldRenderCodeMirrorReviewDiff('line 1\nline 2', 'line 1\nline 3')).toBe(true);
|
||||
});
|
||||
|
||||
it('blocks oversized CodeMirror review diffs by line-product', () => {
|
||||
const original = Array.from({ length: 1200 }, (_, i) => `old ${i}`).join('\n');
|
||||
const modified = Array.from({ length: 1200 }, (_, i) => `new ${i}`).join('\n');
|
||||
|
||||
expect(shouldRenderCodeMirrorReviewDiff(original, modified)).toBe(false);
|
||||
});
|
||||
|
||||
it('allows small snippet previews', () => {
|
||||
expect(
|
||||
shouldRenderSnippetReviewPreview([
|
||||
{
|
||||
filePath: '/tmp/a.ts',
|
||||
oldString: 'const a = 1;\n',
|
||||
newString: 'const a = 2;\n',
|
||||
timestamp: '2026-03-28T10:00:00.000Z',
|
||||
toolUseId: 'tool-1',
|
||||
type: 'edit',
|
||||
replaceAll: false,
|
||||
isError: false,
|
||||
},
|
||||
])
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('blocks oversized snippet previews', () => {
|
||||
expect(
|
||||
shouldRenderSnippetReviewPreview([
|
||||
{
|
||||
filePath: '/tmp/big.ts',
|
||||
oldString: '',
|
||||
newString: 'a'.repeat(600 * 1024),
|
||||
timestamp: '2026-03-28T10:00:00.000Z',
|
||||
toolUseId: 'tool-2',
|
||||
type: 'write-update',
|
||||
replaceAll: false,
|
||||
isError: false,
|
||||
},
|
||||
])
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue