@yuangc/vibe-review 1.0.0 → 1.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/client/edit-diff-data.test.ts +21 -0
- package/client/edit-diff-data.ts +12 -0
- package/client/edit-diff.tsx +66 -0
- package/client/file-tree.tsx +2 -21
- package/client/git-history.tsx +649 -0
- package/client/review.tsx +2 -2
- package/client/split-diff.tsx +3 -3
- package/client/workspace-commit.tsx +381 -0
- package/client/wrap-toggle.tsx +30 -0
- package/index.client.tsx +16 -0
- package/index.server.ts +4 -0
- package/package.json +4 -4
- package/paseo-plugin.json +1 -1
- package/server/git-history.ts +83 -0
- package/server/paseo-api-types.ts +1 -1
- package/server/review.ts +2 -1
- package/server/verify.ts +4 -4
- package/server/workspace-commit.ts +79 -0
- package/shared/git-history.ts +21 -0
- package/shared/workspace-commit.ts +29 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { ToolCallTimelineItem } from "@getpaseo/protocol/agent-types";
|
|
4
|
+
import { editDiffFiles } from "./edit-diff-data.ts";
|
|
5
|
+
|
|
6
|
+
const edit = (unifiedDiff?: string): ToolCallTimelineItem => ({
|
|
7
|
+
type: "tool_call", callId: "edit-1", name: "Edit", status: "completed", error: null,
|
|
8
|
+
detail: { type: "edit", filePath: "client/review.tsx", unifiedDiff },
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("Codex Edit patches use the existing split-diff file model", () => {
|
|
12
|
+
const files = editDiffFiles(edit("@@ -168 +168 @@\n-old\n+new"));
|
|
13
|
+
assert.equal(files?.[0]?.path, "client/review.tsx");
|
|
14
|
+
assert.equal(files?.[0]?.removed, 1);
|
|
15
|
+
assert.equal(files?.[0]?.added, 1);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test("edits without a usable patch keep Paseo's original card", () => {
|
|
19
|
+
assert.equal(editDiffFiles(edit()), null);
|
|
20
|
+
assert.equal(editDiffFiles(edit("tool result without diff markers")), null);
|
|
21
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ToolCallTimelineItem } from "@getpaseo/protocol/agent-types";
|
|
2
|
+
import { parseDiffFiles, type DiffFile } from "./diff.ts";
|
|
3
|
+
|
|
4
|
+
/** Keep the native tool card until Paseo supplies an actual patch to render. */
|
|
5
|
+
export function editDiffFiles(item: ToolCallTimelineItem): DiffFile[] | null {
|
|
6
|
+
if (item.detail.type !== "edit" || !item.detail.unifiedDiff?.trim()) return null;
|
|
7
|
+
if (!/^(?:diff --git |@@ |--- |\+\+\+ )/m.test(item.detail.unifiedDiff)) return null;
|
|
8
|
+
const files = parseDiffFiles(item.detail.unifiedDiff);
|
|
9
|
+
if (!files.length) return null;
|
|
10
|
+
if (files.length === 1 && files[0]!.path === "修改内容") files[0]!.path = item.detail.filePath;
|
|
11
|
+
return files;
|
|
12
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Pressable, StyleSheet, Text, View } from "react-native";
|
|
3
|
+
import type { PluginTimelineItemProps } from "@getpaseo/plugin/client";
|
|
4
|
+
import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { parseDiffFiles } from "./diff";
|
|
7
|
+
import { FileTypeIcon } from "./file-icons";
|
|
8
|
+
import { SplitDiff } from "./split-diff";
|
|
9
|
+
|
|
10
|
+
export const EDIT_DIFF_KIND = "edit-diff";
|
|
11
|
+
export const editDiffSchema = z.object({ path: z.string(), diff: z.string(), status: z.string() });
|
|
12
|
+
|
|
13
|
+
export function EditDiffItem({ item, theme }: PluginTimelineItemProps<z.output<typeof editDiffSchema>>) {
|
|
14
|
+
const [openFile, setOpenFile] = useState<number | null>(null);
|
|
15
|
+
const [hoveredFile, setHoveredFile] = useState<number | null>(null);
|
|
16
|
+
const [wrapLines, setWrapLines] = useState(false);
|
|
17
|
+
const files = parseDiffFiles(item.data.diff);
|
|
18
|
+
if (files.length === 1) files[0]!.path = item.data.path;
|
|
19
|
+
|
|
20
|
+
return <View>{files.map((file, index) => {
|
|
21
|
+
const open = openFile === index;
|
|
22
|
+
const active = open || hoveredFile === index;
|
|
23
|
+
return <View key={`${index}:${file.path}`}>
|
|
24
|
+
<View style={styles.row}>
|
|
25
|
+
<Pressable
|
|
26
|
+
accessibilityRole="button"
|
|
27
|
+
accessibilityLabel={`Edit ${file.path},新增 ${file.added} 行,删除 ${file.removed} 行`}
|
|
28
|
+
accessibilityState={{ expanded: open }}
|
|
29
|
+
onHoverIn={() => setHoveredFile(index)}
|
|
30
|
+
onHoverOut={() => setHoveredFile((current) => current === index ? null : current)}
|
|
31
|
+
onPress={() => { setOpenFile(open ? null : index); setWrapLines(false); }}
|
|
32
|
+
style={styles.fileButton}
|
|
33
|
+
>
|
|
34
|
+
<View style={styles.iconSlot}>
|
|
35
|
+
<View style={active ? styles.chevronNudge : styles.pencilNudge}>
|
|
36
|
+
<Icon name={open ? "ChevronDown" : active ? "ChevronRight" : "Pencil"} size={12} color={active ? theme.colors.foreground : theme.colors.foregroundMuted} />
|
|
37
|
+
</View>
|
|
38
|
+
</View>
|
|
39
|
+
<Text style={[styles.editLabel, { color: active ? theme.colors.foreground : theme.colors.foregroundMuted }]}>Edit</Text>
|
|
40
|
+
<FileTypeIcon fileName={file.path} />
|
|
41
|
+
<Text style={styles.path} numberOfLines={1}>{file.path}</Text>
|
|
42
|
+
{/* TODO(Paseo): Add a file-page shortcut here when timeline renderer props expose a supported openFile navigation API. */}
|
|
43
|
+
</Pressable>
|
|
44
|
+
{open ? <Pressable accessibilityRole="button" accessibilityLabel="折行显示" accessibilityState={{ selected: wrapLines }} onPress={() => setWrapLines((value) => !value)} style={styles.wrapButton}>
|
|
45
|
+
<Icon name="WrapText" size={14} color={theme.colors.foregroundMuted} />
|
|
46
|
+
</Pressable> : null}
|
|
47
|
+
<Text style={styles.added}>+{file.added}</Text>
|
|
48
|
+
<Text style={styles.removed}>-{file.removed}</Text>
|
|
49
|
+
</View>
|
|
50
|
+
{open ? <SplitDiff file={file} wrapLines={wrapLines} /> : null}
|
|
51
|
+
</View>;
|
|
52
|
+
})}</View>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const styles = StyleSheet.create({
|
|
56
|
+
row: { flexDirection: "row", alignItems: "center", gap: 6, minHeight: 30, marginHorizontal: -13, paddingHorizontal: 8 },
|
|
57
|
+
fileButton: { flex: 1, minWidth: 0, minHeight: 30, flexDirection: "row", alignItems: "center", gap: 7 },
|
|
58
|
+
iconSlot: { width: 22, height: 22, alignItems: "center", justifyContent: "center", marginRight: -3 },
|
|
59
|
+
pencilNudge: { marginLeft: -1 },
|
|
60
|
+
chevronNudge: { marginLeft: -4, transform: [{ scale: 1.3 }] },
|
|
61
|
+
editLabel: { fontSize: 14 },
|
|
62
|
+
path: { fontSize: 14, flexShrink: 1, color: "#0133b3" },
|
|
63
|
+
wrapButton: { width: 20, height: 20, alignItems: "center", justifyContent: "center" },
|
|
64
|
+
added: { color: "#077d18", fontSize: 14, fontWeight: "600" },
|
|
65
|
+
removed: { color: "#b91c1c", fontSize: 14, fontWeight: "600" },
|
|
66
|
+
});
|
package/client/file-tree.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { Icon } from "@getpaseo/plugin/client/react-native";
|
|
|
4
4
|
import type { DiffFile } from "./diff";
|
|
5
5
|
import { FileTypeIcon } from "./file-icons";
|
|
6
6
|
import { SplitDiff } from "./split-diff";
|
|
7
|
+
import { WrapToggleButton } from "./wrap-toggle";
|
|
7
8
|
|
|
8
9
|
type FolderNode = { kind: "folder"; name: string; path: string; count: number; children: TreeNode[] };
|
|
9
10
|
type FileNode = { kind: "file"; name: string; file: DiffFile; index: number };
|
|
@@ -51,7 +52,6 @@ export function FileTree({ files, collapseFoldersInitially = false }: { files: D
|
|
|
51
52
|
});
|
|
52
53
|
const [openFile, setOpenFile] = useState<number | null>(null);
|
|
53
54
|
const [wrapLines, setWrapLines] = useState(false);
|
|
54
|
-
const [showWrapTooltip, setShowWrapTooltip] = useState(false);
|
|
55
55
|
const nodes = buildFileTree(files);
|
|
56
56
|
|
|
57
57
|
const toggleFolder = (path: string) => setCollapsed((current) => {
|
|
@@ -90,7 +90,6 @@ export function FileTree({ files, collapseFoldersInitially = false }: { files: D
|
|
|
90
90
|
onPress={() => {
|
|
91
91
|
setOpenFile(isOpen ? null : index);
|
|
92
92
|
setWrapLines(false);
|
|
93
|
-
setShowWrapTooltip(false);
|
|
94
93
|
}}
|
|
95
94
|
style={styles.fileSelect}
|
|
96
95
|
>
|
|
@@ -98,20 +97,7 @@ export function FileTree({ files, collapseFoldersInitially = false }: { files: D
|
|
|
98
97
|
<FileTypeIcon fileName={node.name} />
|
|
99
98
|
<Text style={[styles.fileName, fileNameColors[file.status]]} numberOfLines={1}>{node.name}</Text>
|
|
100
99
|
</TouchableOpacity>
|
|
101
|
-
{isOpen ? <View style={styles.fileActions}>
|
|
102
|
-
<Pressable
|
|
103
|
-
accessibilityRole="button"
|
|
104
|
-
accessibilityLabel="折行显示"
|
|
105
|
-
aria-pressed={wrapLines}
|
|
106
|
-
onHoverIn={() => setShowWrapTooltip(true)}
|
|
107
|
-
onHoverOut={() => setShowWrapTooltip(false)}
|
|
108
|
-
onPress={() => setWrapLines((current) => !current)}
|
|
109
|
-
style={[styles.actionButton, wrapLines && styles.actionButtonActive, showWrapTooltip && styles.actionButtonHover]}
|
|
110
|
-
>
|
|
111
|
-
<Icon name="WrapText" size={13} color="#64748b" />
|
|
112
|
-
</Pressable>
|
|
113
|
-
{showWrapTooltip ? <View style={styles.tooltip}><Text style={styles.tooltipText}>折行显示</Text></View> : null}
|
|
114
|
-
</View> : null}
|
|
100
|
+
{isOpen ? <View style={styles.fileActions}><WrapToggleButton active={wrapLines} onToggle={() => setWrapLines((current) => !current)} /></View> : null}
|
|
115
101
|
<Text style={styles.added}>+{file.added}</Text>
|
|
116
102
|
<Text style={styles.removed}>-{file.removed}</Text>
|
|
117
103
|
</View>
|
|
@@ -127,11 +113,6 @@ const styles = StyleSheet.create({
|
|
|
127
113
|
selected: { backgroundColor: "#e8f0ff", zIndex: 3 },
|
|
128
114
|
fileSelect: { flex: 1, minHeight: 34, flexDirection: "row", alignItems: "center", gap: 6 },
|
|
129
115
|
fileActions: { flexDirection: "row-reverse", alignItems: "center", gap: 4, position: "relative", zIndex: 2 },
|
|
130
|
-
actionButton: { width: 20, height: 20, borderRadius: 4, alignItems: "center", justifyContent: "center" },
|
|
131
|
-
actionButtonActive: { backgroundColor: "#cbcbcd" },
|
|
132
|
-
actionButtonHover: { backgroundColor: "#e5e7eb" },
|
|
133
|
-
tooltip: { position: "absolute", bottom: 24, right: 0, width: 72, paddingHorizontal: 9, paddingVertical: 5, borderRadius: 5, backgroundColor: "#1e293b", zIndex: 4 },
|
|
134
|
-
tooltipText: { color: "#fff", fontSize: 11, textAlign: "center" },
|
|
135
116
|
folderName: { color: "#111827", fontSize: 12, fontWeight: "600", flexShrink: 1 },
|
|
136
117
|
count: { color: "#6b7280", fontSize: 11 },
|
|
137
118
|
fileIndent: { width: 15 },
|