mirror of
https://github.com/nilbuild/diffity.git
synced 2026-09-19 07:26:16 +08:00
Refactor and fix comment navigation for files
This commit is contained in:
@@ -1,43 +0,0 @@
|
||||
# diffity
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- add support for branch comparisons
|
||||
|
||||
## 0.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Refactor skills
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Improve skills and add support for multiple sessions
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fix chunk loading indicator
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- It now handles invalid refs and gracefully exits with help instead of fatal errors.
|
||||
|
||||
## 0.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Improve the skill files for comment resolution and add cleanup for the dev scripts.
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- initial release
|
||||
@@ -7,7 +7,6 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const distDir = join(__dirname, 'dist');
|
||||
const isWatch = process.argv.includes('--watch');
|
||||
|
||||
// Clean all previous build output except the ui/ directory (built separately by vite)
|
||||
for (const entry of readdirSync(distDir)) {
|
||||
if (entry === 'ui') {
|
||||
continue;
|
||||
@@ -20,9 +19,9 @@ for (const entry of readdirSync(distDir)) {
|
||||
const buildOptions = {
|
||||
entryPoints: [join(__dirname, 'src/index.ts')],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
platform: 'node' as const,
|
||||
target: 'node18',
|
||||
format: 'esm',
|
||||
format: 'esm' as const,
|
||||
outfile: join(distDir, 'index.js'),
|
||||
banner: {
|
||||
js: '#!/usr/bin/env node',
|
||||
@@ -7,9 +7,9 @@
|
||||
"diffity": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "node build.js",
|
||||
"build": "tsx build.ts",
|
||||
"dev": "tsx src/index.ts",
|
||||
"dev:watch": "node build.js --watch"
|
||||
"dev:watch": "tsx build.ts --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^12.8.0",
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
isDirty,
|
||||
getTree,
|
||||
getTreeEntries,
|
||||
getTreeFingerprint,
|
||||
getWorkingTreeFileContent,
|
||||
WORKING_TREE_REFS,
|
||||
} from '@diffity/git';
|
||||
import {
|
||||
@@ -445,6 +447,16 @@ export function startServer(options: ServerOptions): Promise<ServerResult> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === '/api/tree/fingerprint') {
|
||||
const raw = getTreeFingerprint();
|
||||
const hash = createHash('sha1')
|
||||
.update(raw)
|
||||
.digest('hex')
|
||||
.slice(0, 12);
|
||||
sendJson(res, { fingerprint: hash });
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === '/api/tree') {
|
||||
try {
|
||||
const paths = getTree();
|
||||
@@ -466,6 +478,19 @@ export function startServer(options: ServerOptions): Promise<ServerResult> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.startsWith('/api/tree/file/')) {
|
||||
const filePath = decodeURIComponent(
|
||||
pathname.slice('/api/tree/file/'.length),
|
||||
);
|
||||
try {
|
||||
const content = getWorkingTreeFileContent(filePath);
|
||||
sendJson(res, { path: filePath, content: content.split('\n') });
|
||||
} catch {
|
||||
sendError(res, 404, `File not found: ${filePath}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname === '/api/tree/info') {
|
||||
const info = getRepoInfo();
|
||||
const session = findOrCreateSession('__tree__');
|
||||
|
||||
@@ -5,5 +5,5 @@ export { getDiff, getDiffFiles, getDiffStat, getDiffStatForRef, getUntrackedFile
|
||||
export type { RefDiffArgs } from './diff.js';
|
||||
export { getStagedFiles, getUnstagedFiles, isDirty } from './status.js';
|
||||
export { getRecentCommits } from './commits.js';
|
||||
export { getTree, getTreeEntries } from './tree.js';
|
||||
export { getTree, getTreeEntries, getTreeFingerprint, getWorkingTreeFileContent } from './tree.js';
|
||||
export type { TreeEntry } from './tree.js';
|
||||
|
||||
+76
-18
@@ -1,4 +1,6 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export interface TreeEntry {
|
||||
type: 'blob' | 'tree';
|
||||
@@ -6,29 +8,85 @@ export interface TreeEntry {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export function getTree(ref = 'HEAD'): string[] {
|
||||
const output = execFileSync('git', ['ls-tree', '-r', '--name-only', ref], {
|
||||
function getWorkingTreeFiles(dirPath?: string): string[] {
|
||||
const pathArgs = dirPath ? [dirPath + '/'] : [];
|
||||
|
||||
const tracked = execFileSync('git', ['ls-files', ...pathArgs], {
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
if (!output) {
|
||||
return [];
|
||||
}
|
||||
return output.split('\n');
|
||||
}
|
||||
|
||||
export function getTreeEntries(ref = 'HEAD', dirPath?: string): TreeEntry[] {
|
||||
const target = dirPath ? `${ref}:${dirPath}` : ref;
|
||||
const raw = execFileSync('git', ['ls-tree', target], {
|
||||
const deleted = execFileSync('git', ['ls-files', '--deleted', ...pathArgs], {
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
if (!raw) {
|
||||
return [];
|
||||
|
||||
const untracked = execFileSync(
|
||||
'git',
|
||||
['ls-files', '--others', '--exclude-standard', ...pathArgs],
|
||||
{ encoding: 'utf-8' },
|
||||
).trim();
|
||||
|
||||
const deletedSet = new Set(deleted ? deleted.split('\n') : []);
|
||||
const files = new Set<string>();
|
||||
|
||||
if (tracked) {
|
||||
for (const f of tracked.split('\n')) {
|
||||
if (!deletedSet.has(f)) {
|
||||
files.add(f);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (untracked) {
|
||||
for (const f of untracked.split('\n')) {
|
||||
files.add(f);
|
||||
}
|
||||
}
|
||||
|
||||
return raw.split('\n').map(line => {
|
||||
const [info, name] = line.split('\t');
|
||||
const type = info.split(/\s+/)[1] as 'blob' | 'tree';
|
||||
const fullPath = dirPath ? `${dirPath}/${name}` : name;
|
||||
return { type, path: fullPath, name };
|
||||
});
|
||||
return Array.from(files).sort();
|
||||
}
|
||||
|
||||
export function getTree(): string[] {
|
||||
return getWorkingTreeFiles();
|
||||
}
|
||||
|
||||
export function getTreeEntries(_ref = 'HEAD', dirPath?: string): TreeEntry[] {
|
||||
const files = getWorkingTreeFiles(dirPath);
|
||||
const prefix = dirPath ? dirPath + '/' : '';
|
||||
const entries = new Map<string, TreeEntry>();
|
||||
|
||||
for (const file of files) {
|
||||
const relative = file.slice(prefix.length);
|
||||
const slashIndex = relative.indexOf('/');
|
||||
if (slashIndex === -1) {
|
||||
entries.set(relative, { type: 'blob', path: file, name: relative });
|
||||
} else {
|
||||
const dirName = relative.slice(0, slashIndex);
|
||||
const fullPath = prefix + dirName;
|
||||
if (!entries.has(dirName)) {
|
||||
entries.set(dirName, { type: 'tree', path: fullPath, name: dirName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(entries.values());
|
||||
}
|
||||
|
||||
export function getTreeFingerprint(): string {
|
||||
const tracked = execFileSync('git', ['ls-files'], {
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
|
||||
const statOutput = execFileSync(
|
||||
'git',
|
||||
['status', '--porcelain', '-u'],
|
||||
{ encoding: 'utf-8' },
|
||||
).trim();
|
||||
|
||||
return `${tracked.length}:${statOutput}`;
|
||||
}
|
||||
|
||||
export function getWorkingTreeFileContent(filePath: string): string {
|
||||
const root = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
||||
encoding: 'utf-8',
|
||||
}).trim();
|
||||
return readFileSync(join(root, filePath), 'utf-8');
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Toaster } from 'sonner';
|
||||
import { useSearchParams } from './hooks/use-search-params';
|
||||
import { DiffPage } from './components/diff-page';
|
||||
import { TreePage } from './components/tree-page';
|
||||
import { DiffPage } from './components/diff/diff-page';
|
||||
import { TreePage } from './components/tree/tree-page';
|
||||
|
||||
export function App() {
|
||||
const { ref, theme, view, mode } = useSearchParams();
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import type { Comment } from '../types/comment';
|
||||
import { PencilIcon } from './icons/pencil-icon';
|
||||
import { TrashIcon } from './icons/trash-icon';
|
||||
import { MarkdownContent } from './markdown-content';
|
||||
import type { Comment } from './types';
|
||||
import { PencilIcon } from '../icons/pencil-icon';
|
||||
import { TrashIcon } from '../icons/trash-icon';
|
||||
import { MarkdownContent } from '../layout/markdown-content';
|
||||
|
||||
interface CommentBubbleProps {
|
||||
comment: Comment;
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { CommentAuthor, CommentSide } from '../types/comment';
|
||||
import type { CommentAuthor, CommentSide } from './types';
|
||||
import { CommentForm } from './comment-form';
|
||||
|
||||
interface CommentFormRowProps {
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
import { cn } from '../lib/cn';
|
||||
import { PlusIcon } from './icons/plus-icon';
|
||||
import { cn } from '../../lib/cn';
|
||||
import { PlusIcon } from '../icons/plus-icon';
|
||||
|
||||
interface CommentLineNumberProps {
|
||||
lineNumber: number | null;
|
||||
+21
-10
@@ -1,9 +1,9 @@
|
||||
import { useState } from 'react';
|
||||
import type { CommentThread as CommentThreadType } from '../types/comment';
|
||||
import type { CommentAuthor, CommentSide } from '../types/comment';
|
||||
import { isThreadResolved } from '../types/comment';
|
||||
import { CommentIcon } from './icons/comment-icon';
|
||||
import { ThreadBadge } from './ui/thread-badge';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import type { CommentThread as CommentThreadType } from './types';
|
||||
import type { CommentAuthor, CommentSide } from './types';
|
||||
import { isThreadResolved } from './types';
|
||||
import { CommentIcon } from '../icons/comment-icon';
|
||||
import { ThreadBadge } from '../ui/thread-badge';
|
||||
import { ThreadCard } from './thread-card';
|
||||
|
||||
interface CommentThreadProps {
|
||||
@@ -50,6 +50,17 @@ export function CommentThread(props: CommentThreadProps) {
|
||||
currentCode,
|
||||
} = props;
|
||||
const [isCollapsed, setIsCollapsed] = useState(isThreadResolved(thread));
|
||||
const rowRef = useRef<HTMLTableRowElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const el = rowRef.current;
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
const handler = () => setIsCollapsed(false);
|
||||
el.addEventListener('diffity:focus-thread', handler);
|
||||
return () => el.removeEventListener('diffity:focus-thread', handler);
|
||||
}, []);
|
||||
|
||||
const isOutdated =
|
||||
thread.anchorContent && currentCode && thread.anchorContent !== currentCode;
|
||||
@@ -74,7 +85,7 @@ export function CommentThread(props: CommentThreadProps) {
|
||||
|
||||
if (viewMode === 'split') {
|
||||
return (
|
||||
<tr data-thread-id={thread.id}>
|
||||
<tr ref={rowRef} data-thread-id={thread.id}>
|
||||
{side === 'old' ? (
|
||||
<>
|
||||
{collapsedContent}
|
||||
@@ -90,7 +101,7 @@ export function CommentThread(props: CommentThreadProps) {
|
||||
);
|
||||
}
|
||||
|
||||
return <tr data-thread-id={thread.id}>{collapsedContent}</tr>;
|
||||
return <tr ref={rowRef} data-thread-id={thread.id}>{collapsedContent}</tr>;
|
||||
}
|
||||
|
||||
const lineLabel =
|
||||
@@ -135,7 +146,7 @@ export function CommentThread(props: CommentThreadProps) {
|
||||
|
||||
if (viewMode === 'split') {
|
||||
return (
|
||||
<tr data-thread-id={thread.id}>
|
||||
<tr ref={rowRef} data-thread-id={thread.id}>
|
||||
{side === 'old' ? (
|
||||
<>
|
||||
{threadContent}
|
||||
@@ -151,5 +162,5 @@ export function CommentThread(props: CommentThreadProps) {
|
||||
);
|
||||
}
|
||||
|
||||
return <tr data-thread-id={thread.id}>{threadContent}</tr>;
|
||||
return <tr ref={rowRef} data-thread-id={thread.id}>{threadContent}</tr>;
|
||||
}
|
||||
+9
-9
@@ -1,13 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { useCopy } from '../hooks/use-copy';
|
||||
import { useThreadNavigation } from '../hooks/use-thread-navigation';
|
||||
import type { CommentThread } from '../types/comment';
|
||||
import { CopyIcon } from './icons/copy-icon';
|
||||
import { CheckIcon } from './icons/check-icon';
|
||||
import { ChevronUpIcon } from './icons/chevron-up-icon';
|
||||
import { ChevronDownIcon } from './icons/chevron-down-icon';
|
||||
import { TrashIcon } from './icons/trash-icon';
|
||||
import { ConfirmDialog } from './ui/confirm-dialog';
|
||||
import { useCopy } from '../../hooks/use-copy';
|
||||
import { useThreadNavigation } from '../../hooks/use-thread-navigation';
|
||||
import type { CommentThread } from './types';
|
||||
import { CopyIcon } from '../icons/copy-icon';
|
||||
import { CheckIcon } from '../icons/check-icon';
|
||||
import { ChevronUpIcon } from '../icons/chevron-up-icon';
|
||||
import { ChevronDownIcon } from '../icons/chevron-down-icon';
|
||||
import { TrashIcon } from '../icons/trash-icon';
|
||||
import { ConfirmDialog } from '../ui/confirm-dialog';
|
||||
|
||||
interface CommentToolbarActionsProps {
|
||||
threads: CommentThread[];
|
||||
+5
-5
@@ -1,10 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import { GENERAL_THREAD_FILE_PATH, isThreadResolved, DEFAULT_AUTHOR } from '../types/comment';
|
||||
import type { CommentThread as CommentThreadType } from '../types/comment';
|
||||
import type { CommentActions } from '../hooks/use-comment-actions';
|
||||
import { GENERAL_THREAD_FILE_PATH, isThreadResolved, DEFAULT_AUTHOR } from './types';
|
||||
import type { CommentThread as CommentThreadType } from './types';
|
||||
import type { CommentActions } from '../../hooks/use-comment-actions';
|
||||
import { CommentForm } from './comment-form';
|
||||
import { CommentIcon } from './icons/comment-icon';
|
||||
import { ThreadBadge } from './ui/thread-badge';
|
||||
import { CommentIcon } from '../icons/comment-icon';
|
||||
import { ThreadBadge } from '../ui/thread-badge';
|
||||
import { ThreadCard } from './thread-card';
|
||||
|
||||
interface GeneralCommentsProps {
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { CommentThread as CommentThreadType } from '../types/comment';
|
||||
import { isThreadResolved } from '../types/comment';
|
||||
import { CommentIcon } from './icons/comment-icon';
|
||||
import { ChevronIcon } from './icons/chevron-icon';
|
||||
import { ThreadBadge } from './ui/thread-badge';
|
||||
import type { CommentThread as CommentThreadType } from './types';
|
||||
import { isThreadResolved } from './types';
|
||||
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 {
|
||||
+18
-7
@@ -1,10 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import type { CommentThread as CommentThreadType } from '../types/comment';
|
||||
import { isThreadResolved, DEFAULT_AUTHOR } from '../types/comment';
|
||||
import type { CommentActions } from '../hooks/use-comment-actions';
|
||||
import { useState, useEffect } from 'react';
|
||||
import type { CommentThread as CommentThreadType } from './types';
|
||||
import { isThreadResolved, DEFAULT_AUTHOR } from './types';
|
||||
import type { CommentActions } from '../../hooks/use-comment-actions';
|
||||
import { CommentForm } from './comment-form';
|
||||
import { CommentIcon } from './icons/comment-icon';
|
||||
import { ThreadBadge } from './ui/thread-badge';
|
||||
import { CommentIcon } from '../icons/comment-icon';
|
||||
import { ThreadBadge } from '../ui/thread-badge';
|
||||
import { ThreadCard } from './thread-card';
|
||||
|
||||
interface PathCommentsProps {
|
||||
@@ -13,13 +13,24 @@ interface PathCommentsProps {
|
||||
commentActions: CommentActions;
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
focusedThreadId?: string | null;
|
||||
}
|
||||
|
||||
export function PathComments(props: PathCommentsProps) {
|
||||
const { pathKey, threads, commentActions, label, children } = props;
|
||||
const { pathKey, threads, commentActions, label, children, focusedThreadId } = props;
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!focusedThreadId) {
|
||||
return;
|
||||
}
|
||||
const hasThread = threads.some(t => t.id === focusedThreadId);
|
||||
if (hasThread) {
|
||||
setIsExpanded(true);
|
||||
}
|
||||
}, [focusedThreadId, threads]);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (isExpanded) {
|
||||
setIsExpanded(false);
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
import { useState } from 'react';
|
||||
import type { CommentThread as CommentThreadType } from '../types/comment';
|
||||
import { isThreadResolved } from '../types/comment';
|
||||
import type { CommentThread as CommentThreadType } from './types';
|
||||
import { isThreadResolved } from './types';
|
||||
import { CommentBubble } from './comment-bubble';
|
||||
import { CommentForm } from './comment-form';
|
||||
import { TrashIcon } from './icons/trash-icon';
|
||||
import { cn } from '../lib/cn';
|
||||
import { TrashIcon } from '../icons/trash-icon';
|
||||
import { cn } from '../../lib/cn';
|
||||
|
||||
interface ThreadCardProps {
|
||||
thread: CommentThreadType;
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
import type { DiffLine as DiffLineType } from '@diffity/parser';
|
||||
import { cn } from '../lib/cn';
|
||||
import { getLineBg } from '../lib/diff-utils';
|
||||
import { renderContent } from '../lib/render-content';
|
||||
import type { SyntaxToken } from '../lib/syntax-token';
|
||||
import { CommentLineNumber } from './comment-line-number';
|
||||
import type { CommentSide } from '../types/comment';
|
||||
import { cn } from '../../lib/cn';
|
||||
import { getLineBg } from '../../lib/diff-utils';
|
||||
import { renderContent } from '../../lib/render-content';
|
||||
import type { SyntaxToken } from '../../lib/syntax-token';
|
||||
import { CommentLineNumber } from '../comments/comment-line-number';
|
||||
import type { CommentSide } from '../comments/types';
|
||||
|
||||
export type { SyntaxToken };
|
||||
|
||||
+19
-19
@@ -1,25 +1,25 @@
|
||||
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useDiff } from '../hooks/use-diff';
|
||||
import { useInfo } from '../hooks/use-info';
|
||||
import { useTheme } from '../hooks/use-theme';
|
||||
import { useKeyboard } from '../hooks/use-keyboard';
|
||||
import { useReviewThreads } from '../hooks/use-review-threads';
|
||||
import { useCommentActions } from '../hooks/use-comment-actions';
|
||||
import { Toolbar } from './toolbar';
|
||||
import { useDiff } from '../../hooks/use-diff';
|
||||
import { useInfo } from '../../hooks/use-info';
|
||||
import { useTheme } from '../../hooks/use-theme';
|
||||
import { useKeyboard } from '../../hooks/use-keyboard';
|
||||
import { useReviewThreads } from '../../hooks/use-review-threads';
|
||||
import { useCommentActions } from '../../hooks/use-comment-actions';
|
||||
import { Toolbar } from '../layout/toolbar';
|
||||
import { DiffView, type DiffViewHandle } from './diff-view';
|
||||
import { Sidebar } from './sidebar';
|
||||
import { ShortcutModal } from './shortcut-modal';
|
||||
import { StaleDiffBanner } from './stale-diff-banner';
|
||||
import { CheckCircleIcon } from './icons/check-circle-icon';
|
||||
import { PageLoader } from './skeleton';
|
||||
import { useDiffStaleness } from '../hooks/use-diff-staleness';
|
||||
import { type ViewMode, getFilePath, getAutoCollapsedPaths } from '../lib/diff-utils';
|
||||
import { buildFirstOpenThreadByFile, buildThreadCountsByFile } from '../lib/comment-navigation';
|
||||
import { getHunkHeaders, scrollToElement } from '../lib/dom-utils';
|
||||
import { fetchGitHubDetails, type GitHubDetails } from '../lib/api';
|
||||
import type { LineSelection } from '../types/comment';
|
||||
import { isThreadResolved } from '../types/comment';
|
||||
import { Sidebar } from '../layout/sidebar';
|
||||
import { ShortcutModal } from '../layout/shortcut-modal';
|
||||
import { StaleDiffBanner } from '../layout/stale-diff-banner';
|
||||
import { CheckCircleIcon } from '../icons/check-circle-icon';
|
||||
import { PageLoader } from '../layout/skeleton';
|
||||
import { useDiffStaleness } from '../../hooks/use-diff-staleness';
|
||||
import { type ViewMode, getFilePath, getAutoCollapsedPaths } from '../../lib/diff-utils';
|
||||
import { buildFirstOpenThreadByFile, buildThreadCountsByFile } from '../../lib/comment-navigation';
|
||||
import { getHunkHeaders, scrollToElement } from '../../lib/dom-utils';
|
||||
import { fetchGitHubDetails, type GitHubDetails } from '../../lib/api';
|
||||
import type { LineSelection } from '../comments/types';
|
||||
import { isThreadResolved } from '../comments/types';
|
||||
|
||||
interface DiffPageProps {
|
||||
refParam?: string;
|
||||
+6
-5
@@ -2,13 +2,14 @@ import { useMemo, useRef, useState, useCallback, useImperativeHandle, useEffect
|
||||
import { useVirtualizer } from '@tanstack/react-virtual';
|
||||
import type { ParsedDiff } from '@diffity/parser';
|
||||
import { FileBlock, LARGE_DIFF_LINE_THRESHOLD } from './file-block';
|
||||
import { GeneralComments } from './general-comments';
|
||||
import { useHighlighter } from '../hooks/use-highlighter';
|
||||
import { type ViewMode, getFilePath } from '../lib/diff-utils';
|
||||
import type { CommentThread, LineSelection } from '../types/comment';
|
||||
import type { CommentActions } from '../hooks/use-comment-actions';
|
||||
import { GeneralComments } from '../comments/general-comments';
|
||||
import { useHighlighter } from '../../hooks/use-highlighter';
|
||||
import { type ViewMode, getFilePath } from '../../lib/diff-utils';
|
||||
import type { CommentThread, LineSelection } from '../comments/types';
|
||||
import type { CommentActions } from '../../hooks/use-comment-actions';
|
||||
|
||||
function flashThreadElement(element: Element) {
|
||||
element.dispatchEvent(new CustomEvent('diffity:focus-thread', { bubbles: false }));
|
||||
element.classList.remove('flash-thread');
|
||||
void (element as HTMLElement).offsetWidth;
|
||||
element.classList.add('flash-thread');
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import { ArrowUpIcon } from './icons/arrow-up-icon';
|
||||
import { ArrowDownIcon } from './icons/arrow-down-icon';
|
||||
import { Spinner } from './icons/spinner';
|
||||
import { ArrowUpIcon } from '../icons/arrow-up-icon';
|
||||
import { ArrowDownIcon } from '../icons/arrow-down-icon';
|
||||
import { Spinner } from '../icons/spinner';
|
||||
|
||||
interface ExpandRowProps {
|
||||
position: 'top' | 'bottom';
|
||||
+22
-22
@@ -2,29 +2,29 @@ import { useState, useEffect, useMemo, useCallback } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import type { DiffHunk } from '@diffity/parser';
|
||||
import type { DiffFile, DiffLine as DiffLineType } from '@diffity/parser';
|
||||
import type { SyntaxToken } from '../lib/syntax-token';
|
||||
import type { HighlightedTokens } from '../hooks/use-highlighter';
|
||||
import type { CommentSide, LineSelection } from '../types/comment';
|
||||
import { type ViewMode, getFilePath, buildChangeGroupPatch, extractLinesFromDiff, extractLinesFromExpandedLines } from '../lib/diff-utils';
|
||||
import { revertHunk as apiRevertHunk } from '../lib/api';
|
||||
import { ConfirmDialog } from './ui/confirm-dialog';
|
||||
import { computeGaps, createContextLines, getExpandRange, type ExpandableGap } from '../lib/context-expansion';
|
||||
import { fileContentOptions } from '../queries/file';
|
||||
import type { CommentActions } from '../hooks/use-comment-actions';
|
||||
import type { CommentThread } from '../types/comment';
|
||||
import { GENERAL_THREAD_FILE_PATH, DEFAULT_AUTHOR } from '../types/comment';
|
||||
import { useLineSelection } from '../hooks/use-line-selection';
|
||||
import { useCopy } from '../hooks/use-copy';
|
||||
import { CopyIcon } from './icons/copy-icon';
|
||||
import { CheckIcon } from './icons/check-icon';
|
||||
import { CommentIcon } from './icons/comment-icon';
|
||||
import type { SyntaxToken } from '../../lib/syntax-token';
|
||||
import type { HighlightedTokens } from '../../hooks/use-highlighter';
|
||||
import type { CommentSide, LineSelection } from '../comments/types';
|
||||
import { type ViewMode, getFilePath, buildChangeGroupPatch, extractLinesFromDiff, extractLinesFromExpandedLines } from '../../lib/diff-utils';
|
||||
import { revertHunk as apiRevertHunk } from '../../lib/api';
|
||||
import { ConfirmDialog } from '../ui/confirm-dialog';
|
||||
import { computeGaps, createContextLines, getExpandRange, type ExpandableGap } from '../../lib/context-expansion';
|
||||
import { fileContentOptions } from '../../queries/file';
|
||||
import type { CommentActions } from '../../hooks/use-comment-actions';
|
||||
import type { CommentThread } from '../comments/types';
|
||||
import { GENERAL_THREAD_FILE_PATH, DEFAULT_AUTHOR } from '../comments/types';
|
||||
import { useLineSelection } from '../../hooks/use-line-selection';
|
||||
import { useCopy } from '../../hooks/use-copy';
|
||||
import { CopyIcon } from '../icons/copy-icon';
|
||||
import { CheckIcon } from '../icons/check-icon';
|
||||
import { CommentIcon } from '../icons/comment-icon';
|
||||
import { DiffStats } from './diff-stats';
|
||||
import { Badge } from './ui/badge';
|
||||
import { IconButton } from './ui/icon-button';
|
||||
import { StatusBadge } from './ui/status-badge';
|
||||
import { Badge } from '../ui/badge';
|
||||
import { IconButton } from '../ui/icon-button';
|
||||
import { StatusBadge } from '../ui/status-badge';
|
||||
import { HunkWithGap } from './hunk-with-gap';
|
||||
import { OrphanedThreads } from './orphaned-threads';
|
||||
import { ThreadBadge } from './ui/thread-badge';
|
||||
import { OrphanedThreads } from '../comments/orphaned-threads';
|
||||
import { ThreadBadge } from '../ui/thread-badge';
|
||||
import { buildExpansionSyntaxMap, renderExpansionRows } from './render-expansion-rows';
|
||||
import { ExpandRow } from './expand-row';
|
||||
|
||||
@@ -119,7 +119,7 @@ export function FileBlock(props: FileBlockProps) {
|
||||
return extractLinesFromExpandedLines(allExpandedLines, side, startLine, endLine);
|
||||
}, [file.hunks, allExpandedLines]);
|
||||
|
||||
const addThread = useCallback((fp: string, side: CommentSide, startLine: number, endLine: number, body: string, author: import('../types/comment').CommentAuthor) => {
|
||||
const addThread = useCallback((fp: string, side: CommentSide, startLine: number, endLine: number, body: string, author: import('../comments/types').CommentAuthor) => {
|
||||
let anchorContent = extractLinesFromDiff(file.hunks, side, startLine, endLine);
|
||||
if (!anchorContent) {
|
||||
anchorContent = extractLinesFromExpandedLines(allExpandedLines, side, startLine, endLine);
|
||||
+9
-9
@@ -1,15 +1,15 @@
|
||||
import { useState, useMemo } from 'react';
|
||||
import type { DiffHunk, DiffLine as DiffLineType } from '@diffity/parser';
|
||||
import { cn } from '../lib/cn';
|
||||
import { getLineBg, getChangeGroups } from '../lib/diff-utils';
|
||||
import { renderContent } from '../lib/render-content';
|
||||
import type { SyntaxToken } from '../lib/syntax-token';
|
||||
import type { CommentThread as CommentThreadType, CommentAuthor, CommentSide, LineSelection, LineRenderProps } from '../types/comment';
|
||||
import { cn } from '../../lib/cn';
|
||||
import { getLineBg, getChangeGroups } from '../../lib/diff-utils';
|
||||
import { renderContent } from '../../lib/render-content';
|
||||
import type { SyntaxToken } from '../../lib/syntax-token';
|
||||
import type { CommentThread as CommentThreadType, CommentAuthor, CommentSide, LineSelection, LineRenderProps } from '../comments/types';
|
||||
import { HunkHeader, type ExpandControls } from './hunk-header';
|
||||
import { CommentLineNumber } from './comment-line-number';
|
||||
import { CommentThread } from './comment-thread';
|
||||
import { CommentFormRow } from './comment-form-row';
|
||||
import { UndoIcon } from './icons/undo-icon';
|
||||
import { CommentLineNumber } from '../comments/comment-line-number';
|
||||
import { CommentThread } from '../comments/comment-thread';
|
||||
import { CommentFormRow } from '../comments/comment-form-row';
|
||||
import { UndoIcon } from '../icons/undo-icon';
|
||||
|
||||
interface HunkBlockSplitProps {
|
||||
hunk: DiffHunk;
|
||||
+6
-6
@@ -1,13 +1,13 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { DiffHunk, DiffLine as DiffLineType } from '@diffity/parser';
|
||||
import type { SyntaxToken } from '../lib/syntax-token';
|
||||
import type { CommentThread as CommentThreadType, CommentAuthor, CommentSide, LineSelection, LineRenderProps } from '../types/comment';
|
||||
import { getChangeGroups } from '../lib/diff-utils';
|
||||
import type { SyntaxToken } from '../../lib/syntax-token';
|
||||
import type { CommentThread as CommentThreadType, CommentAuthor, CommentSide, LineSelection, LineRenderProps } from '../comments/types';
|
||||
import { getChangeGroups } from '../../lib/diff-utils';
|
||||
import { DiffLine } from './diff-line';
|
||||
import { HunkHeader, type ExpandControls } from './hunk-header';
|
||||
import { CommentThread } from './comment-thread';
|
||||
import { CommentFormRow } from './comment-form-row';
|
||||
import { UndoIcon } from './icons/undo-icon';
|
||||
import { CommentThread } from '../comments/comment-thread';
|
||||
import { CommentFormRow } from '../comments/comment-form-row';
|
||||
import { UndoIcon } from '../icons/undo-icon';
|
||||
|
||||
interface HunkBlockProps {
|
||||
hunk: DiffHunk;
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
import type { DiffHunk } from '@diffity/parser';
|
||||
import { ArrowUpIcon } from './icons/arrow-up-icon';
|
||||
import { ArrowDownIcon } from './icons/arrow-down-icon';
|
||||
import { ChevronUpDownIcon } from './icons/chevron-up-down-icon';
|
||||
import { Spinner } from './icons/spinner';
|
||||
import { ArrowUpIcon } from '../icons/arrow-up-icon';
|
||||
import { ArrowDownIcon } from '../icons/arrow-down-icon';
|
||||
import { ChevronUpDownIcon } from '../icons/chevron-up-down-icon';
|
||||
import { Spinner } from '../icons/spinner';
|
||||
|
||||
export interface ExpandControls {
|
||||
position: 'top' | 'between' | 'bottom';
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import type { DiffHunk, DiffLine as DiffLineType } from '@diffity/parser';
|
||||
import type { HighlightedTokens } from '../hooks/use-highlighter';
|
||||
import type { ViewMode } from '../lib/diff-utils';
|
||||
import type { SyntaxToken } from '../lib/syntax-token';
|
||||
import type { HighlightedTokens } from '../../hooks/use-highlighter';
|
||||
import type { ViewMode } from '../../lib/diff-utils';
|
||||
import type { SyntaxToken } from '../../lib/syntax-token';
|
||||
import type { ExpandControls } from './hunk-header';
|
||||
import type { CommentThread as CommentThreadType, CommentAuthor, CommentSide, LineSelection } from '../types/comment';
|
||||
import type { CommentThread as CommentThreadType, CommentAuthor, CommentSide, LineSelection } from '../comments/types';
|
||||
import { HunkBlock } from './hunk-block';
|
||||
import { HunkBlockSplit } from './hunk-block-split';
|
||||
import { buildExpansionSyntaxMap, renderExpansionRows } from './render-expansion-rows';
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { cn } from '../lib/cn';
|
||||
import { cn } from '../../lib/cn';
|
||||
|
||||
interface LineNumberCellProps {
|
||||
lineNumber: number | null;
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
import type { DiffLine as DiffLineType } from '@diffity/parser';
|
||||
import type { HighlightedTokens } from '../hooks/use-highlighter';
|
||||
import type { SyntaxToken } from '../lib/syntax-token';
|
||||
import type { ViewMode } from '../lib/diff-utils';
|
||||
import type { LineRenderProps } from '../types/comment';
|
||||
import type { HighlightedTokens } from '../../hooks/use-highlighter';
|
||||
import type { SyntaxToken } from '../../lib/syntax-token';
|
||||
import type { ViewMode } from '../../lib/diff-utils';
|
||||
import type { LineRenderProps } from '../comments/types';
|
||||
import { renderLineWithComments } from './hunk-block';
|
||||
import { renderSplitRows } from './hunk-block-split';
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import type { DiffLine } from '@diffity/parser';
|
||||
import type { SyntaxToken } from '../lib/syntax-token';
|
||||
import type { SyntaxToken } from '../../lib/syntax-token';
|
||||
|
||||
interface WordDiffProps {
|
||||
line: DiffLine;
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback, useRef } from 'react';
|
||||
import { type Commit, fetchCommits } from '../lib/api';
|
||||
import { type Commit, fetchCommits } from '../../lib/api';
|
||||
|
||||
interface CommitListProps {
|
||||
initialCommits: Commit[];
|
||||
+4
-4
@@ -1,9 +1,9 @@
|
||||
import { useOverview } from '../hooks/use-overview';
|
||||
import { useCommits } from '../hooks/use-commits';
|
||||
import { useInfo } from '../hooks/use-info';
|
||||
import { useOverview } from '../../hooks/use-overview';
|
||||
import { useCommits } from '../../hooks/use-commits';
|
||||
import { useInfo } from '../../hooks/use-info';
|
||||
import { OverviewFileList } from './overview-file-list';
|
||||
import { CommitList } from './commit-list';
|
||||
import { CheckCircleIcon } from './icons/check-circle-icon';
|
||||
import { CheckCircleIcon } from '../icons/check-circle-icon';
|
||||
import { PageLoader } from './skeleton';
|
||||
|
||||
interface DashboardProps {
|
||||
+7
-7
@@ -2,13 +2,13 @@ import { useState, useEffect } from 'react';
|
||||
import dayjs from 'dayjs';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
import { toast } from 'sonner';
|
||||
import { GitHubIcon } from './icons/github-icon';
|
||||
import { UploadIcon } from './icons/upload-icon';
|
||||
import { DownloadIcon } from './icons/download-icon';
|
||||
import { XIcon } from './icons/x-icon';
|
||||
import { pushCommentsToGitHub, pullCommentsFromGitHub, type GitHubDetails, type PrCommentPayload } from '../lib/api';
|
||||
import type { CommentThread } from '../types/comment';
|
||||
import { GENERAL_THREAD_FILE_PATH, isThreadResolved } from '../types/comment';
|
||||
import { GitHubIcon } from '../icons/github-icon';
|
||||
import { UploadIcon } from '../icons/upload-icon';
|
||||
import { DownloadIcon } from '../icons/download-icon';
|
||||
import { XIcon } from '../icons/x-icon';
|
||||
import { pushCommentsToGitHub, pullCommentsFromGitHub, type GitHubDetails, type PrCommentPayload } from '../../lib/api';
|
||||
import type { CommentThread } from '../comments/types';
|
||||
import { GENERAL_THREAD_FILE_PATH, isThreadResolved } from '../comments/types';
|
||||
|
||||
dayjs.extend(relativeTime);
|
||||
|
||||
+2
-2
@@ -2,8 +2,8 @@ import { useMemo } from 'react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import type { Components } from 'react-markdown';
|
||||
import { useHighlighter } from '../hooks/use-highlighter';
|
||||
import { getTheme } from '../hooks/use-theme';
|
||||
import { useHighlighter } from '../../hooks/use-highlighter';
|
||||
import { getTheme } from '../../hooks/use-theme';
|
||||
|
||||
interface MarkdownContentProps {
|
||||
content: string;
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
import { useState, useRef, useEffect, type ReactNode } from 'react';
|
||||
import { SunIcon } from './icons/sun-icon';
|
||||
import { MoonIcon } from './icons/moon-icon';
|
||||
import { EllipsisIcon } from './icons/ellipsis-icon';
|
||||
import { GitHubIcon } from './icons/github-icon';
|
||||
import { SunIcon } from '../icons/sun-icon';
|
||||
import { MoonIcon } from '../icons/moon-icon';
|
||||
import { EllipsisIcon } from '../icons/ellipsis-icon';
|
||||
import { GitHubIcon } from '../icons/github-icon';
|
||||
|
||||
export const menuItemClass = 'flex items-center gap-2.5 w-full px-3 py-1.5 text-xs text-text-secondary hover:bg-hover hover:text-text transition-colors cursor-pointer text-left';
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { OverviewFile } from '../lib/api';
|
||||
import type { OverviewFile } from '../../lib/api';
|
||||
|
||||
interface OverviewFileListProps {
|
||||
files: OverviewFile[];
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { XIcon } from './icons/x-icon';
|
||||
import { XIcon } from '../icons/x-icon';
|
||||
|
||||
interface ShortcutModalProps {
|
||||
onClose: () => void;
|
||||
+8
-8
@@ -1,13 +1,13 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { DiffFile } from '@diffity/parser';
|
||||
import { FileTree } from './file-tree';
|
||||
import type { FileTreeHandle } from './file-tree';
|
||||
import { SidebarIcon } from './icons/sidebar-icon';
|
||||
import { SearchIcon } from './icons/search-icon';
|
||||
import { XIcon } from './icons/x-icon';
|
||||
import { CommentIcon } from './icons/comment-icon';
|
||||
import { CollapseAllIcon } from './icons/collapse-all-icon';
|
||||
import { ExpandAllIcon } from './icons/expand-all-icon';
|
||||
import { FileTree } from '../tree/file-tree';
|
||||
import type { FileTreeHandle } from '../tree/file-tree';
|
||||
import { SidebarIcon } from '../icons/sidebar-icon';
|
||||
import { SearchIcon } from '../icons/search-icon';
|
||||
import { XIcon } from '../icons/x-icon';
|
||||
import { CommentIcon } from '../icons/comment-icon';
|
||||
import { CollapseAllIcon } from '../icons/collapse-all-icon';
|
||||
import { ExpandAllIcon } from '../icons/expand-all-icon';
|
||||
|
||||
interface SidebarProps {
|
||||
files: DiffFile[];
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { Spinner } from './icons/spinner';
|
||||
import { Spinner } from '../icons/spinner';
|
||||
|
||||
export function PageLoader() {
|
||||
return (
|
||||
+3
-2
@@ -1,14 +1,15 @@
|
||||
interface StaleDiffBannerProps {
|
||||
onRefresh: () => void;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export function StaleDiffBanner(props: StaleDiffBannerProps) {
|
||||
const { onRefresh } = props;
|
||||
const { onRefresh, message = 'Files have changed since this diff was loaded' } = props;
|
||||
|
||||
return (
|
||||
<div className="sticky top-0 z-30 flex items-center justify-center gap-3 px-4 py-1.5 bg-accent/10 border-b border-accent/20 text-xs animate-slide-down">
|
||||
<span className="text-accent font-medium">
|
||||
Files have changed since this diff was loaded
|
||||
{message}
|
||||
</span>
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import type { ParsedDiff } from '@diffity/parser';
|
||||
import { DiffStats } from './diff-stats';
|
||||
import { GitBranchIcon } from './icons/git-branch-icon';
|
||||
import { DiffStats } from '../diff/diff-stats';
|
||||
import { GitBranchIcon } from '../icons/git-branch-icon';
|
||||
|
||||
interface SummaryBarProps {
|
||||
diff: ParsedDiff | null;
|
||||
+15
-15
@@ -1,22 +1,22 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import type { ParsedDiff } from '@diffity/parser';
|
||||
import { cn } from '../lib/cn';
|
||||
import { getFilePath } from '../lib/diff-utils';
|
||||
import { UnifiedViewIcon } from './icons/unified-view-icon';
|
||||
import { SplitViewIcon } from './icons/split-view-icon';
|
||||
import { EyeIcon } from './icons/eye-icon';
|
||||
import { EyeOffIcon } from './icons/eye-off-icon';
|
||||
import { KeyboardIcon } from './icons/keyboard-icon';
|
||||
import { GitBranchIcon } from './icons/git-branch-icon';
|
||||
import { GitHubIcon } from './icons/github-icon';
|
||||
import { DiffStats } from './diff-stats';
|
||||
import { cn } from '../../lib/cn';
|
||||
import { getFilePath } from '../../lib/diff-utils';
|
||||
import { UnifiedViewIcon } from '../icons/unified-view-icon';
|
||||
import { SplitViewIcon } from '../icons/split-view-icon';
|
||||
import { EyeIcon } from '../icons/eye-icon';
|
||||
import { EyeOffIcon } from '../icons/eye-off-icon';
|
||||
import { KeyboardIcon } from '../icons/keyboard-icon';
|
||||
import { GitBranchIcon } from '../icons/git-branch-icon';
|
||||
import { GitHubIcon } from '../icons/github-icon';
|
||||
import { DiffStats } from '../diff/diff-stats';
|
||||
import { GitHubDialog } from './github-dialog';
|
||||
import { CommentToolbarActions } from './comment-toolbar-actions';
|
||||
import { CommentToolbarActions } from '../comments/comment-toolbar-actions';
|
||||
import { OptionsMenu, menuItemClass } from './options-menu';
|
||||
import { GENERAL_THREAD_FILE_PATH } from '../types/comment';
|
||||
import type { ViewMode } from '../lib/diff-utils';
|
||||
import type { CommentThread } from '../types/comment';
|
||||
import { isThreadResolved } from '../types/comment';
|
||||
import { GENERAL_THREAD_FILE_PATH } from '../comments/types';
|
||||
import type { ViewMode } from '../../lib/diff-utils';
|
||||
import type { CommentThread } from '../comments/types';
|
||||
import { isThreadResolved } from '../comments/types';
|
||||
|
||||
interface ToolbarProps {
|
||||
viewMode: ViewMode;
|
||||
+7
-7
@@ -1,10 +1,10 @@
|
||||
import type { TreeNode } from '../lib/file-tree';
|
||||
import { cn } from '../lib/cn';
|
||||
import { StatusBadge } from './ui/status-badge';
|
||||
import { ChevronIcon } from './icons/chevron-icon';
|
||||
import { FolderIcon } from './icons/folder-icon';
|
||||
import { FileIcon } from './icons/file-icon';
|
||||
import { CommentIcon } from './icons/comment-icon';
|
||||
import type { TreeNode } from '../../lib/file-tree';
|
||||
import { cn } from '../../lib/cn';
|
||||
import { StatusBadge } from '../ui/status-badge';
|
||||
import { ChevronIcon } from '../icons/chevron-icon';
|
||||
import { FolderIcon } from '../icons/folder-icon';
|
||||
import { FileIcon } from '../icons/file-icon';
|
||||
import { CommentIcon } from '../icons/comment-icon';
|
||||
|
||||
interface FileTreeItemProps {
|
||||
node: TreeNode;
|
||||
+1
-1
@@ -7,7 +7,7 @@ import {
|
||||
filterTree,
|
||||
filterTreeToPaths,
|
||||
collectAllDirPaths,
|
||||
} from '../lib/file-tree';
|
||||
} from '../../lib/file-tree';
|
||||
import { FileTreeItem } from './file-tree-item';
|
||||
|
||||
interface FileTreeProps {
|
||||
+8
-8
@@ -1,12 +1,12 @@
|
||||
import { useMemo, useState, useCallback } from 'react';
|
||||
import { useHighlighter } from '../hooks/use-highlighter';
|
||||
import { useLineSelection } from '../hooks/use-line-selection';
|
||||
import type { CommentThread as CommentThreadType, CommentAuthor, LineSelection } from '../types/comment';
|
||||
import type { CommentActions } from '../hooks/use-comment-actions';
|
||||
import { CommentThread } from './comment-thread';
|
||||
import { CommentForm } from './comment-form';
|
||||
import { CommentLineNumber } from './comment-line-number';
|
||||
import { cn } from '../lib/cn';
|
||||
import { useHighlighter } from '../../hooks/use-highlighter';
|
||||
import { useLineSelection } from '../../hooks/use-line-selection';
|
||||
import type { CommentThread as CommentThreadType, CommentAuthor, LineSelection } from '../comments/types';
|
||||
import type { CommentActions } from '../../hooks/use-comment-actions';
|
||||
import { CommentThread } from '../comments/comment-thread';
|
||||
import { CommentForm } from '../comments/comment-form';
|
||||
import { CommentLineNumber } from '../comments/comment-line-number';
|
||||
import { cn } from '../../lib/cn';
|
||||
|
||||
interface FileViewerProps {
|
||||
filePath: string;
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
import type { TreeEntryResponse } from '../lib/api';
|
||||
import { FileIcon } from './icons/file-icon';
|
||||
import { FolderIcon } from './icons/folder-icon';
|
||||
import type { TreeEntryResponse } from '../../lib/api';
|
||||
import { FileIcon } from '../icons/file-icon';
|
||||
import { FolderIcon } from '../icons/folder-icon';
|
||||
|
||||
interface FolderViewerProps {
|
||||
entries: TreeEntryResponse[];
|
||||
+33
-14
@@ -1,21 +1,23 @@
|
||||
import { useState, useMemo, useCallback, useEffect, useRef } from 'react';
|
||||
import { useQuery, useQueryClient, keepPreviousData } from '@tanstack/react-query';
|
||||
import NProgress from 'nprogress';
|
||||
import { treePathsOptions, treeInfoOptions, treeFileContentOptions, treeEntriesOptions } from '../queries/tree';
|
||||
import { useTheme } from '../hooks/use-theme';
|
||||
import { useReviewThreads } from '../hooks/use-review-threads';
|
||||
import { useCommentActions } from '../hooks/use-comment-actions';
|
||||
import { isThreadResolved, GENERAL_THREAD_FILE_PATH } from '../types/comment';
|
||||
import type { CommentThread } from '../types/comment';
|
||||
import type { CommentAuthor } from '../types/comment';
|
||||
import { treePathsOptions, treeInfoOptions, treeFileContentOptions, treeEntriesOptions } from '../../queries/tree';
|
||||
import { useTheme } from '../../hooks/use-theme';
|
||||
import { useReviewThreads } from '../../hooks/use-review-threads';
|
||||
import { useCommentActions } from '../../hooks/use-comment-actions';
|
||||
import { isThreadResolved, GENERAL_THREAD_FILE_PATH } from '../comments/types';
|
||||
import type { CommentThread } from '../comments/types';
|
||||
import type { CommentAuthor } from '../comments/types';
|
||||
import { TreeSidebar } from './tree-sidebar';
|
||||
import { FolderViewer } from './folder-viewer';
|
||||
import { FileViewer } from './file-viewer';
|
||||
import { PathComments } from './path-comments';
|
||||
import { CommentToolbarActions } from './comment-toolbar-actions';
|
||||
import { PageLoader } from './skeleton';
|
||||
import { OptionsMenu } from './options-menu';
|
||||
import { GitBranchIcon } from './icons/git-branch-icon';
|
||||
import { PathComments } from '../comments/path-comments';
|
||||
import { CommentToolbarActions } from '../comments/comment-toolbar-actions';
|
||||
import { PageLoader } from '../layout/skeleton';
|
||||
import { OptionsMenu } from '../layout/options-menu';
|
||||
import { GitBranchIcon } from '../icons/git-branch-icon';
|
||||
import { StaleDiffBanner } from '../layout/stale-diff-banner';
|
||||
import { useTreeStaleness } from '../../hooks/use-tree-staleness';
|
||||
|
||||
interface TreePageProps {
|
||||
initialTheme?: 'light' | 'dark' | null;
|
||||
@@ -90,8 +92,10 @@ export function TreePage(props: TreePageProps) {
|
||||
const { initialTheme } = props;
|
||||
const { theme, toggleTheme } = useTheme(initialTheme);
|
||||
const queryClient = useQueryClient();
|
||||
const { isStale, resetStaleness } = useTreeStaleness();
|
||||
|
||||
const [nav, setNav] = useState(getPathFromUrl);
|
||||
const [focusedThreadId, setFocusedThreadId] = useState<string | null>(null);
|
||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||
const mainRef = useRef<HTMLElement>(null);
|
||||
const nprogressActive = useRef(false);
|
||||
@@ -199,6 +203,7 @@ export function TreePage(props: TreePageProps) {
|
||||
const scrollToThreadElement = useCallback((threadId: string) => {
|
||||
const el = document.querySelector(`[data-thread-id="${threadId}"]`);
|
||||
if (el) {
|
||||
el.dispatchEvent(new CustomEvent('diffity:focus-thread', { bubbles: false }));
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
el.classList.add('flash-thread');
|
||||
setTimeout(() => el.classList.remove('flash-thread'), 1500);
|
||||
@@ -211,9 +216,9 @@ export function TreePage(props: TreePageProps) {
|
||||
let targetType: 'file' | 'dir' = 'file';
|
||||
|
||||
if (isPathComment) {
|
||||
setFocusedThreadId(threadId);
|
||||
const rawPath = filePath.slice('__path__:'.length);
|
||||
targetPath = rawPath === '__root__' ? '' : rawPath;
|
||||
// Determine if this path is a file or directory
|
||||
const isFile = paths.includes(targetPath);
|
||||
targetType = isFile ? 'file' : 'dir';
|
||||
}
|
||||
@@ -228,7 +233,6 @@ export function TreePage(props: TreePageProps) {
|
||||
}
|
||||
updateUrl(targetPath, targetType);
|
||||
setNav({ path: targetPath, type: targetType });
|
||||
// Wait for React to render with the new data
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
scrollToThreadElement(threadId);
|
||||
@@ -240,6 +244,13 @@ export function TreePage(props: TreePageProps) {
|
||||
scrollToThreadElement(threadId);
|
||||
}, [nav, queryClient, paths, scrollToThreadElement]);
|
||||
|
||||
const handleRefreshTree = useCallback(() => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tree-paths'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['tree-entries'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['tree-file-content'] });
|
||||
resetStaleness();
|
||||
}, [queryClient, resetStaleness]);
|
||||
|
||||
const formatForCopy = useCallback(() => {
|
||||
return formatTreeThreadsForCopy(threads);
|
||||
}, [threads]);
|
||||
@@ -296,6 +307,13 @@ export function TreePage(props: TreePageProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isStale && (
|
||||
<StaleDiffBanner
|
||||
onRefresh={handleRefreshTree}
|
||||
message="Files have changed since this tree was loaded"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<TreeSidebar
|
||||
ref={searchInputRef}
|
||||
@@ -312,6 +330,7 @@ export function TreePage(props: TreePageProps) {
|
||||
threads={pathThreads}
|
||||
commentActions={commentActions}
|
||||
label={nav.path ? nav.path.split('/').pop()! : info?.name ?? 'root'}
|
||||
focusedThreadId={focusedThreadId}
|
||||
>
|
||||
<button
|
||||
className={breadcrumbs.length > 0 ? 'text-accent hover:underline cursor-pointer' : 'text-text font-medium'}
|
||||
+7
-7
@@ -6,14 +6,14 @@ import {
|
||||
filterTree,
|
||||
filterTreeToPaths,
|
||||
collectAllDirPaths,
|
||||
} from '../lib/file-tree';
|
||||
} from '../../lib/file-tree';
|
||||
import { FileTreeItem } from './file-tree-item';
|
||||
import { SidebarIcon } from './icons/sidebar-icon';
|
||||
import { SearchIcon } from './icons/search-icon';
|
||||
import { XIcon } from './icons/x-icon';
|
||||
import { CommentIcon } from './icons/comment-icon';
|
||||
import { CollapseAllIcon } from './icons/collapse-all-icon';
|
||||
import { ExpandAllIcon } from './icons/expand-all-icon';
|
||||
import { SidebarIcon } from '../icons/sidebar-icon';
|
||||
import { SearchIcon } from '../icons/search-icon';
|
||||
import { XIcon } from '../icons/x-icon';
|
||||
import { CommentIcon } from '../icons/comment-icon';
|
||||
import { CollapseAllIcon } from '../icons/collapse-all-icon';
|
||||
import { ExpandAllIcon } from '../icons/expand-all-icon';
|
||||
|
||||
interface TreeSidebarProps {
|
||||
paths: string[];
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import type { CommentAuthor, CommentSide } from '../types/comment';
|
||||
import type { CommentAuthor, CommentSide } from '../components/comments/types';
|
||||
import * as api from '../lib/api';
|
||||
|
||||
export function useCommentActions(sessionId: string | null, enabled: boolean) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import type { CommentSide, LineSelection } from '../types/comment';
|
||||
import type { CommentSide, LineSelection } from '../components/comments/types';
|
||||
|
||||
interface UseLineSelectionOptions {
|
||||
filePath: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchThreads } from '../lib/api';
|
||||
import type { CommentThread } from '../types/comment';
|
||||
import type { CommentThread } from '../components/comments/types';
|
||||
|
||||
export function useReviewThreads(sessionId: string | null | undefined) {
|
||||
return useQuery<CommentThread[]>({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback, useMemo } from 'react';
|
||||
import type { CommentThread } from '../types/comment';
|
||||
import type { CommentThread } from '../components/comments/types';
|
||||
import { getUnresolvedFileThreads } from '../lib/comment-navigation';
|
||||
|
||||
export function useThreadNavigation(threads: CommentThread[], onScrollToThread: (threadId: string, filePath: string) => void) {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { fetchTreeFingerprint } from '../lib/api';
|
||||
|
||||
const POLL_INTERVAL = 3000;
|
||||
|
||||
export function useTreeStaleness() {
|
||||
const [isStale, setIsStale] = useState(false);
|
||||
const baselineRef = useRef<string | null>(null);
|
||||
|
||||
function resetStaleness() {
|
||||
baselineRef.current = null;
|
||||
setIsStale(false);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
let timer: ReturnType<typeof setTimeout>;
|
||||
let cancelled = false;
|
||||
|
||||
async function poll() {
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const fingerprint = await fetchTreeFingerprint();
|
||||
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (baselineRef.current === null) {
|
||||
baselineRef.current = fingerprint;
|
||||
} else if (fingerprint !== baselineRef.current) {
|
||||
setIsStale(true);
|
||||
}
|
||||
} catch {
|
||||
// ignore fetch errors
|
||||
}
|
||||
|
||||
if (!cancelled) {
|
||||
timer = setTimeout(poll, POLL_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
poll();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { isStale, resetStaleness };
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ParsedDiff } from '@diffity/parser';
|
||||
import type { CommentThread, CommentAuthor, CommentSide, Comment } from '../types/comment';
|
||||
import type { CommentThread, CommentAuthor, CommentSide, Comment } from '../components/comments/types';
|
||||
|
||||
async function apiFetch<T>(url: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(url, init);
|
||||
@@ -260,3 +260,15 @@ export function fetchTreeEntries(dirPath?: string): Promise<{ entries: TreeEntry
|
||||
export function fetchTreeInfo(): Promise<RepoInfo> {
|
||||
return apiFetch('/api/tree/info');
|
||||
}
|
||||
|
||||
export async function fetchTreeFingerprint(): Promise<string> {
|
||||
const json = await apiFetch<{ fingerprint: string }>('/api/tree/fingerprint');
|
||||
return json.fingerprint;
|
||||
}
|
||||
|
||||
export async function fetchTreeFileContent(filePath: string): Promise<string[]> {
|
||||
const json = await apiFetch<{ content: string[] }>(
|
||||
`/api/tree/file/${encodeURIComponent(filePath)}`,
|
||||
);
|
||||
return json.content;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { CommentSide, CommentThread } from '../types/comment';
|
||||
import { GENERAL_THREAD_FILE_PATH, isThreadResolved } from '../types/comment';
|
||||
import type { CommentSide, CommentThread } from '../components/comments/types';
|
||||
import { GENERAL_THREAD_FILE_PATH, isThreadResolved } from '../components/comments/types';
|
||||
|
||||
export function getUnresolvedFileThreads(threads: CommentThread[]): CommentThread[] {
|
||||
return threads.filter(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { DiffFile, DiffHunk } from '@diffity/parser';
|
||||
import type { CommentSide } from '../types/comment';
|
||||
import type { CommentSide } from '../components/comments/types';
|
||||
|
||||
export type ViewMode = 'unified' | 'split';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { DiffLine } from '@diffity/parser';
|
||||
import { WordDiff } from '../components/word-diff';
|
||||
import { WordDiff } from '../components/diff/word-diff';
|
||||
import type { SyntaxToken } from '../lib/syntax-token';
|
||||
|
||||
export function renderContent(line: DiffLine, syntaxTokens?: SyntaxToken[]) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
import { fetchTreePaths, fetchTreeEntries, fetchTreeInfo, fetchFileContent } from '../lib/api';
|
||||
import { fetchTreePaths, fetchTreeEntries, fetchTreeInfo, fetchTreeFileContent } from '../lib/api';
|
||||
|
||||
export function treePathsOptions() {
|
||||
return queryOptions({
|
||||
@@ -28,7 +28,7 @@ export function treeInfoOptions() {
|
||||
export function treeFileContentOptions(filePath: string) {
|
||||
return queryOptions({
|
||||
queryKey: ['tree-file-content', filePath],
|
||||
queryFn: () => fetchFileContent(filePath),
|
||||
queryFn: () => fetchTreeFileContent(filePath),
|
||||
staleTime: 30_000,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { join } from 'path';
|
||||
import matter from 'gray-matter';
|
||||
import { writeFile, type Skill, type TransformOptions } from '../utils.js';
|
||||
|
||||
export function transform(skill: Skill, outputDir: string, options: TransformOptions): void {
|
||||
const body = skill.content.replaceAll('{{binary}}', options.binary);
|
||||
const data = {
|
||||
name: skill.data.name,
|
||||
description: skill.data.description,
|
||||
};
|
||||
if (options.namePrefix) {
|
||||
data.name = data.name.replace('diffity-', `${options.namePrefix}-`);
|
||||
}
|
||||
const content = matter.stringify(body, data);
|
||||
const outputPath = join(outputDir, '.codex', 'skills', skill.name, 'SKILL.md');
|
||||
writeFile(outputPath, content);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { join } from 'path';
|
||||
import matter from 'gray-matter';
|
||||
import { writeFile, type Skill, type TransformOptions } from '../utils.js';
|
||||
|
||||
export function transform(skill: Skill, outputDir: string, options: TransformOptions): void {
|
||||
const body = skill.content.replaceAll('{{binary}}', options.binary);
|
||||
const data = {
|
||||
name: skill.data.name,
|
||||
description: skill.data.description,
|
||||
};
|
||||
if (options.namePrefix) {
|
||||
data.name = data.name.replace('diffity-', `${options.namePrefix}-`);
|
||||
}
|
||||
const content = matter.stringify(body, data);
|
||||
const outputPath = join(outputDir, '.cursor', 'skills', skill.name, 'SKILL.md');
|
||||
writeFile(outputPath, content);
|
||||
}
|
||||
@@ -1,3 +1 @@
|
||||
export { transform as claudeCode } from './claude-code.js';
|
||||
export { transform as cursor } from './cursor.js';
|
||||
export { transform as codex } from './codex.js';
|
||||
|
||||
+26
-8
@@ -5,15 +5,23 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dirname, '..');
|
||||
const cliPkgPath = resolve(root, 'packages/cli/package.json');
|
||||
const lockPath = resolve(root, 'package-lock.json');
|
||||
|
||||
const packagePaths = [
|
||||
'packages/cli',
|
||||
'packages/git',
|
||||
'packages/github',
|
||||
'packages/parser',
|
||||
'packages/ui',
|
||||
];
|
||||
|
||||
const bump = process.argv[2] as 'patch' | 'minor';
|
||||
if (bump !== 'patch' && bump !== 'minor') {
|
||||
console.error('Usage: tsx scripts/release.ts <patch|minor>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cliPkgPath = resolve(root, 'packages/cli/package.json');
|
||||
const cliPkg = JSON.parse(readFileSync(cliPkgPath, 'utf-8'));
|
||||
const [major, minor, patch] = cliPkg.version.split('.').map(Number);
|
||||
|
||||
@@ -24,16 +32,26 @@ const newVersion =
|
||||
|
||||
console.log(`${cliPkg.version} → ${newVersion}\n`);
|
||||
|
||||
cliPkg.version = newVersion;
|
||||
writeFileSync(cliPkgPath, JSON.stringify(cliPkg, null, 2) + '\n');
|
||||
const filesToStage: string[] = [];
|
||||
|
||||
const lockJson = JSON.parse(readFileSync(lockPath, 'utf-8'));
|
||||
if (lockJson.packages?.['packages/cli']) {
|
||||
lockJson.packages['packages/cli'].version = newVersion;
|
||||
writeFileSync(lockPath, JSON.stringify(lockJson, null, 2) + '\n');
|
||||
for (const pkgDir of packagePaths) {
|
||||
const pkgPath = resolve(root, pkgDir, 'package.json');
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
||||
pkg.version = newVersion;
|
||||
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
||||
filesToStage.push(`${pkgDir}/package.json`);
|
||||
}
|
||||
|
||||
execSync('git add packages/cli/package.json package-lock.json', { cwd: root, stdio: 'inherit' });
|
||||
const lockJson = JSON.parse(readFileSync(lockPath, 'utf-8'));
|
||||
for (const pkgDir of packagePaths) {
|
||||
if (lockJson.packages?.[pkgDir]) {
|
||||
lockJson.packages[pkgDir].version = newVersion;
|
||||
}
|
||||
}
|
||||
writeFileSync(lockPath, JSON.stringify(lockJson, null, 2) + '\n');
|
||||
filesToStage.push('package-lock.json');
|
||||
|
||||
execSync(`git add ${filesToStage.join(' ')}`, { cwd: root, stdio: 'inherit' });
|
||||
execSync(`git commit -m "chore: release v${newVersion}"`, { cwd: root, stdio: 'inherit' });
|
||||
execSync(`git tag v${newVersion}`, { cwd: root, stdio: 'inherit' });
|
||||
|
||||
|
||||
Reference in New Issue
Block a user