@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,143 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { FilePlus, FileEdit, FileX, ArrowRight } from 'lucide-vue-next'
|
|
3
|
+
import type { FileDiff, FileStatus } from '~/types/git'
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
/** Array of file diffs to display */
|
|
7
|
+
files: FileDiff[]
|
|
8
|
+
/** Currently selected file path */
|
|
9
|
+
selectedFile?: string
|
|
10
|
+
}>()
|
|
11
|
+
|
|
12
|
+
const emit = defineEmits<{
|
|
13
|
+
select: [path: string]
|
|
14
|
+
}>()
|
|
15
|
+
|
|
16
|
+
// Calculate max changes for proportional bar sizing
|
|
17
|
+
const maxChanges = computed(() => {
|
|
18
|
+
if (props.files.length === 0) return 1
|
|
19
|
+
return Math.max(...props.files.map(f => f.additions + f.deletions), 1)
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
// Get status icon and color
|
|
23
|
+
function getStatusConfig(status: FileStatus) {
|
|
24
|
+
switch (status) {
|
|
25
|
+
case 'added':
|
|
26
|
+
return { icon: FilePlus, class: 'text-green-600 dark:text-green-400' }
|
|
27
|
+
case 'deleted':
|
|
28
|
+
return { icon: FileX, class: 'text-red-600 dark:text-red-400' }
|
|
29
|
+
case 'renamed':
|
|
30
|
+
return { icon: ArrowRight, class: 'text-blue-600 dark:text-blue-400' }
|
|
31
|
+
case 'modified':
|
|
32
|
+
default:
|
|
33
|
+
return { icon: FileEdit, class: 'text-yellow-600 dark:text-yellow-400' }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Calculate bar width percentages
|
|
38
|
+
function getBarWidths(file: FileDiff) {
|
|
39
|
+
const total = file.additions + file.deletions
|
|
40
|
+
if (total === 0) return { additions: 0, deletions: 0, total: 0 }
|
|
41
|
+
|
|
42
|
+
const totalWidth = (total / maxChanges.value) * 100
|
|
43
|
+
const additionsWidth = (file.additions / total) * totalWidth
|
|
44
|
+
const deletionsWidth = (file.deletions / total) * totalWidth
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
additions: additionsWidth,
|
|
48
|
+
deletions: deletionsWidth,
|
|
49
|
+
total: totalWidth,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Get display name for file (handle renames)
|
|
54
|
+
function getDisplayName(file: FileDiff): string {
|
|
55
|
+
if (file.status === 'renamed' && file.oldPath) {
|
|
56
|
+
return `${file.oldPath} → ${file.path}`
|
|
57
|
+
}
|
|
58
|
+
return file.path
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Get short name (just filename)
|
|
62
|
+
function getShortName(path: string): string {
|
|
63
|
+
return path.split('/').pop() || path
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function handleClick(path: string) {
|
|
67
|
+
emit('select', path)
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<template>
|
|
72
|
+
<div class="space-y-0.5 p-2">
|
|
73
|
+
<button
|
|
74
|
+
v-for="file in files"
|
|
75
|
+
:key="file.path"
|
|
76
|
+
class="group flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-sm transition-colors hover:bg-muted/50"
|
|
77
|
+
:class="{
|
|
78
|
+
'bg-muted': selectedFile === file.path,
|
|
79
|
+
}"
|
|
80
|
+
@click="handleClick(file.path)"
|
|
81
|
+
>
|
|
82
|
+
<!-- Status icon -->
|
|
83
|
+
<component
|
|
84
|
+
:is="getStatusConfig(file.status).icon"
|
|
85
|
+
class="size-4 shrink-0"
|
|
86
|
+
:class="getStatusConfig(file.status).class"
|
|
87
|
+
/>
|
|
88
|
+
|
|
89
|
+
<!-- File info -->
|
|
90
|
+
<div class="min-w-0 flex-1">
|
|
91
|
+
<!-- File path -->
|
|
92
|
+
<div
|
|
93
|
+
class="truncate text-xs"
|
|
94
|
+
:class="{
|
|
95
|
+
'font-medium': selectedFile === file.path,
|
|
96
|
+
}"
|
|
97
|
+
:title="getDisplayName(file)"
|
|
98
|
+
>
|
|
99
|
+
<template v-if="file.status === 'renamed' && file.oldPath">
|
|
100
|
+
<span class="text-muted-foreground">{{ getShortName(file.oldPath) }}</span>
|
|
101
|
+
<ArrowRight class="mx-1 inline size-3 text-muted-foreground" />
|
|
102
|
+
<span>{{ getShortName(file.path) }}</span>
|
|
103
|
+
</template>
|
|
104
|
+
<template v-else>
|
|
105
|
+
{{ file.path }}
|
|
106
|
+
</template>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<!-- Change bar -->
|
|
110
|
+
<div class="mt-1 flex h-1.5 w-full overflow-hidden rounded-full bg-muted">
|
|
111
|
+
<div
|
|
112
|
+
v-if="getBarWidths(file).additions > 0"
|
|
113
|
+
class="bg-green-500 dark:bg-green-400"
|
|
114
|
+
:style="{ width: `${getBarWidths(file).additions}%` }"
|
|
115
|
+
/>
|
|
116
|
+
<div
|
|
117
|
+
v-if="getBarWidths(file).deletions > 0"
|
|
118
|
+
class="bg-red-500 dark:bg-red-400"
|
|
119
|
+
:style="{ width: `${getBarWidths(file).deletions}%` }"
|
|
120
|
+
/>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
|
|
124
|
+
<!-- Stats -->
|
|
125
|
+
<div class="flex shrink-0 items-center gap-1 text-xs">
|
|
126
|
+
<span v-if="file.additions > 0" class="text-green-600 dark:text-green-400">
|
|
127
|
+
+{{ file.additions }}
|
|
128
|
+
</span>
|
|
129
|
+
<span v-if="file.deletions > 0" class="text-red-600 dark:text-red-400">
|
|
130
|
+
-{{ file.deletions }}
|
|
131
|
+
</span>
|
|
132
|
+
</div>
|
|
133
|
+
</button>
|
|
134
|
+
|
|
135
|
+
<!-- Empty state -->
|
|
136
|
+
<div
|
|
137
|
+
v-if="files.length === 0"
|
|
138
|
+
class="py-4 text-center text-sm text-muted-foreground"
|
|
139
|
+
>
|
|
140
|
+
No files changed
|
|
141
|
+
</div>
|
|
142
|
+
</div>
|
|
143
|
+
</template>
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { GitCommit as GitCommitIcon, Plus, Minus, FileText, FolderGit2, AlertCircle } from 'lucide-vue-next'
|
|
3
|
+
import { Badge } from '~/components/ui/badge'
|
|
4
|
+
import type { GitCommit } from '~/types/git'
|
|
5
|
+
import type { CommitRef } from '~/types/task'
|
|
6
|
+
|
|
7
|
+
const props = defineProps<{
|
|
8
|
+
/** Array of commits to display - expects CommitRef objects with sha and repo */
|
|
9
|
+
commits: CommitRef[]
|
|
10
|
+
/** Repository ID for API calls */
|
|
11
|
+
repoId: string
|
|
12
|
+
}>()
|
|
13
|
+
|
|
14
|
+
const emit = defineEmits<{
|
|
15
|
+
select: [sha: string, repo?: string]
|
|
16
|
+
}>()
|
|
17
|
+
|
|
18
|
+
const { fetchCommits, isLoadingCommits } = useGit()
|
|
19
|
+
|
|
20
|
+
// Fetched commit details (keyed by sha for lookup)
|
|
21
|
+
const commitDetails = ref<Map<string, GitCommit>>(new Map())
|
|
22
|
+
|
|
23
|
+
// Track commits that failed to load
|
|
24
|
+
const failedCommits = ref<Set<string>>(new Set())
|
|
25
|
+
|
|
26
|
+
// Check if any commits have repo info (indicates pseudo-monorepo)
|
|
27
|
+
const hasMultipleRepos = computed(() => {
|
|
28
|
+
const repos = new Set(props.commits.map(c => c.repo).filter(Boolean))
|
|
29
|
+
return repos.size > 1
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Fetch commit details when props change
|
|
33
|
+
// Group commits by repo to make efficient API calls
|
|
34
|
+
watch(
|
|
35
|
+
() => ({ commits: props.commits, repoId: props.repoId }),
|
|
36
|
+
async ({ commits, repoId }) => {
|
|
37
|
+
if (commits.length === 0 || !repoId) {
|
|
38
|
+
commitDetails.value = new Map()
|
|
39
|
+
failedCommits.value = new Set()
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Group commits by repo
|
|
44
|
+
const commitsByRepo = new Map<string, string[]>()
|
|
45
|
+
for (const commit of commits) {
|
|
46
|
+
const repoPath = commit.repo || ''
|
|
47
|
+
if (!commitsByRepo.has(repoPath)) {
|
|
48
|
+
commitsByRepo.set(repoPath, [])
|
|
49
|
+
}
|
|
50
|
+
commitsByRepo.get(repoPath)!.push(commit.sha)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Fetch commits for each repo in parallel
|
|
54
|
+
const results = await Promise.all(
|
|
55
|
+
Array.from(commitsByRepo.entries()).map(async ([repoPath, shas]) => {
|
|
56
|
+
const result = await fetchCommits(repoId, shas, repoPath || undefined)
|
|
57
|
+
return {
|
|
58
|
+
commits: result.commits.map(c => ({ ...c, repoPath })),
|
|
59
|
+
failedShas: result.failedShas,
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
// Build map of sha -> commit details and collect failed SHAs
|
|
65
|
+
const detailsMap = new Map<string, GitCommit>()
|
|
66
|
+
const failed = new Set<string>()
|
|
67
|
+
|
|
68
|
+
for (const { commits: repoCommits, failedShas } of results) {
|
|
69
|
+
for (const commit of repoCommits) {
|
|
70
|
+
// Key by shortSha since props contain abbreviated SHAs
|
|
71
|
+
detailsMap.set(commit.shortSha, commit)
|
|
72
|
+
}
|
|
73
|
+
for (const sha of failedShas) {
|
|
74
|
+
failed.add(sha)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
commitDetails.value = detailsMap
|
|
79
|
+
failedCommits.value = failed
|
|
80
|
+
},
|
|
81
|
+
{ immediate: true }
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
// Format relative date
|
|
85
|
+
function formatRelativeDate(isoDate: string): string {
|
|
86
|
+
const date = new Date(isoDate)
|
|
87
|
+
const now = new Date()
|
|
88
|
+
const diffMs = now.getTime() - date.getTime()
|
|
89
|
+
const diffSeconds = Math.floor(diffMs / 1000)
|
|
90
|
+
const diffMinutes = Math.floor(diffSeconds / 60)
|
|
91
|
+
const diffHours = Math.floor(diffMinutes / 60)
|
|
92
|
+
const diffDays = Math.floor(diffHours / 24)
|
|
93
|
+
|
|
94
|
+
if (diffSeconds < 60) {
|
|
95
|
+
return 'just now'
|
|
96
|
+
} else if (diffMinutes < 60) {
|
|
97
|
+
return `${diffMinutes} minute${diffMinutes === 1 ? '' : 's'} ago`
|
|
98
|
+
} else if (diffHours < 24) {
|
|
99
|
+
return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`
|
|
100
|
+
} else if (diffDays < 7) {
|
|
101
|
+
return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`
|
|
102
|
+
} else {
|
|
103
|
+
return date.toLocaleDateString()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function handleClick(commit: CommitRef) {
|
|
108
|
+
emit('select', commit.sha, commit.repo)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Get commit details by SHA (supports prefix matching for abbreviated SHAs)
|
|
112
|
+
function getDetails(sha: string): GitCommit | undefined {
|
|
113
|
+
// Direct lookup first
|
|
114
|
+
const direct = commitDetails.value.get(sha)
|
|
115
|
+
if (direct) return direct
|
|
116
|
+
|
|
117
|
+
// Try prefix matching (short SHA might be abbreviated differently)
|
|
118
|
+
for (const [key, commit] of commitDetails.value) {
|
|
119
|
+
if (key.startsWith(sha) || sha.startsWith(key)) {
|
|
120
|
+
return commit
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return undefined
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check if a commit failed to load (supports prefix matching)
|
|
127
|
+
function isFailed(sha: string): boolean {
|
|
128
|
+
if (failedCommits.value.has(sha)) return true
|
|
129
|
+
|
|
130
|
+
// Try prefix matching
|
|
131
|
+
for (const failedSha of failedCommits.value) {
|
|
132
|
+
if (failedSha.startsWith(sha) || sha.startsWith(failedSha)) {
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
</script>
|
|
139
|
+
|
|
140
|
+
<template>
|
|
141
|
+
<div class="space-y-2">
|
|
142
|
+
<!-- Loading skeleton -->
|
|
143
|
+
<template v-if="isLoadingCommits">
|
|
144
|
+
<div
|
|
145
|
+
v-for="i in Math.min(commits.length, 3)"
|
|
146
|
+
:key="i"
|
|
147
|
+
class="animate-pulse rounded-lg border bg-muted/50 p-3"
|
|
148
|
+
>
|
|
149
|
+
<div class="flex items-start gap-3">
|
|
150
|
+
<div class="h-5 w-5 rounded bg-muted" />
|
|
151
|
+
<div class="flex-1 space-y-2">
|
|
152
|
+
<div class="h-4 w-20 rounded bg-muted" />
|
|
153
|
+
<div class="h-4 w-3/4 rounded bg-muted" />
|
|
154
|
+
<div class="h-3 w-1/2 rounded bg-muted" />
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</template>
|
|
159
|
+
|
|
160
|
+
<!-- Empty state -->
|
|
161
|
+
<div
|
|
162
|
+
v-else-if="commits.length === 0"
|
|
163
|
+
class="rounded-lg border border-dashed p-4 text-center text-sm text-muted-foreground"
|
|
164
|
+
>
|
|
165
|
+
No commits recorded for this task
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<!-- Commit list -->
|
|
169
|
+
<template v-else>
|
|
170
|
+
<button
|
|
171
|
+
v-for="commit in commits"
|
|
172
|
+
:key="commit.sha"
|
|
173
|
+
class="w-full rounded-lg border bg-card p-3 text-left transition-colors hover:bg-muted/50 focus:outline-none focus:ring-2 focus:ring-ring"
|
|
174
|
+
@click="handleClick(commit)"
|
|
175
|
+
>
|
|
176
|
+
<div class="flex items-start gap-3">
|
|
177
|
+
<GitCommitIcon class="mt-0.5 size-5 shrink-0 text-muted-foreground" />
|
|
178
|
+
<div class="min-w-0 flex-1">
|
|
179
|
+
<!-- SHA, stats, and repo badge -->
|
|
180
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
181
|
+
<code class="font-mono text-xs font-medium text-primary">
|
|
182
|
+
{{ getDetails(commit.sha)?.shortSha || commit.sha.substring(0, 7) }}
|
|
183
|
+
</code>
|
|
184
|
+
<template v-if="getDetails(commit.sha)">
|
|
185
|
+
<span class="flex items-center gap-1 text-xs text-muted-foreground">
|
|
186
|
+
<FileText class="size-3" />
|
|
187
|
+
{{ getDetails(commit.sha)!.filesChanged }}
|
|
188
|
+
</span>
|
|
189
|
+
<span class="flex items-center gap-0.5 text-xs text-green-600 dark:text-green-400">
|
|
190
|
+
<Plus class="size-3" />{{ getDetails(commit.sha)!.additions }}
|
|
191
|
+
</span>
|
|
192
|
+
<span class="flex items-center gap-0.5 text-xs text-red-600 dark:text-red-400">
|
|
193
|
+
<Minus class="size-3" />{{ getDetails(commit.sha)!.deletions }}
|
|
194
|
+
</span>
|
|
195
|
+
</template>
|
|
196
|
+
<!-- Repo badge - only show for pseudo-monorepos with multiple repos -->
|
|
197
|
+
<Badge v-if="commit.repo && hasMultipleRepos" variant="secondary" class="gap-1 text-xs">
|
|
198
|
+
<FolderGit2 class="size-3" />
|
|
199
|
+
{{ commit.repo }}
|
|
200
|
+
</Badge>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<!-- Message -->
|
|
204
|
+
<p class="mt-1 truncate text-sm">
|
|
205
|
+
<template v-if="getDetails(commit.sha)">
|
|
206
|
+
{{ getDetails(commit.sha)!.message }}
|
|
207
|
+
</template>
|
|
208
|
+
<span v-else-if="isFailed(commit.sha)" class="flex items-center gap-1 text-muted-foreground">
|
|
209
|
+
<AlertCircle class="size-3" />
|
|
210
|
+
Commit unavailable
|
|
211
|
+
</span>
|
|
212
|
+
<span v-else class="text-muted-foreground">Loading...</span>
|
|
213
|
+
</p>
|
|
214
|
+
|
|
215
|
+
<!-- Author and date -->
|
|
216
|
+
<p v-if="getDetails(commit.sha)" class="mt-1 text-xs text-muted-foreground">
|
|
217
|
+
{{ getDetails(commit.sha)!.author }} · {{ formatRelativeDate(getDetails(commit.sha)!.date) }}
|
|
218
|
+
</p>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</button>
|
|
222
|
+
</template>
|
|
223
|
+
</div>
|
|
224
|
+
</template>
|