Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@diffity/api",
"version": "0.10.36",
"version": "0.10.37",
"private": true,
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@naturalcycles/diffity",
"version": "0.10.36",
"version": "0.10.37",
"description": "Agent-agnostic, GitHub-style diff viewer and code review tool with a live agent loop",
"type": "module",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion packages/git/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@diffity/git",
"version": "0.10.36",
"version": "0.10.37",
"private": true,
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/github/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@diffity/github",
"version": "0.10.36",
"version": "0.10.37",
"private": true,
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/parser/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@diffity/parser",
"version": "0.10.36",
"version": "0.10.37",
"private": true,
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@diffity/ui",
"version": "0.10.36",
"version": "0.10.37",
"type": "module",
"private": true,
"scripts": {
Expand Down
19 changes: 11 additions & 8 deletions packages/ui/src/components/comments/orphaned-threads.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { useEffect, useState } from 'react';
import type { CommentThread as CommentThreadType } from './types';
import { isThreadResolved } from './types';
import { DEFAULT_AUTHOR, isThreadResolved } from './types';
import type { CommentActions } from '../../hooks/use-comment-actions';
import { CommentIcon } from '../icons/comment-icon';
import { ChevronIcon } from '../icons/chevron-icon';
import { ThreadBadge } from '../ui/thread-badge';
import { ThreadCard } from './thread-card';

interface OrphanedThreadsProps {
threads: CommentThreadType[];
onEditComment: (commentId: string, body: string) => void;
onDeleteComment: (threadId: string, commentId: string) => void;
onDeleteThread: (threadId: string) => void;
commentActions: CommentActions;
}

export function OrphanedThreads(props: OrphanedThreadsProps) {
const { threads, onEditComment, onDeleteComment, onDeleteThread } = props;
const { threads, commentActions } = props;
const [isExpanded, setIsExpanded] = useState(() => threads.some(thread => !isThreadResolved(thread)));

useEffect(() => {
Expand Down Expand Up @@ -47,13 +46,17 @@ export function OrphanedThreads(props: OrphanedThreadsProps) {
? `Line ${thread.startLine}`
: `Lines ${thread.startLine}–${thread.endLine}`;

// Ask and Act stay off: the agent would be pointed at an anchor the diff no longer has.
return (
<ThreadCard
key={thread.id}
thread={thread}
onEditComment={(commentId, body) => onEditComment(commentId, body)}
onDeleteComment={(commentId) => onDeleteComment(thread.id, commentId)}
onDeleteThread={() => onDeleteThread(thread.id)}
onReply={(body) => commentActions.addReply(thread.id, body, DEFAULT_AUTHOR)}
onResolve={() => commentActions.resolveThread(thread.id)}
onUnresolve={() => commentActions.unresolveThread(thread.id)}
onEditComment={(commentId, body) => commentActions.editComment(commentId, body)}
onDeleteComment={(commentId) => commentActions.deleteComment(thread.id, commentId)}
onDeleteThread={() => commentActions.deleteThread(thread.id)}
className="border border-border max-w-[700px]"
headerLeft={
<>
Expand Down
4 changes: 1 addition & 3 deletions packages/ui/src/components/diff/diff-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -351,9 +351,7 @@ export function DiffView(props: DiffViewProps) {
<div className="px-4 pt-2" data-testid="threads-without-file">
<OrphanedThreads
threads={lostThreads}
onEditComment={commentActions.editComment}
onDeleteComment={commentActions.deleteComment}
onDeleteThread={commentActions.deleteThread}
commentActions={commentActions}
/>
</div>
)}
Expand Down
4 changes: 1 addition & 3 deletions packages/ui/src/components/diff/file-block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -546,9 +546,7 @@ export function FileBlock(props: FileBlockProps) {
<>
<OrphanedThreads
threads={orphanedThreads}
onEditComment={editComment}
onDeleteComment={deleteComment}
onDeleteThread={deleteThread}
commentActions={commentActions}
/>
<div className="code-scroll">
<table className="w-full border-collapse table-fixed code-table">
Expand Down
81 changes: 81 additions & 0 deletions packages/ui/tests/outdated-thread-controls.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { describe, it, expect, afterEach, vi } from 'vitest';
import { render, cleanup, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { OrphanedThreads } from '../src/components/comments/orphaned-threads';
import type { CommentActions } from '../src/hooks/use-comment-actions';
import { DEFAULT_AUTHOR } from '../src/components/comments/types';
import type { CommentThread } from '../src/components/comments/types';
import { makeComment, makeThread } from './helpers/wire';

function actions(): CommentActions {
return {
addThread: vi.fn(),
addReply: vi.fn(),
resolveThread: vi.fn(),
unresolveThread: vi.fn(),
dismissThread: vi.fn(),
editComment: vi.fn(),
deleteComment: vi.fn(),
deleteThread: vi.fn(),
deleteAllThreads: vi.fn(),
};
}

function outdated(over: Partial<CommentThread> = {}): CommentThread {
return makeThread({
id: 'gone',
anchorContent: 'const removed = true;',
comments: [makeComment({ id: 'c1', body: 'P2: this reads oddly' })],
...over,
});
}

function renderThreads(commentActions: CommentActions, thread = outdated()) {
return render(<OrphanedThreads threads={[thread]} commentActions={commentActions} />);
}

afterEach(cleanup);

describe('an outdated thread', () => {
it('can be replied to', async () => {
// Per-keystroke delays are the default and make this the slowest test in the file.
const user = userEvent.setup({ delay: null });
const commentActions = actions();
renderThreads(commentActions);

await user.click(screen.getByText('Reply'));
await user.type(screen.getByPlaceholderText('Reply...'), 'still applies');
await user.click(screen.getByRole('button', { name: 'Reply' }));

expect(commentActions.addReply).toHaveBeenCalledWith('gone', 'still applies', DEFAULT_AUTHOR);
});

it('can be resolved', async () => {
const user = userEvent.setup({ delay: null });
const commentActions = actions();
renderThreads(commentActions);

await user.click(screen.getByText('Resolve'));

expect(commentActions.resolveThread).toHaveBeenCalledWith('gone');
});

it('offers reopen once resolved, not resolve again', async () => {
const user = userEvent.setup({ delay: null });
const commentActions = actions();
renderThreads(commentActions, outdated({ status: 'resolved' }));
// Nothing here is open, so the list starts collapsed.
await user.click(screen.getByText('1 outdated comment'));

expect(screen.queryByText('Resolve')).toBeNull();
await user.click(screen.getByText('Reopen'));

expect(commentActions.unresolveThread).toHaveBeenCalledWith('gone');
});

it('keeps the stale anchor on show', () => {
renderThreads(actions());

expect(screen.getByText('const removed = true;')).toBeTruthy();
});
});
Loading