diffity 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +11 -0
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/development.md +156 -0
- package/package.json +32 -0
- package/packages/cli/build.js +38 -0
- package/packages/cli/package.json +51 -0
- package/packages/cli/src/agent.ts +187 -0
- package/packages/cli/src/db.ts +58 -0
- package/packages/cli/src/index.ts +196 -0
- package/packages/cli/src/review-routes.ts +150 -0
- package/packages/cli/src/server.ts +370 -0
- package/packages/cli/src/session.ts +48 -0
- package/packages/cli/src/threads.ts +238 -0
- package/packages/cli/tsconfig.json +13 -0
- package/packages/git/package.json +24 -0
- package/packages/git/src/commits.ts +28 -0
- package/packages/git/src/diff.ts +97 -0
- package/packages/git/src/exec.ts +35 -0
- package/packages/git/src/index.ts +5 -0
- package/packages/git/src/repo.ts +63 -0
- package/packages/git/src/status.ts +9 -0
- package/packages/git/src/types.ts +12 -0
- package/packages/git/tsconfig.json +9 -0
- package/packages/parser/package.json +26 -0
- package/packages/parser/src/index.ts +12 -0
- package/packages/parser/src/parse.ts +299 -0
- package/packages/parser/src/types.ts +52 -0
- package/packages/parser/src/word-diff.ts +155 -0
- package/packages/parser/tests/fixtures/binary-deleted.diff +4 -0
- package/packages/parser/tests/fixtures/binary-file.diff +4 -0
- package/packages/parser/tests/fixtures/binary-modified.diff +3 -0
- package/packages/parser/tests/fixtures/copied-file.diff +12 -0
- package/packages/parser/tests/fixtures/deleted-file.diff +9 -0
- package/packages/parser/tests/fixtures/empty.diff +0 -0
- package/packages/parser/tests/fixtures/hunk-with-context.diff +12 -0
- package/packages/parser/tests/fixtures/mode-change-with-content.diff +10 -0
- package/packages/parser/tests/fixtures/mode-change.diff +3 -0
- package/packages/parser/tests/fixtures/multi-file.diff +22 -0
- package/packages/parser/tests/fixtures/new-file.diff +9 -0
- package/packages/parser/tests/fixtures/no-newline.diff +10 -0
- package/packages/parser/tests/fixtures/renamed-file.diff +12 -0
- package/packages/parser/tests/fixtures/single-file-additions.diff +11 -0
- package/packages/parser/tests/fixtures/single-file-deletions.diff +11 -0
- package/packages/parser/tests/fixtures/single-file-mixed.diff +15 -0
- package/packages/parser/tests/fixtures/single-file-multi-hunk.diff +22 -0
- package/packages/parser/tests/fixtures/spaces-in-path.diff +9 -0
- package/packages/parser/tests/fixtures/submodule.diff +7 -0
- package/packages/parser/tests/fixtures/unicode-content.diff +11 -0
- package/packages/parser/tests/parse.test.ts +312 -0
- package/packages/parser/tests/word-diff-integration.test.ts +52 -0
- package/packages/parser/tests/word-diff.test.ts +121 -0
- package/packages/parser/tsconfig.json +10 -0
- package/packages/skills/diffity-resolve/SKILL.md +55 -0
- package/packages/skills/diffity-review/SKILL.md +74 -0
- package/packages/skills/diffity-start/SKILL.md +25 -0
- package/packages/ui/index.html +13 -0
- package/packages/ui/package.json +35 -0
- package/packages/ui/public/brand.svg +12 -0
- package/packages/ui/public/favicon.svg +15 -0
- package/packages/ui/src/app.tsx +14 -0
- package/packages/ui/src/components/comment-bubble.tsx +78 -0
- package/packages/ui/src/components/comment-form-row.tsx +58 -0
- package/packages/ui/src/components/comment-form.tsx +78 -0
- package/packages/ui/src/components/comment-line-number.tsx +60 -0
- package/packages/ui/src/components/comment-thread.tsx +209 -0
- package/packages/ui/src/components/commit-list.tsx +100 -0
- package/packages/ui/src/components/dashboard.tsx +84 -0
- package/packages/ui/src/components/diff-line.tsx +90 -0
- package/packages/ui/src/components/diff-page.tsx +332 -0
- package/packages/ui/src/components/diff-stats.tsx +20 -0
- package/packages/ui/src/components/diff-view.tsx +278 -0
- package/packages/ui/src/components/expand-row.tsx +45 -0
- package/packages/ui/src/components/file-block.tsx +536 -0
- package/packages/ui/src/components/file-tree-item.tsx +84 -0
- package/packages/ui/src/components/file-tree.tsx +72 -0
- package/packages/ui/src/components/general-comments.tsx +174 -0
- package/packages/ui/src/components/hunk-block-split.tsx +357 -0
- package/packages/ui/src/components/hunk-block.tsx +161 -0
- package/packages/ui/src/components/hunk-header.tsx +144 -0
- package/packages/ui/src/components/hunk-with-gap.tsx +113 -0
- package/packages/ui/src/components/icons/arrow-down-icon.tsx +7 -0
- package/packages/ui/src/components/icons/arrow-up-icon.tsx +7 -0
- package/packages/ui/src/components/icons/check-circle-icon.tsx +8 -0
- package/packages/ui/src/components/icons/check-icon.tsx +9 -0
- package/packages/ui/src/components/icons/chevron-down-icon.tsx +11 -0
- package/packages/ui/src/components/icons/chevron-icon.tsx +20 -0
- package/packages/ui/src/components/icons/chevron-up-down-icon.tsx +7 -0
- package/packages/ui/src/components/icons/chevron-up-icon.tsx +11 -0
- package/packages/ui/src/components/icons/comment-icon.tsx +9 -0
- package/packages/ui/src/components/icons/copy-icon.tsx +10 -0
- package/packages/ui/src/components/icons/eye-icon.tsx +10 -0
- package/packages/ui/src/components/icons/eye-off-icon.tsx +12 -0
- package/packages/ui/src/components/icons/file-icon.tsx +7 -0
- package/packages/ui/src/components/icons/folder-icon.tsx +19 -0
- package/packages/ui/src/components/icons/git-branch-icon.tsx +13 -0
- package/packages/ui/src/components/icons/keyboard-icon.tsx +13 -0
- package/packages/ui/src/components/icons/moon-icon.tsx +9 -0
- package/packages/ui/src/components/icons/plus-icon.tsx +9 -0
- package/packages/ui/src/components/icons/search-icon.tsx +10 -0
- package/packages/ui/src/components/icons/sidebar-icon.tsx +10 -0
- package/packages/ui/src/components/icons/spinner.tsx +7 -0
- package/packages/ui/src/components/icons/split-view-icon.tsx +10 -0
- package/packages/ui/src/components/icons/sun-icon.tsx +17 -0
- package/packages/ui/src/components/icons/trash-icon.tsx +11 -0
- package/packages/ui/src/components/icons/undo-icon.tsx +9 -0
- package/packages/ui/src/components/icons/unified-view-icon.tsx +12 -0
- package/packages/ui/src/components/icons/x-icon.tsx +10 -0
- package/packages/ui/src/components/line-number-cell.tsx +18 -0
- package/packages/ui/src/components/markdown-content.tsx +139 -0
- package/packages/ui/src/components/orphaned-threads.tsx +80 -0
- package/packages/ui/src/components/overview-file-list.tsx +57 -0
- package/packages/ui/src/components/render-expansion-rows.tsx +47 -0
- package/packages/ui/src/components/shortcut-modal.tsx +93 -0
- package/packages/ui/src/components/sidebar.tsx +80 -0
- package/packages/ui/src/components/skeleton.tsx +9 -0
- package/packages/ui/src/components/stale-diff-banner.tsx +21 -0
- package/packages/ui/src/components/summary-bar.tsx +39 -0
- package/packages/ui/src/components/toolbar.tsx +246 -0
- package/packages/ui/src/components/ui/badge.tsx +17 -0
- package/packages/ui/src/components/ui/confirm-dialog.tsx +52 -0
- package/packages/ui/src/components/ui/icon-button.tsx +23 -0
- package/packages/ui/src/components/ui/status-badge.tsx +57 -0
- package/packages/ui/src/components/ui/thread-badge.tsx +35 -0
- package/packages/ui/src/components/word-diff.tsx +126 -0
- package/packages/ui/src/hooks/use-comment-actions.ts +97 -0
- package/packages/ui/src/hooks/use-commits.ts +12 -0
- package/packages/ui/src/hooks/use-copy.ts +18 -0
- package/packages/ui/src/hooks/use-diff-staleness.ts +58 -0
- package/packages/ui/src/hooks/use-diff.ts +12 -0
- package/packages/ui/src/hooks/use-highlighter.ts +190 -0
- package/packages/ui/src/hooks/use-info.ts +12 -0
- package/packages/ui/src/hooks/use-keyboard.ts +55 -0
- package/packages/ui/src/hooks/use-line-selection.ts +157 -0
- package/packages/ui/src/hooks/use-overview.ts +12 -0
- package/packages/ui/src/hooks/use-review-threads.ts +12 -0
- package/packages/ui/src/hooks/use-search-params.ts +26 -0
- package/packages/ui/src/hooks/use-theme.ts +34 -0
- package/packages/ui/src/hooks/use-thread-navigation.ts +43 -0
- package/packages/ui/src/lib/api.ts +232 -0
- package/packages/ui/src/lib/cn.ts +6 -0
- package/packages/ui/src/lib/context-expansion.ts +122 -0
- package/packages/ui/src/lib/diff-utils.ts +268 -0
- package/packages/ui/src/lib/dom-utils.ts +13 -0
- package/packages/ui/src/lib/file-tree.ts +122 -0
- package/packages/ui/src/lib/query-client.ts +10 -0
- package/packages/ui/src/lib/render-content.tsx +23 -0
- package/packages/ui/src/lib/syntax-token.ts +4 -0
- package/packages/ui/src/main.tsx +14 -0
- package/packages/ui/src/queries/commits.ts +9 -0
- package/packages/ui/src/queries/diff.ts +9 -0
- package/packages/ui/src/queries/file.ts +10 -0
- package/packages/ui/src/queries/info.ts +9 -0
- package/packages/ui/src/queries/overview.ts +9 -0
- package/packages/ui/src/styles/app.css +178 -0
- package/packages/ui/src/types/comment.ts +61 -0
- package/packages/ui/src/vite-env.d.ts +1 -0
- package/packages/ui/tests/context-expansion.test.ts +279 -0
- package/packages/ui/tests/diff-utils.test.ts +409 -0
- package/packages/ui/tsconfig.json +14 -0
- package/packages/ui/vite.config.ts +23 -0
- package/scripts/build-skills.ts +26 -0
- package/scripts/build.ts +15 -0
- package/scripts/dev.ts +32 -0
- package/scripts/lib/transformers/claude-code.ts +11 -0
- package/scripts/lib/transformers/codex.ts +17 -0
- package/scripts/lib/transformers/cursor.ts +17 -0
- package/scripts/lib/transformers/index.ts +3 -0
- package/scripts/lib/utils.ts +70 -0
- package/scripts/link-dev.ts +54 -0
- package/skills/diffity-resolve/SKILL.md +55 -0
- package/skills/diffity-review/SKILL.md +74 -0
- package/skills/diffity-start/SKILL.md +27 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import ReactMarkdown from 'react-markdown';
|
|
3
|
+
import remarkGfm from 'remark-gfm';
|
|
4
|
+
import type { Components } from 'react-markdown';
|
|
5
|
+
import { useHighlighter } from '../hooks/use-highlighter';
|
|
6
|
+
import { getTheme } from '../hooks/use-theme';
|
|
7
|
+
|
|
8
|
+
interface MarkdownContentProps {
|
|
9
|
+
content: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function MarkdownContent(props: MarkdownContentProps) {
|
|
13
|
+
const { content } = props;
|
|
14
|
+
const { highlight, ready } = useHighlighter();
|
|
15
|
+
|
|
16
|
+
const components = useMemo<Components>(() => ({
|
|
17
|
+
p({ children }) {
|
|
18
|
+
return <p className="mb-1.5 last:mb-0">{children}</p>;
|
|
19
|
+
},
|
|
20
|
+
strong({ children }) {
|
|
21
|
+
return <strong className="font-semibold text-text">{children}</strong>;
|
|
22
|
+
},
|
|
23
|
+
em({ children }) {
|
|
24
|
+
return <em>{children}</em>;
|
|
25
|
+
},
|
|
26
|
+
a({ href, children }) {
|
|
27
|
+
return (
|
|
28
|
+
<a href={href} className="text-accent hover:underline" target="_blank" rel="noopener noreferrer">
|
|
29
|
+
{children}
|
|
30
|
+
</a>
|
|
31
|
+
);
|
|
32
|
+
},
|
|
33
|
+
ul({ children }) {
|
|
34
|
+
return <ul className="list-disc pl-4 mb-1.5 last:mb-0">{children}</ul>;
|
|
35
|
+
},
|
|
36
|
+
ol({ children }) {
|
|
37
|
+
return <ol className="list-decimal pl-4 mb-1.5 last:mb-0">{children}</ol>;
|
|
38
|
+
},
|
|
39
|
+
li({ children }) {
|
|
40
|
+
return <li className="mb-0.5">{children}</li>;
|
|
41
|
+
},
|
|
42
|
+
blockquote({ children }) {
|
|
43
|
+
return (
|
|
44
|
+
<blockquote className="border-l-2 border-border pl-3 text-text-muted mb-1.5 last:mb-0">
|
|
45
|
+
{children}
|
|
46
|
+
</blockquote>
|
|
47
|
+
);
|
|
48
|
+
},
|
|
49
|
+
pre({ children }) {
|
|
50
|
+
return <div className="mb-1.5 last:mb-0">{children}</div>;
|
|
51
|
+
},
|
|
52
|
+
code({ className, children }) {
|
|
53
|
+
const match = /language-(\w+)/.exec(className || '');
|
|
54
|
+
const lang = match ? match[1] : null;
|
|
55
|
+
const codeString = String(children).replace(/\n$/, '');
|
|
56
|
+
|
|
57
|
+
if (!lang) {
|
|
58
|
+
return (
|
|
59
|
+
<code className="px-1 py-0.5 rounded bg-bg-tertiary text-[0.9em] font-mono">
|
|
60
|
+
{children}
|
|
61
|
+
</code>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let highlighted: { text: string; color?: string }[][] | null = null;
|
|
66
|
+
if (ready && lang) {
|
|
67
|
+
const result = highlight(codeString, `file.${lang}`, getTheme());
|
|
68
|
+
if (result) {
|
|
69
|
+
highlighted = result.map((line) => line.tokens);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className="rounded-md border border-border overflow-hidden">
|
|
75
|
+
<div className="bg-bg-secondary px-3 py-1 border-b border-border">
|
|
76
|
+
<span className="text-[10px] text-text-muted font-mono">{lang}</span>
|
|
77
|
+
</div>
|
|
78
|
+
<pre className="px-3 py-2 overflow-x-auto bg-bg text-xs leading-5 font-mono">
|
|
79
|
+
{highlighted ? (
|
|
80
|
+
highlighted.map((tokens, lineIdx) => (
|
|
81
|
+
<div key={lineIdx}>
|
|
82
|
+
{tokens.map((token, tokenIdx) => (
|
|
83
|
+
<span key={tokenIdx} style={token.color ? { color: token.color } : undefined}>
|
|
84
|
+
{token.text}
|
|
85
|
+
</span>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
))
|
|
89
|
+
) : (
|
|
90
|
+
<code>{codeString}</code>
|
|
91
|
+
)}
|
|
92
|
+
</pre>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
hr() {
|
|
97
|
+
return <hr className="border-border my-2" />;
|
|
98
|
+
},
|
|
99
|
+
h1({ children }) {
|
|
100
|
+
return <p className="font-semibold text-text mb-1">{children}</p>;
|
|
101
|
+
},
|
|
102
|
+
h2({ children }) {
|
|
103
|
+
return <p className="font-semibold text-text mb-1">{children}</p>;
|
|
104
|
+
},
|
|
105
|
+
h3({ children }) {
|
|
106
|
+
return <p className="font-semibold text-text mb-1">{children}</p>;
|
|
107
|
+
},
|
|
108
|
+
del({ children }) {
|
|
109
|
+
return <del className="text-text-muted">{children}</del>;
|
|
110
|
+
},
|
|
111
|
+
table({ children }) {
|
|
112
|
+
return (
|
|
113
|
+
<table className="border-collapse border border-border text-xs my-1.5">
|
|
114
|
+
{children}
|
|
115
|
+
</table>
|
|
116
|
+
);
|
|
117
|
+
},
|
|
118
|
+
th({ children }) {
|
|
119
|
+
return (
|
|
120
|
+
<th className="border border-border px-2 py-1 bg-bg-secondary text-left font-medium">
|
|
121
|
+
{children}
|
|
122
|
+
</th>
|
|
123
|
+
);
|
|
124
|
+
},
|
|
125
|
+
td({ children }) {
|
|
126
|
+
return (
|
|
127
|
+
<td className="border border-border px-2 py-1">{children}</td>
|
|
128
|
+
);
|
|
129
|
+
},
|
|
130
|
+
}), [highlight, ready]);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div className="markdown-body">
|
|
134
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]} components={components}>
|
|
135
|
+
{content}
|
|
136
|
+
</ReactMarkdown>
|
|
137
|
+
</div>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { CommentThread as CommentThreadType } from '../types/comment';
|
|
3
|
+
import { CommentBubble } from './comment-bubble';
|
|
4
|
+
import { CommentIcon } from './icons/comment-icon';
|
|
5
|
+
import { ChevronIcon } from './icons/chevron-icon';
|
|
6
|
+
import { TrashIcon } from './icons/trash-icon';
|
|
7
|
+
import { ThreadBadge } from './ui/thread-badge';
|
|
8
|
+
|
|
9
|
+
interface OrphanedThreadsProps {
|
|
10
|
+
threads: CommentThreadType[];
|
|
11
|
+
onDeleteComment: (threadId: string, commentId: string) => void;
|
|
12
|
+
onDeleteThread: (threadId: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function OrphanedThreads(props: OrphanedThreadsProps) {
|
|
16
|
+
const { threads, onDeleteComment, onDeleteThread } = props;
|
|
17
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
18
|
+
|
|
19
|
+
if (threads.length === 0) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="border-b border-border bg-bg-secondary/50">
|
|
25
|
+
<button
|
|
26
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
27
|
+
className="flex items-center gap-2 w-full px-4 py-2 text-xs text-text-muted hover:text-text-secondary transition-colors cursor-pointer"
|
|
28
|
+
>
|
|
29
|
+
<ChevronIcon expanded={isExpanded} />
|
|
30
|
+
<CommentIcon className="w-3.5 h-3.5" />
|
|
31
|
+
<span>
|
|
32
|
+
{threads.length} outdated comment{threads.length !== 1 ? 's' : ''}
|
|
33
|
+
</span>
|
|
34
|
+
<ThreadBadge variant="outdated" />
|
|
35
|
+
</button>
|
|
36
|
+
{isExpanded && (
|
|
37
|
+
<div className="px-4 pb-3 space-y-2">
|
|
38
|
+
{threads.map((thread) => {
|
|
39
|
+
const lineLabel = thread.startLine === thread.endLine
|
|
40
|
+
? `Line ${thread.startLine}`
|
|
41
|
+
: `Lines ${thread.startLine}–${thread.endLine}`;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div key={thread.id} className="border border-border rounded-lg overflow-hidden max-w-[700px]">
|
|
45
|
+
<div className="flex items-center justify-between px-3 py-1.5 bg-bg-secondary border-b border-border">
|
|
46
|
+
<div className="flex items-center gap-2">
|
|
47
|
+
<span className="text-[11px] text-text-muted font-mono">{lineLabel}</span>
|
|
48
|
+
<ThreadBadge variant="outdated" />
|
|
49
|
+
{(thread.status === 'resolved' || thread.status === 'dismissed') && (
|
|
50
|
+
<ThreadBadge variant={thread.status} />
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
<button
|
|
54
|
+
onClick={() => onDeleteThread(thread.id)}
|
|
55
|
+
className="text-text-muted hover:text-deleted transition-colors cursor-pointer"
|
|
56
|
+
title="Delete thread"
|
|
57
|
+
>
|
|
58
|
+
<TrashIcon className="w-3.5 h-3.5" />
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
{thread.anchorContent && (
|
|
62
|
+
<pre className="px-3 py-2 text-xs font-mono text-text-muted bg-bg-tertiary/50 border-b border-border overflow-x-auto whitespace-pre max-h-24 overflow-y-auto">{thread.anchorContent}</pre>
|
|
63
|
+
)}
|
|
64
|
+
<div>
|
|
65
|
+
{thread.comments.map((comment) => (
|
|
66
|
+
<CommentBubble
|
|
67
|
+
key={comment.id}
|
|
68
|
+
comment={comment}
|
|
69
|
+
onDelete={() => onDeleteComment(thread.id, comment.id)}
|
|
70
|
+
/>
|
|
71
|
+
))}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
})}
|
|
76
|
+
</div>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { OverviewFile } from '../lib/api';
|
|
2
|
+
|
|
3
|
+
interface OverviewFileListProps {
|
|
4
|
+
files: OverviewFile[];
|
|
5
|
+
onViewAll: () => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const STATUS_COLORS: Record<string, string> = {
|
|
9
|
+
staged: 'text-added',
|
|
10
|
+
modified: 'text-changed',
|
|
11
|
+
added: 'text-added',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const STATUS_LABELS: Record<string, string> = {
|
|
15
|
+
staged: 'S',
|
|
16
|
+
modified: 'M',
|
|
17
|
+
added: 'A',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function OverviewFileList(props: OverviewFileListProps) {
|
|
21
|
+
const { files, onViewAll } = props;
|
|
22
|
+
|
|
23
|
+
if (files.length === 0) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="border border-border rounded-lg bg-bg-secondary overflow-hidden">
|
|
29
|
+
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
|
30
|
+
<div className="flex items-center gap-2">
|
|
31
|
+
<h3 className="font-medium text-text">Changed files</h3>
|
|
32
|
+
<span className="px-2 py-0.5 text-xs font-mono rounded-full bg-bg-tertiary text-text-secondary">
|
|
33
|
+
{files.length}
|
|
34
|
+
</span>
|
|
35
|
+
</div>
|
|
36
|
+
<button
|
|
37
|
+
onClick={onViewAll}
|
|
38
|
+
className="text-xs font-medium text-accent hover:text-accent/80 transition-colors"
|
|
39
|
+
>
|
|
40
|
+
View diff
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
<ul className="divide-y divide-border">
|
|
44
|
+
{files.map((file) => (
|
|
45
|
+
<li key={file.path} className="flex items-center gap-3 px-4 py-2">
|
|
46
|
+
<span className={`text-xs font-mono font-bold w-4 shrink-0 ${STATUS_COLORS[file.status]}`}>
|
|
47
|
+
{STATUS_LABELS[file.status]}
|
|
48
|
+
</span>
|
|
49
|
+
<span className="text-sm font-mono text-text-secondary truncate">
|
|
50
|
+
{file.path}
|
|
51
|
+
</span>
|
|
52
|
+
</li>
|
|
53
|
+
))}
|
|
54
|
+
</ul>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { DiffLine as DiffLineType } from '@diffity/parser';
|
|
2
|
+
import type { HighlightedTokens } from '../hooks/use-highlighter';
|
|
3
|
+
import type { SyntaxToken } from '../lib/syntax-token';
|
|
4
|
+
import type { ViewMode } from '../lib/diff-utils';
|
|
5
|
+
import type { LineRenderProps } from '../types/comment';
|
|
6
|
+
import { renderLineWithComments } from './hunk-block';
|
|
7
|
+
import { renderSplitRows } from './hunk-block-split';
|
|
8
|
+
|
|
9
|
+
export function buildExpansionSyntaxMap(
|
|
10
|
+
lines: DiffLineType[],
|
|
11
|
+
highlightLine?: (code: string) => HighlightedTokens[] | null,
|
|
12
|
+
): Map<string, SyntaxToken[]> {
|
|
13
|
+
const map = new Map<string, SyntaxToken[]>();
|
|
14
|
+
if (!highlightLine) {
|
|
15
|
+
return map;
|
|
16
|
+
}
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
if (!line.content) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
const highlighted = highlightLine(line.content);
|
|
22
|
+
if (!highlighted || highlighted.length === 0) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const key = `${line.type}-${line.type === 'delete' ? line.oldLineNumber : line.newLineNumber}`;
|
|
26
|
+
map.set(key, highlighted[0].tokens);
|
|
27
|
+
}
|
|
28
|
+
return map;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function renderExpansionRows(
|
|
32
|
+
lines: DiffLineType[],
|
|
33
|
+
viewMode: ViewMode,
|
|
34
|
+
keyPrefix: string,
|
|
35
|
+
syntaxMap: Map<string, SyntaxToken[]> | undefined,
|
|
36
|
+
props: LineRenderProps,
|
|
37
|
+
): React.ReactNode[] {
|
|
38
|
+
if (viewMode === 'split') {
|
|
39
|
+
return renderSplitRows(lines, true, syntaxMap, keyPrefix, props);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result: React.ReactNode[] = [];
|
|
43
|
+
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
result.push(...renderLineWithComments(lines[i], i, true, syntaxMap, props));
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { XIcon } from './icons/x-icon';
|
|
3
|
+
|
|
4
|
+
interface ShortcutModalProps {
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const shortcuts = [
|
|
9
|
+
{
|
|
10
|
+
category: 'Navigation',
|
|
11
|
+
items: [
|
|
12
|
+
{ key: 'j', description: 'Next file' },
|
|
13
|
+
{ key: 'k', description: 'Previous file' },
|
|
14
|
+
{ key: 'n', description: 'Next changed hunk' },
|
|
15
|
+
{ key: 'p', description: 'Previous changed hunk' },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
category: 'View',
|
|
20
|
+
items: [
|
|
21
|
+
{ key: 'u', description: 'Unified view' },
|
|
22
|
+
{ key: 's', description: 'Split view' },
|
|
23
|
+
{ key: 'x', description: 'Collapse/expand file' },
|
|
24
|
+
{ key: 'Shift+x', description: 'Collapse/expand all files' },
|
|
25
|
+
{ key: 'r', description: 'Toggle file as viewed' },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
category: 'Other',
|
|
30
|
+
items: [
|
|
31
|
+
{ key: '/', description: 'Focus search' },
|
|
32
|
+
{ key: '?', description: 'Show shortcuts' },
|
|
33
|
+
{ key: 'Esc', description: 'Close modal / clear' },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
export function ShortcutModal(props: ShortcutModalProps) {
|
|
39
|
+
const { onClose } = props;
|
|
40
|
+
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
41
|
+
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const dialog = dialogRef.current;
|
|
44
|
+
if (!dialog) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
dialog.showModal();
|
|
49
|
+
return () => dialog.close();
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<dialog
|
|
54
|
+
ref={dialogRef}
|
|
55
|
+
className="bg-bg text-text border border-border rounded-xl shadow-md w-[420px] max-w-[90vw] max-h-[80vh] overflow-y-auto backdrop:bg-black/60 backdrop:backdrop-blur-sm p-0 m-auto fixed inset-0 h-fit"
|
|
56
|
+
onClose={onClose}
|
|
57
|
+
onClick={(e) => {
|
|
58
|
+
if (e.target === dialogRef.current) {
|
|
59
|
+
onClose();
|
|
60
|
+
}
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<div className="flex items-center justify-between px-5 py-3.5 border-b border-border">
|
|
64
|
+
<h2 className="text-sm font-semibold">Keyboard shortcuts</h2>
|
|
65
|
+
<button
|
|
66
|
+
className="p-1 rounded-md text-text-muted hover:text-text hover:bg-hover cursor-pointer"
|
|
67
|
+
onClick={onClose}
|
|
68
|
+
>
|
|
69
|
+
<XIcon className="w-4 h-4" />
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="px-5 py-4">
|
|
73
|
+
{shortcuts.map(group => (
|
|
74
|
+
<div key={group.category} className="mb-5 last:mb-0">
|
|
75
|
+
<h3 className="text-[10px] font-semibold text-text-muted mb-2.5 uppercase tracking-widest">
|
|
76
|
+
{group.category}
|
|
77
|
+
</h3>
|
|
78
|
+
<div className="flex flex-col gap-1.5">
|
|
79
|
+
{group.items.map(item => (
|
|
80
|
+
<div key={item.key} className="flex items-center justify-between py-0.5">
|
|
81
|
+
<span className="text-xs text-text-secondary">{item.description}</span>
|
|
82
|
+
<kbd className="inline-flex items-center justify-center min-w-6 h-5 px-1.5 bg-bg-secondary border border-border rounded font-mono text-[11px] text-text-muted shadow-[inset_0_-1px_0_var(--color-border)]">
|
|
83
|
+
{item.key}
|
|
84
|
+
</kbd>
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</dialog>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import type { DiffFile } from '@diffity/parser';
|
|
3
|
+
import { FileTree } from './file-tree';
|
|
4
|
+
import { SidebarIcon } from './icons/sidebar-icon';
|
|
5
|
+
import { SearchIcon } from './icons/search-icon';
|
|
6
|
+
import { XIcon } from './icons/x-icon';
|
|
7
|
+
|
|
8
|
+
interface SidebarProps {
|
|
9
|
+
files: DiffFile[];
|
|
10
|
+
activeFile: string | null;
|
|
11
|
+
reviewedFiles: Set<string>;
|
|
12
|
+
filesWithComments: Set<string>;
|
|
13
|
+
onFileClick: (path: string) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function Sidebar(props: SidebarProps) {
|
|
17
|
+
const { files, activeFile, reviewedFiles, filesWithComments, onFileClick } = props;
|
|
18
|
+
const [search, setSearch] = useState('');
|
|
19
|
+
const [collapsed, setCollapsed] = useState(false);
|
|
20
|
+
|
|
21
|
+
if (collapsed) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="w-10 min-w-10 border-r border-border bg-bg-secondary flex items-start justify-center pt-3">
|
|
24
|
+
<button
|
|
25
|
+
className="p-1.5 rounded-md text-text-muted hover:text-text hover:bg-hover cursor-pointer"
|
|
26
|
+
onClick={() => setCollapsed(false)}
|
|
27
|
+
title="Show sidebar"
|
|
28
|
+
>
|
|
29
|
+
<SidebarIcon className="w-4 h-4" />
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<aside className="w-72 min-w-72 border-r border-border bg-bg-secondary flex flex-col overflow-hidden">
|
|
37
|
+
<div className="flex items-center justify-between px-3 py-2.5 border-b border-border">
|
|
38
|
+
<span className="text-xs font-medium text-text-secondary flex items-center gap-2 uppercase tracking-wider">
|
|
39
|
+
Files
|
|
40
|
+
<span className="inline-flex items-center justify-center min-w-5 h-5 px-1.5 bg-bg-tertiary rounded-full text-[10px] font-semibold text-text-muted">
|
|
41
|
+
{reviewedFiles.size > 0 ? `${reviewedFiles.size}/${files.length}` : files.length}
|
|
42
|
+
</span>
|
|
43
|
+
</span>
|
|
44
|
+
<button
|
|
45
|
+
className="p-1 rounded-md text-text-muted hover:text-text hover:bg-hover cursor-pointer"
|
|
46
|
+
onClick={() => setCollapsed(true)}
|
|
47
|
+
title="Hide sidebar"
|
|
48
|
+
>
|
|
49
|
+
<SidebarIcon className="w-3.5 h-3.5" />
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
<div className="relative px-3 py-2">
|
|
53
|
+
<SearchIcon className="absolute left-5 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-text-muted pointer-events-none" />
|
|
54
|
+
<input
|
|
55
|
+
className="w-full pl-7 pr-7 py-1.5 border border-border rounded-md bg-bg text-xs outline-none focus:border-accent focus:ring-1 focus:ring-accent/20 placeholder:text-text-muted"
|
|
56
|
+
type="text"
|
|
57
|
+
placeholder="Filter files..."
|
|
58
|
+
value={search}
|
|
59
|
+
onChange={e => setSearch(e.target.value)}
|
|
60
|
+
/>
|
|
61
|
+
{search && (
|
|
62
|
+
<button
|
|
63
|
+
className="absolute right-5 top-1/2 -translate-y-1/2 text-text-muted hover:text-text cursor-pointer"
|
|
64
|
+
onClick={() => setSearch('')}
|
|
65
|
+
>
|
|
66
|
+
<XIcon className="w-3 h-3" />
|
|
67
|
+
</button>
|
|
68
|
+
)}
|
|
69
|
+
</div>
|
|
70
|
+
<FileTree
|
|
71
|
+
files={files}
|
|
72
|
+
search={search}
|
|
73
|
+
activeFile={activeFile}
|
|
74
|
+
reviewedFiles={reviewedFiles}
|
|
75
|
+
filesWithComments={filesWithComments}
|
|
76
|
+
onFileClick={onFileClick}
|
|
77
|
+
/>
|
|
78
|
+
</aside>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
interface StaleDiffBannerProps {
|
|
2
|
+
onRefresh: () => void;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function StaleDiffBanner(props: StaleDiffBannerProps) {
|
|
6
|
+
const { onRefresh } = props;
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<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">
|
|
10
|
+
<span className="text-accent font-medium">
|
|
11
|
+
Files have changed since this diff was loaded
|
|
12
|
+
</span>
|
|
13
|
+
<button
|
|
14
|
+
onClick={onRefresh}
|
|
15
|
+
className="px-2.5 py-0.5 bg-accent text-white rounded-md text-[11px] font-medium hover:bg-accent-hover transition-colors cursor-pointer"
|
|
16
|
+
>
|
|
17
|
+
Refresh
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ParsedDiff } from '@diffity/parser';
|
|
2
|
+
import { DiffStats } from './diff-stats';
|
|
3
|
+
import { GitBranchIcon } from './icons/git-branch-icon';
|
|
4
|
+
|
|
5
|
+
interface SummaryBarProps {
|
|
6
|
+
diff: ParsedDiff | null;
|
|
7
|
+
repoName: string | null;
|
|
8
|
+
branch: string | null;
|
|
9
|
+
description: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function SummaryBar(props: SummaryBarProps) {
|
|
13
|
+
const { diff, repoName, branch, description } = props;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="flex items-center justify-between px-4 py-2.5 bg-bg-secondary border-b border-border font-sans text-sm">
|
|
17
|
+
<div className="flex items-center gap-2.5">
|
|
18
|
+
{repoName && <span className="font-semibold text-text">{repoName}</span>}
|
|
19
|
+
{branch && (
|
|
20
|
+
<span className="inline-flex items-center gap-1 px-2 py-0.5 bg-diff-hunk-bg text-diff-hunk-text rounded-md font-mono text-xs">
|
|
21
|
+
<GitBranchIcon className="w-3 h-3" />
|
|
22
|
+
{branch}
|
|
23
|
+
</span>
|
|
24
|
+
)}
|
|
25
|
+
{description && <span className="text-text-secondary">{description}</span>}
|
|
26
|
+
</div>
|
|
27
|
+
{diff ? (
|
|
28
|
+
<div className="flex items-center gap-3">
|
|
29
|
+
<span className="text-text-muted text-xs">
|
|
30
|
+
{diff.stats.filesChanged} file{diff.stats.filesChanged !== 1 ? 's' : ''}
|
|
31
|
+
</span>
|
|
32
|
+
<DiffStats additions={diff.stats.totalAdditions} deletions={diff.stats.totalDeletions} />
|
|
33
|
+
</div>
|
|
34
|
+
) : (
|
|
35
|
+
<div className="w-48 h-4 bg-bg-tertiary rounded animate-pulse" />
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|