Refactor and fix comment navigation for files

This commit is contained in:
Kamran Ahmed
2026-03-25 05:27:37 +00:00
parent 6c9c27d62a
commit 13e56c5ca8
63 changed files with 473 additions and 343 deletions
-43
View File
@@ -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',
+2 -2
View File
@@ -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",
+25
View File
@@ -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__');
+1 -1
View File
@@ -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
View File
@@ -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');
}
+2 -2
View File
@@ -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();
@@ -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,4 +1,4 @@
import type { CommentAuthor, CommentSide } from '../types/comment';
import type { CommentAuthor, CommentSide } from './types';
import { CommentForm } from './comment-form';
interface CommentFormRowProps {
@@ -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;
@@ -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>;
}
@@ -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[];
@@ -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 {
@@ -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 {
@@ -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);
@@ -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;
@@ -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 };
@@ -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;
@@ -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');
@@ -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';
@@ -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);
@@ -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;
@@ -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;
@@ -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';
@@ -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,4 +1,4 @@
import { cn } from '../lib/cn';
import { cn } from '../../lib/cn';
interface LineNumberCellProps {
lineNumber: number | null;
@@ -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,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,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[];
@@ -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 {
@@ -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,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;
@@ -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,4 +1,4 @@
import type { OverviewFile } from '../lib/api';
import type { OverviewFile } from '../../lib/api';
interface OverviewFileListProps {
files: OverviewFile[];
@@ -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;
@@ -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,4 +1,4 @@
import { Spinner } from './icons/spinner';
import { Spinner } from '../icons/spinner';
export function PageLoader() {
return (
@@ -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}
@@ -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;
@@ -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;
@@ -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;
@@ -7,7 +7,7 @@ import {
filterTree,
filterTreeToPaths,
collectAllDirPaths,
} from '../lib/file-tree';
} from '../../lib/file-tree';
import { FileTreeItem } from './file-tree-item';
interface FileTreeProps {
@@ -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;
@@ -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[];
@@ -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'}
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 };
}
+13 -1
View File
@@ -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;
}
+2 -2
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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[]) {
+2 -2
View File
@@ -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,
});
}
-17
View File
@@ -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);
}
-17
View File
@@ -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);
}
-2
View File
@@ -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
View File
@@ -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' });