@thxgg/steward 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/.env.example +7 -0
- package/LICENSE +21 -0
- package/README.md +175 -0
- package/app/app.vue +14 -0
- package/app/assets/css/main.css +129 -0
- package/app/components/CommandPalette.vue +182 -0
- package/app/components/ShortcutsHelp.vue +85 -0
- package/app/components/git/ChangesMinimap.vue +143 -0
- package/app/components/git/CommitList.vue +224 -0
- package/app/components/git/DiffPanel.vue +402 -0
- package/app/components/git/DiffViewer.vue +803 -0
- package/app/components/layout/RepoSelector.vue +358 -0
- package/app/components/layout/Sidebar.vue +91 -0
- package/app/components/prd/Meta.vue +69 -0
- package/app/components/prd/Viewer.vue +285 -0
- package/app/components/tasks/Board.vue +86 -0
- package/app/components/tasks/Card.vue +108 -0
- package/app/components/tasks/Column.vue +108 -0
- package/app/components/tasks/Detail.vue +291 -0
- package/app/components/ui/badge/Badge.vue +26 -0
- package/app/components/ui/badge/index.ts +26 -0
- package/app/components/ui/button/Button.vue +29 -0
- package/app/components/ui/button/index.ts +38 -0
- package/app/components/ui/card/Card.vue +22 -0
- package/app/components/ui/card/CardAction.vue +17 -0
- package/app/components/ui/card/CardContent.vue +17 -0
- package/app/components/ui/card/CardDescription.vue +17 -0
- package/app/components/ui/card/CardFooter.vue +17 -0
- package/app/components/ui/card/CardHeader.vue +17 -0
- package/app/components/ui/card/CardTitle.vue +17 -0
- package/app/components/ui/card/index.ts +7 -0
- package/app/components/ui/combobox/Combobox.vue +19 -0
- package/app/components/ui/combobox/ComboboxAnchor.vue +23 -0
- package/app/components/ui/combobox/ComboboxEmpty.vue +21 -0
- package/app/components/ui/combobox/ComboboxGroup.vue +27 -0
- package/app/components/ui/combobox/ComboboxInput.vue +42 -0
- package/app/components/ui/combobox/ComboboxItem.vue +24 -0
- package/app/components/ui/combobox/ComboboxItemIndicator.vue +23 -0
- package/app/components/ui/combobox/ComboboxList.vue +33 -0
- package/app/components/ui/combobox/ComboboxSeparator.vue +21 -0
- package/app/components/ui/combobox/ComboboxTrigger.vue +24 -0
- package/app/components/ui/combobox/ComboboxViewport.vue +23 -0
- package/app/components/ui/combobox/index.ts +13 -0
- package/app/components/ui/command/Command.vue +103 -0
- package/app/components/ui/command/CommandDialog.vue +33 -0
- package/app/components/ui/command/CommandEmpty.vue +27 -0
- package/app/components/ui/command/CommandGroup.vue +45 -0
- package/app/components/ui/command/CommandInput.vue +54 -0
- package/app/components/ui/command/CommandItem.vue +76 -0
- package/app/components/ui/command/CommandList.vue +25 -0
- package/app/components/ui/command/CommandSeparator.vue +21 -0
- package/app/components/ui/command/CommandShortcut.vue +17 -0
- package/app/components/ui/command/index.ts +25 -0
- package/app/components/ui/dialog/Dialog.vue +19 -0
- package/app/components/ui/dialog/DialogClose.vue +15 -0
- package/app/components/ui/dialog/DialogContent.vue +53 -0
- package/app/components/ui/dialog/DialogDescription.vue +23 -0
- package/app/components/ui/dialog/DialogFooter.vue +15 -0
- package/app/components/ui/dialog/DialogHeader.vue +17 -0
- package/app/components/ui/dialog/DialogOverlay.vue +21 -0
- package/app/components/ui/dialog/DialogScrollContent.vue +59 -0
- package/app/components/ui/dialog/DialogTitle.vue +23 -0
- package/app/components/ui/dialog/DialogTrigger.vue +15 -0
- package/app/components/ui/dialog/index.ts +10 -0
- package/app/components/ui/input/Input.vue +33 -0
- package/app/components/ui/input/index.ts +1 -0
- package/app/components/ui/scroll-area/ScrollArea.vue +33 -0
- package/app/components/ui/scroll-area/ScrollBar.vue +32 -0
- package/app/components/ui/scroll-area/index.ts +2 -0
- package/app/components/ui/separator/Separator.vue +29 -0
- package/app/components/ui/separator/index.ts +1 -0
- package/app/components/ui/sheet/Sheet.vue +19 -0
- package/app/components/ui/sheet/SheetClose.vue +15 -0
- package/app/components/ui/sheet/SheetContent.vue +62 -0
- package/app/components/ui/sheet/SheetDescription.vue +21 -0
- package/app/components/ui/sheet/SheetFooter.vue +16 -0
- package/app/components/ui/sheet/SheetHeader.vue +15 -0
- package/app/components/ui/sheet/SheetOverlay.vue +21 -0
- package/app/components/ui/sheet/SheetTitle.vue +21 -0
- package/app/components/ui/sheet/SheetTrigger.vue +15 -0
- package/app/components/ui/sheet/index.ts +8 -0
- package/app/components/ui/tabs/Tabs.vue +24 -0
- package/app/components/ui/tabs/TabsContent.vue +21 -0
- package/app/components/ui/tabs/TabsList.vue +24 -0
- package/app/components/ui/tabs/TabsTrigger.vue +26 -0
- package/app/components/ui/tabs/index.ts +4 -0
- package/app/components/ui/tooltip/Tooltip.vue +19 -0
- package/app/components/ui/tooltip/TooltipContent.vue +34 -0
- package/app/components/ui/tooltip/TooltipProvider.vue +14 -0
- package/app/components/ui/tooltip/TooltipTrigger.vue +15 -0
- package/app/components/ui/tooltip/index.ts +4 -0
- package/app/composables/useFileWatch.ts +78 -0
- package/app/composables/useGit.ts +180 -0
- package/app/composables/useKeyboard.ts +180 -0
- package/app/composables/usePrd.ts +86 -0
- package/app/composables/useRepos.ts +108 -0
- package/app/composables/useThemeMode.ts +38 -0
- package/app/composables/useToast.ts +31 -0
- package/app/layouts/default.vue +197 -0
- package/app/lib/utils.ts +7 -0
- package/app/pages/[repo]/[prd].vue +263 -0
- package/app/pages/index.vue +257 -0
- package/app/types/git.ts +81 -0
- package/app/types/index.ts +29 -0
- package/app/types/prd.ts +49 -0
- package/app/types/repo.ts +37 -0
- package/app/types/task.ts +134 -0
- package/bin/prd +21 -0
- package/components.json +21 -0
- package/dist/app/types/git.js +1 -0
- package/dist/app/types/prd.js +1 -0
- package/dist/app/types/repo.js +1 -0
- package/dist/app/types/task.js +1 -0
- package/dist/host/src/api/git.js +96 -0
- package/dist/host/src/api/index.js +4 -0
- package/dist/host/src/api/prds.js +195 -0
- package/dist/host/src/api/repos.js +47 -0
- package/dist/host/src/api/state.js +63 -0
- package/dist/host/src/executor.js +109 -0
- package/dist/host/src/index.js +95 -0
- package/dist/host/src/mcp.js +62 -0
- package/dist/host/src/ui.js +64 -0
- package/dist/server/utils/db.js +125 -0
- package/dist/server/utils/git.js +396 -0
- package/dist/server/utils/prd-state.js +229 -0
- package/dist/server/utils/repos.js +256 -0
- package/docs/MCP.md +180 -0
- package/nuxt.config.ts +34 -0
- package/package.json +88 -0
- package/public/favicon.ico +0 -0
- package/public/robots.txt +1 -0
- package/server/api/browse.get.ts +52 -0
- package/server/api/repos/[repoId]/git/commits.get.ts +103 -0
- package/server/api/repos/[repoId]/git/diff.get.ts +77 -0
- package/server/api/repos/[repoId]/git/file-content.get.ts +66 -0
- package/server/api/repos/[repoId]/git/file-diff.get.ts +109 -0
- package/server/api/repos/[repoId]/prd/[prdSlug]/progress.get.ts +36 -0
- package/server/api/repos/[repoId]/prd/[prdSlug]/tasks/[taskId]/commits.get.ts +146 -0
- package/server/api/repos/[repoId]/prd/[prdSlug]/tasks.get.ts +36 -0
- package/server/api/repos/[repoId]/prd/[prdSlug].get.ts +97 -0
- package/server/api/repos/[repoId]/prds.get.ts +85 -0
- package/server/api/repos/[repoId]/refresh-git-repos.post.ts +42 -0
- package/server/api/repos/[repoId].delete.ts +27 -0
- package/server/api/repos/index.get.ts +5 -0
- package/server/api/repos/index.post.ts +39 -0
- package/server/api/watch.get.ts +63 -0
- package/server/plugins/migrate-legacy-state.ts +19 -0
- package/server/tsconfig.json +3 -0
- package/server/utils/db.ts +169 -0
- package/server/utils/git.ts +478 -0
- package/server/utils/prd-state.ts +335 -0
- package/server/utils/repos.ts +322 -0
- package/server/utils/watcher.ts +179 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { AlertCircle, RefreshCw, Loader2, Keyboard, FileCode, FileDiff as FileDiffIcon } from 'lucide-vue-next'
|
|
3
|
+
import { ScrollArea } from '~/components/ui/scroll-area'
|
|
4
|
+
import { Button } from '~/components/ui/button'
|
|
5
|
+
import {
|
|
6
|
+
Tooltip,
|
|
7
|
+
TooltipContent,
|
|
8
|
+
TooltipProvider,
|
|
9
|
+
TooltipTrigger,
|
|
10
|
+
} from '~/components/ui/tooltip'
|
|
11
|
+
import type { FileDiff, DiffHunk } from '~/types/git'
|
|
12
|
+
|
|
13
|
+
const props = defineProps<{
|
|
14
|
+
/** Repository ID */
|
|
15
|
+
repoId: string
|
|
16
|
+
/** Commit SHA to display diff for */
|
|
17
|
+
commitSha: string
|
|
18
|
+
/** Optional relative path to git repo (for pseudo-monorepos) */
|
|
19
|
+
repoPath?: string
|
|
20
|
+
}>()
|
|
21
|
+
|
|
22
|
+
const emit = defineEmits<{
|
|
23
|
+
close: []
|
|
24
|
+
}>()
|
|
25
|
+
|
|
26
|
+
const { fetchDiff, fetchFileDiff, fetchFileContent, isLoadingDiff, isLoadingFileDiff, isLoadingFileContent } = useGit()
|
|
27
|
+
|
|
28
|
+
// State
|
|
29
|
+
const files = ref<FileDiff[]>([])
|
|
30
|
+
const selectedFile = ref<string | undefined>()
|
|
31
|
+
const hunks = ref<DiffHunk[]>([])
|
|
32
|
+
const fileContent = ref<string | null>(null)
|
|
33
|
+
const error = ref<string | null>(null)
|
|
34
|
+
const fileDiffError = ref<string | null>(null)
|
|
35
|
+
|
|
36
|
+
// View mode: 'changes' shows only hunks, 'full' shows entire file with changes highlighted
|
|
37
|
+
const viewMode = ref<'changes' | 'full'>('changes')
|
|
38
|
+
|
|
39
|
+
// Get the selected file's metadata
|
|
40
|
+
const selectedFileDiff = computed(() => files.value.find(f => f.path === selectedFile.value))
|
|
41
|
+
|
|
42
|
+
// Fetch file list on mount and when commitSha changes
|
|
43
|
+
async function loadDiff() {
|
|
44
|
+
error.value = null
|
|
45
|
+
files.value = []
|
|
46
|
+
selectedFile.value = undefined
|
|
47
|
+
hunks.value = []
|
|
48
|
+
|
|
49
|
+
const result = await fetchDiff(props.repoId, props.commitSha, props.repoPath)
|
|
50
|
+
|
|
51
|
+
if (result.length === 0 && !isLoadingDiff.value) {
|
|
52
|
+
// Check if it was an error (empty could be valid, but error is set by toast)
|
|
53
|
+
// We'll set a generic message if truly no files
|
|
54
|
+
error.value = null // Let empty state show naturally
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
files.value = result
|
|
58
|
+
|
|
59
|
+
// Auto-select first file
|
|
60
|
+
if (result.length > 0) {
|
|
61
|
+
selectedFile.value = result[0]!.path
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Fetch file diff when selection changes
|
|
66
|
+
async function loadFileDiff() {
|
|
67
|
+
if (!selectedFile.value) {
|
|
68
|
+
hunks.value = []
|
|
69
|
+
fileContent.value = null
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fileDiffError.value = null
|
|
74
|
+
|
|
75
|
+
// Fetch hunks (always needed for highlighting changes)
|
|
76
|
+
const result = await fetchFileDiff(props.repoId, props.commitSha, selectedFile.value, props.repoPath)
|
|
77
|
+
hunks.value = result
|
|
78
|
+
|
|
79
|
+
// Fetch full file content if in full mode
|
|
80
|
+
if (viewMode.value === 'full') {
|
|
81
|
+
const content = await fetchFileContent(props.repoId, props.commitSha, selectedFile.value, props.repoPath)
|
|
82
|
+
fileContent.value = content
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Toggle view mode
|
|
87
|
+
function toggleViewMode() {
|
|
88
|
+
viewMode.value = viewMode.value === 'changes' ? 'full' : 'changes'
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Reload file content when switching to full mode
|
|
92
|
+
watch(viewMode, async (mode) => {
|
|
93
|
+
if (mode === 'full' && selectedFile.value && !fileContent.value) {
|
|
94
|
+
const content = await fetchFileContent(props.repoId, props.commitSha, selectedFile.value, props.repoPath)
|
|
95
|
+
fileContent.value = content
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// Handle file selection from minimap
|
|
100
|
+
function handleFileSelect(path: string) {
|
|
101
|
+
selectedFile.value = path
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Retry loading
|
|
105
|
+
function retry() {
|
|
106
|
+
loadDiff()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function retryFileDiff() {
|
|
110
|
+
loadFileDiff()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Watch for selection changes
|
|
114
|
+
watch(selectedFile, () => {
|
|
115
|
+
loadFileDiff()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
// Watch for prop changes
|
|
119
|
+
watch(
|
|
120
|
+
() => [props.repoId, props.commitSha, props.repoPath] as const,
|
|
121
|
+
() => {
|
|
122
|
+
loadDiff()
|
|
123
|
+
},
|
|
124
|
+
{ immediate: true }
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
// Keyboard navigation
|
|
128
|
+
const panelRef = ref<HTMLElement | null>(null)
|
|
129
|
+
const diffViewerRef = ref<HTMLElement | null>(null)
|
|
130
|
+
|
|
131
|
+
// Navigate to next/previous file
|
|
132
|
+
function navigateFile(direction: 'next' | 'prev') {
|
|
133
|
+
if (files.value.length === 0) return
|
|
134
|
+
|
|
135
|
+
const currentIndex = selectedFile.value
|
|
136
|
+
? files.value.findIndex(f => f.path === selectedFile.value)
|
|
137
|
+
: -1
|
|
138
|
+
|
|
139
|
+
let newIndex: number
|
|
140
|
+
if (direction === 'next') {
|
|
141
|
+
newIndex = currentIndex < files.value.length - 1 ? currentIndex + 1 : 0
|
|
142
|
+
} else {
|
|
143
|
+
newIndex = currentIndex > 0 ? currentIndex - 1 : files.value.length - 1
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const file = files.value[newIndex]
|
|
147
|
+
if (file) {
|
|
148
|
+
selectedFile.value = file.path
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Jump between hunks
|
|
153
|
+
function jumpToHunk(direction: 'next' | 'prev') {
|
|
154
|
+
if (!diffViewerRef.value) return
|
|
155
|
+
|
|
156
|
+
const separators = diffViewerRef.value.querySelectorAll('.diff-separator')
|
|
157
|
+
if (separators.length === 0) return
|
|
158
|
+
|
|
159
|
+
const container = diffViewerRef.value.closest('.overflow-y-auto, [data-radix-scroll-area-viewport]')
|
|
160
|
+
if (!container) return
|
|
161
|
+
|
|
162
|
+
const scrollTop = container.scrollTop
|
|
163
|
+
const containerRect = container.getBoundingClientRect()
|
|
164
|
+
|
|
165
|
+
let targetSeparator: Element | null = null
|
|
166
|
+
|
|
167
|
+
if (direction === 'next') {
|
|
168
|
+
// Find the first separator below current scroll position
|
|
169
|
+
for (const sep of separators) {
|
|
170
|
+
const rect = sep.getBoundingClientRect()
|
|
171
|
+
const relativeTop = rect.top - containerRect.top + scrollTop
|
|
172
|
+
if (relativeTop > scrollTop + 50) {
|
|
173
|
+
targetSeparator = sep
|
|
174
|
+
break
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// If no separator found below, wrap to first
|
|
178
|
+
if (!targetSeparator && separators.length > 0) {
|
|
179
|
+
targetSeparator = separators[0]!
|
|
180
|
+
}
|
|
181
|
+
} else {
|
|
182
|
+
// Find the last separator above current scroll position
|
|
183
|
+
for (let i = separators.length - 1; i >= 0; i--) {
|
|
184
|
+
const sep = separators[i]!
|
|
185
|
+
const rect = sep.getBoundingClientRect()
|
|
186
|
+
const relativeTop = rect.top - containerRect.top + scrollTop
|
|
187
|
+
if (relativeTop < scrollTop - 10) {
|
|
188
|
+
targetSeparator = sep
|
|
189
|
+
break
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// If no separator found above, wrap to last
|
|
193
|
+
if (!targetSeparator && separators.length > 0) {
|
|
194
|
+
targetSeparator = separators[separators.length - 1]!
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (targetSeparator) {
|
|
199
|
+
targetSeparator.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Handle keyboard events
|
|
204
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
205
|
+
// Skip if user is typing in an input
|
|
206
|
+
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) {
|
|
207
|
+
return
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
switch (event.key) {
|
|
211
|
+
case 'j':
|
|
212
|
+
case 'ArrowDown':
|
|
213
|
+
if (!event.ctrlKey && !event.metaKey) {
|
|
214
|
+
event.preventDefault()
|
|
215
|
+
navigateFile('next')
|
|
216
|
+
}
|
|
217
|
+
break
|
|
218
|
+
case 'k':
|
|
219
|
+
case 'ArrowUp':
|
|
220
|
+
if (!event.ctrlKey && !event.metaKey) {
|
|
221
|
+
event.preventDefault()
|
|
222
|
+
navigateFile('prev')
|
|
223
|
+
}
|
|
224
|
+
break
|
|
225
|
+
case '[':
|
|
226
|
+
event.preventDefault()
|
|
227
|
+
jumpToHunk('prev')
|
|
228
|
+
break
|
|
229
|
+
case ']':
|
|
230
|
+
event.preventDefault()
|
|
231
|
+
jumpToHunk('next')
|
|
232
|
+
break
|
|
233
|
+
case 'Escape':
|
|
234
|
+
event.preventDefault()
|
|
235
|
+
emit('close')
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Set up keyboard listeners when mounted
|
|
241
|
+
onMounted(() => {
|
|
242
|
+
document.addEventListener('keydown', handleKeydown)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
onUnmounted(() => {
|
|
246
|
+
document.removeEventListener('keydown', handleKeydown)
|
|
247
|
+
})
|
|
248
|
+
</script>
|
|
249
|
+
|
|
250
|
+
<template>
|
|
251
|
+
<div ref="panelRef" class="flex h-full flex-col overflow-hidden" tabindex="-1">
|
|
252
|
+
<!-- Error state -->
|
|
253
|
+
<div v-if="error" class="flex flex-1 flex-col items-center justify-center gap-4 p-8">
|
|
254
|
+
<AlertCircle class="size-12 text-destructive" />
|
|
255
|
+
<div class="text-center">
|
|
256
|
+
<p class="font-medium">Failed to load diff</p>
|
|
257
|
+
<p class="text-sm text-muted-foreground">{{ error }}</p>
|
|
258
|
+
</div>
|
|
259
|
+
<Button variant="outline" size="sm" @click="retry">
|
|
260
|
+
<RefreshCw class="mr-2 size-4" />
|
|
261
|
+
Retry
|
|
262
|
+
</Button>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<!-- Loading state (initial load) -->
|
|
266
|
+
<div v-else-if="isLoadingDiff && files.length === 0" class="flex flex-1 items-center justify-center">
|
|
267
|
+
<Loader2 class="size-8 animate-spin text-muted-foreground" />
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<!-- Empty state -->
|
|
271
|
+
<div
|
|
272
|
+
v-else-if="files.length === 0"
|
|
273
|
+
class="flex flex-1 flex-col items-center justify-center gap-2 p-8 text-center text-muted-foreground"
|
|
274
|
+
>
|
|
275
|
+
<p>No files changed in this commit</p>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<!-- Main content -->
|
|
279
|
+
<div v-else class="flex min-h-0 flex-1 overflow-hidden">
|
|
280
|
+
<!-- Minimap sidebar -->
|
|
281
|
+
<div class="flex w-56 shrink-0 flex-col border-r border-border">
|
|
282
|
+
<div class="flex items-center justify-between border-b border-border px-3 py-2">
|
|
283
|
+
<span class="text-xs font-medium text-muted-foreground">
|
|
284
|
+
{{ files.length }} file{{ files.length !== 1 ? 's' : '' }} changed
|
|
285
|
+
</span>
|
|
286
|
+
<!-- Keyboard shortcuts hint -->
|
|
287
|
+
<TooltipProvider>
|
|
288
|
+
<Tooltip>
|
|
289
|
+
<TooltipTrigger as-child>
|
|
290
|
+
<button class="text-muted-foreground/50 transition-colors hover:text-muted-foreground">
|
|
291
|
+
<Keyboard class="size-3.5" />
|
|
292
|
+
</button>
|
|
293
|
+
</TooltipTrigger>
|
|
294
|
+
<TooltipContent side="bottom" align="end" class="max-w-xs">
|
|
295
|
+
<div class="space-y-1 text-xs">
|
|
296
|
+
<div class="flex justify-between gap-4">
|
|
297
|
+
<span class="text-muted-foreground">Navigate files</span>
|
|
298
|
+
<span class="font-mono">j/k or ↑/↓</span>
|
|
299
|
+
</div>
|
|
300
|
+
<div class="flex justify-between gap-4">
|
|
301
|
+
<span class="text-muted-foreground">Jump to hunk</span>
|
|
302
|
+
<span class="font-mono">[ / ]</span>
|
|
303
|
+
</div>
|
|
304
|
+
<div class="flex justify-between gap-4">
|
|
305
|
+
<span class="text-muted-foreground">Close</span>
|
|
306
|
+
<span class="font-mono">Esc</span>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
</TooltipContent>
|
|
310
|
+
</Tooltip>
|
|
311
|
+
</TooltipProvider>
|
|
312
|
+
</div>
|
|
313
|
+
<div class="min-h-0 flex-1 overflow-y-auto">
|
|
314
|
+
<GitChangesMinimap
|
|
315
|
+
:files="files"
|
|
316
|
+
:selected-file="selectedFile"
|
|
317
|
+
@select="handleFileSelect"
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<!-- Diff viewer area -->
|
|
323
|
+
<div class="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
324
|
+
<!-- File header with rename support -->
|
|
325
|
+
<div v-if="selectedFile" class="flex items-center gap-2 border-b border-border px-4 py-2">
|
|
326
|
+
<span v-if="selectedFileDiff?.oldPath" class="min-w-0 flex-1 truncate font-mono text-sm">
|
|
327
|
+
<span class="text-muted-foreground">{{ selectedFileDiff.oldPath }}</span>
|
|
328
|
+
<span class="mx-2 text-muted-foreground">→</span>
|
|
329
|
+
<span>{{ selectedFile }}</span>
|
|
330
|
+
</span>
|
|
331
|
+
<span v-else class="min-w-0 flex-1 truncate font-mono text-sm">{{ selectedFile }}</span>
|
|
332
|
+
<span v-if="selectedFileDiff?.binary" class="rounded bg-yellow-500/10 px-1.5 py-0.5 text-xs text-yellow-600 dark:text-yellow-400">
|
|
333
|
+
binary
|
|
334
|
+
</span>
|
|
335
|
+
<!-- View mode toggle -->
|
|
336
|
+
<TooltipProvider v-if="!selectedFileDiff?.binary">
|
|
337
|
+
<Tooltip>
|
|
338
|
+
<TooltipTrigger as-child>
|
|
339
|
+
<Button
|
|
340
|
+
variant="ghost"
|
|
341
|
+
size="sm"
|
|
342
|
+
class="h-7 shrink-0 gap-1.5 text-xs"
|
|
343
|
+
@click="toggleViewMode"
|
|
344
|
+
>
|
|
345
|
+
<component :is="viewMode === 'changes' ? FileDiffIcon : FileCode" class="size-3.5" />
|
|
346
|
+
{{ viewMode === 'changes' ? 'Changes' : 'Full file' }}
|
|
347
|
+
</Button>
|
|
348
|
+
</TooltipTrigger>
|
|
349
|
+
<TooltipContent>
|
|
350
|
+
{{ viewMode === 'changes' ? 'Show full file with changes' : 'Show changes only' }}
|
|
351
|
+
</TooltipContent>
|
|
352
|
+
</Tooltip>
|
|
353
|
+
</TooltipProvider>
|
|
354
|
+
</div>
|
|
355
|
+
|
|
356
|
+
<!-- File diff loading -->
|
|
357
|
+
<div v-if="isLoadingFileDiff" class="flex flex-1 items-center justify-center">
|
|
358
|
+
<Loader2 class="size-6 animate-spin text-muted-foreground" />
|
|
359
|
+
</div>
|
|
360
|
+
|
|
361
|
+
<!-- File diff error -->
|
|
362
|
+
<div
|
|
363
|
+
v-else-if="fileDiffError"
|
|
364
|
+
class="flex flex-1 flex-col items-center justify-center gap-4 p-8"
|
|
365
|
+
>
|
|
366
|
+
<AlertCircle class="size-8 text-destructive" />
|
|
367
|
+
<div class="text-center">
|
|
368
|
+
<p class="text-sm font-medium">Failed to load file diff</p>
|
|
369
|
+
<p class="text-xs text-muted-foreground">{{ fileDiffError }}</p>
|
|
370
|
+
</div>
|
|
371
|
+
<Button variant="outline" size="sm" @click="retryFileDiff">
|
|
372
|
+
<RefreshCw class="mr-2 size-4" />
|
|
373
|
+
Retry
|
|
374
|
+
</Button>
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
<!-- Diff viewer (min-h-0 enables flex item to shrink for scrolling) -->
|
|
378
|
+
<ScrollArea v-else-if="selectedFile" class="h-0 flex-1 overflow-hidden">
|
|
379
|
+
<div ref="diffViewerRef">
|
|
380
|
+
<GitDiffViewer
|
|
381
|
+
:hunks="hunks"
|
|
382
|
+
:file-path="selectedFile"
|
|
383
|
+
:binary="selectedFileDiff?.binary"
|
|
384
|
+
:old-path="selectedFileDiff?.oldPath"
|
|
385
|
+
:file-content="fileContent"
|
|
386
|
+
:show-full-file="viewMode === 'full'"
|
|
387
|
+
:is-loading-content="isLoadingFileContent"
|
|
388
|
+
/>
|
|
389
|
+
</div>
|
|
390
|
+
</ScrollArea>
|
|
391
|
+
|
|
392
|
+
<!-- No file selected -->
|
|
393
|
+
<div
|
|
394
|
+
v-else
|
|
395
|
+
class="flex flex-1 items-center justify-center text-sm text-muted-foreground"
|
|
396
|
+
>
|
|
397
|
+
Select a file to view changes
|
|
398
|
+
</div>
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
</div>
|
|
402
|
+
</template>
|