@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,358 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { onClickOutside } from '@vueuse/core'
|
|
3
|
+
import { ChevronDown, Plus, FolderOpen, Check, Trash2, Folder } from 'lucide-vue-next'
|
|
4
|
+
import {
|
|
5
|
+
Sheet,
|
|
6
|
+
SheetContent,
|
|
7
|
+
SheetDescription,
|
|
8
|
+
SheetFooter,
|
|
9
|
+
SheetHeader,
|
|
10
|
+
SheetTitle
|
|
11
|
+
} from '~/components/ui/sheet'
|
|
12
|
+
import { Button } from '~/components/ui/button'
|
|
13
|
+
import { Input } from '~/components/ui/input'
|
|
14
|
+
|
|
15
|
+
const router = useRouter()
|
|
16
|
+
const route = useRoute()
|
|
17
|
+
const { repos, currentRepo, currentRepoId, selectRepo, addRepo, removeRepo } = useRepos()
|
|
18
|
+
const { showSuccess, showError } = useToast()
|
|
19
|
+
|
|
20
|
+
// Dropdown state
|
|
21
|
+
const open = ref(false)
|
|
22
|
+
const searchQuery = ref('')
|
|
23
|
+
const dropdownRef = ref<HTMLElement | null>(null)
|
|
24
|
+
|
|
25
|
+
// Close dropdown when clicking outside
|
|
26
|
+
onClickOutside(dropdownRef, () => {
|
|
27
|
+
if (open.value) {
|
|
28
|
+
open.value = false
|
|
29
|
+
searchQuery.value = ''
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// Close dropdown when route changes (e.g., command palette navigation)
|
|
34
|
+
watch(() => route.fullPath, () => {
|
|
35
|
+
open.value = false
|
|
36
|
+
searchQuery.value = ''
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
async function handleSelectRepo(id: string) {
|
|
40
|
+
selectRepo(id)
|
|
41
|
+
open.value = false
|
|
42
|
+
searchQuery.value = ''
|
|
43
|
+
|
|
44
|
+
// Fetch PRDs for new repo and navigate to first one
|
|
45
|
+
try {
|
|
46
|
+
const prds = await $fetch<{ slug: string }[]>(`/api/repos/${id}/prds`)
|
|
47
|
+
const firstPrd = prds?.[0]
|
|
48
|
+
if (firstPrd) {
|
|
49
|
+
router.push(`/${id}/${firstPrd.slug}`)
|
|
50
|
+
} else {
|
|
51
|
+
router.push('/')
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
router.push('/')
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function handleAddClick() {
|
|
59
|
+
open.value = false
|
|
60
|
+
showAddDialog.value = true
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Directory browser state
|
|
64
|
+
const showBrowser = ref(false)
|
|
65
|
+
const browserPath = ref('')
|
|
66
|
+
const browserDirs = ref<{ name: string; path: string }[]>([])
|
|
67
|
+
const browserLoading = ref(false)
|
|
68
|
+
|
|
69
|
+
async function browseDirectory(path?: string) {
|
|
70
|
+
browserLoading.value = true
|
|
71
|
+
try {
|
|
72
|
+
const data = await $fetch('/api/browse', {
|
|
73
|
+
query: { path: path || browserPath.value || undefined }
|
|
74
|
+
})
|
|
75
|
+
browserPath.value = data.current
|
|
76
|
+
browserDirs.value = data.directories
|
|
77
|
+
} catch {
|
|
78
|
+
// Silently fail - directory browser will show empty state
|
|
79
|
+
} finally {
|
|
80
|
+
browserLoading.value = false
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function openBrowser() {
|
|
85
|
+
showBrowser.value = true
|
|
86
|
+
browseDirectory(newRepoPath.value || undefined)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function selectDirectory(path: string) {
|
|
90
|
+
newRepoPath.value = path
|
|
91
|
+
showBrowser.value = false
|
|
92
|
+
addError.value = null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function navigateUp() {
|
|
96
|
+
const parent = browserPath.value.split('/').slice(0, -1).join('/') || '/'
|
|
97
|
+
browseDirectory(parent)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Add repo dialog state
|
|
101
|
+
const showAddDialog = ref(false)
|
|
102
|
+
const newRepoPath = ref('')
|
|
103
|
+
const addError = ref<string | null>(null)
|
|
104
|
+
const isAdding = ref(false)
|
|
105
|
+
|
|
106
|
+
// Filtered repos based on search
|
|
107
|
+
const filteredRepos = computed(() => {
|
|
108
|
+
if (!searchQuery.value) return repos.value
|
|
109
|
+
const query = searchQuery.value.toLowerCase()
|
|
110
|
+
return repos.value?.filter(
|
|
111
|
+
repo => repo.name.toLowerCase().includes(query) || repo.path.toLowerCase().includes(query)
|
|
112
|
+
) ?? []
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async function handleAddRepo() {
|
|
117
|
+
if (!newRepoPath.value.trim()) {
|
|
118
|
+
addError.value = 'Please enter a repository path'
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
isAdding.value = true
|
|
123
|
+
addError.value = null
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
const newRepo = await addRepo(newRepoPath.value.trim())
|
|
127
|
+
showAddDialog.value = false
|
|
128
|
+
newRepoPath.value = ''
|
|
129
|
+
showSuccess('Repository added', newRepo?.name || 'Successfully added repository')
|
|
130
|
+
|
|
131
|
+
// Navigate to first PRD of newly added repo
|
|
132
|
+
if (newRepo?.id) {
|
|
133
|
+
try {
|
|
134
|
+
const prds = await $fetch<{ slug: string }[]>(`/api/repos/${newRepo.id}/prds`)
|
|
135
|
+
const firstPrd = prds?.[0]
|
|
136
|
+
if (firstPrd) {
|
|
137
|
+
router.push(`/${newRepo.id}/${firstPrd.slug}`)
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
// Ignore navigation errors
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
if (error instanceof Error) {
|
|
145
|
+
// Extract message from fetch error
|
|
146
|
+
const fetchError = error as { data?: { message?: string } }
|
|
147
|
+
addError.value = fetchError.data?.message || error.message
|
|
148
|
+
} else {
|
|
149
|
+
addError.value = 'Failed to add repository'
|
|
150
|
+
}
|
|
151
|
+
} finally {
|
|
152
|
+
isAdding.value = false
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function handleRemoveRepo(event: Event, repoId: string) {
|
|
157
|
+
event.stopPropagation()
|
|
158
|
+
await removeRepo(repoId)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function handleDialogClose() {
|
|
162
|
+
showAddDialog.value = false
|
|
163
|
+
newRepoPath.value = ''
|
|
164
|
+
addError.value = null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Expose method to open the add dialog from outside (e.g., via keyboard shortcut)
|
|
168
|
+
defineExpose({
|
|
169
|
+
openAddDialog: () => {
|
|
170
|
+
showAddDialog.value = true
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
</script>
|
|
174
|
+
|
|
175
|
+
<template>
|
|
176
|
+
<div ref="dropdownRef" class="relative">
|
|
177
|
+
<!-- Simple dropdown for repo selection -->
|
|
178
|
+
<button
|
|
179
|
+
type="button"
|
|
180
|
+
class="inline-flex h-8 w-[200px] items-center justify-between gap-2 rounded-md border border-input bg-background px-3 text-sm font-normal ring-offset-background transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
|
|
181
|
+
@click="open = !open"
|
|
182
|
+
>
|
|
183
|
+
<span class="flex items-center gap-2 truncate">
|
|
184
|
+
<FolderOpen class="size-4 shrink-0 text-muted-foreground" />
|
|
185
|
+
<span class="truncate">
|
|
186
|
+
{{ currentRepo?.name ?? 'Select repository' }}
|
|
187
|
+
</span>
|
|
188
|
+
</span>
|
|
189
|
+
<ChevronDown class="size-4 shrink-0 opacity-50" />
|
|
190
|
+
</button>
|
|
191
|
+
|
|
192
|
+
<!-- Dropdown content -->
|
|
193
|
+
<div
|
|
194
|
+
v-if="open"
|
|
195
|
+
class="absolute top-full right-0 z-[9999] mt-1 w-[280px] rounded-md border bg-popover p-1 text-popover-foreground shadow-md"
|
|
196
|
+
>
|
|
197
|
+
<!-- Search input -->
|
|
198
|
+
<input
|
|
199
|
+
v-model="searchQuery"
|
|
200
|
+
type="text"
|
|
201
|
+
placeholder="Search repositories..."
|
|
202
|
+
class="mb-1 h-9 w-full rounded-md border border-input bg-background px-3 text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
203
|
+
/>
|
|
204
|
+
|
|
205
|
+
<!-- Repository items -->
|
|
206
|
+
<div class="max-h-[200px] overflow-y-auto">
|
|
207
|
+
<template v-if="filteredRepos?.length">
|
|
208
|
+
<div
|
|
209
|
+
v-for="repo in filteredRepos"
|
|
210
|
+
:key="repo.id"
|
|
211
|
+
class="group flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground cursor-pointer"
|
|
212
|
+
role="option"
|
|
213
|
+
:aria-selected="currentRepoId === repo.id"
|
|
214
|
+
@click="handleSelectRepo(repo.id)"
|
|
215
|
+
>
|
|
216
|
+
<Check
|
|
217
|
+
class="size-4 shrink-0"
|
|
218
|
+
:class="currentRepoId === repo.id ? 'opacity-100' : 'opacity-0'"
|
|
219
|
+
/>
|
|
220
|
+
<div class="flex flex-1 flex-col gap-0.5 overflow-hidden text-left">
|
|
221
|
+
<span class="truncate font-medium">{{ repo.name }}</span>
|
|
222
|
+
<span class="truncate text-xs text-muted-foreground">{{ repo.path }}</span>
|
|
223
|
+
</div>
|
|
224
|
+
<button
|
|
225
|
+
type="button"
|
|
226
|
+
class="ml-auto opacity-0 group-hover:opacity-100 p-1 hover:bg-destructive/10 rounded transition-opacity"
|
|
227
|
+
title="Remove repository"
|
|
228
|
+
@click="handleRemoveRepo($event, repo.id)"
|
|
229
|
+
>
|
|
230
|
+
<Trash2 class="size-3.5 text-destructive" />
|
|
231
|
+
</button>
|
|
232
|
+
</div>
|
|
233
|
+
</template>
|
|
234
|
+
<div v-else class="px-2 py-1.5 text-sm text-muted-foreground">
|
|
235
|
+
No repositories found.
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<!-- Separator -->
|
|
239
|
+
<div v-if="filteredRepos?.length" class="my-1 h-px bg-border" />
|
|
240
|
+
|
|
241
|
+
<!-- Add repository option -->
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
class="flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm text-muted-foreground hover:bg-accent hover:text-accent-foreground"
|
|
245
|
+
@click="handleAddClick"
|
|
246
|
+
>
|
|
247
|
+
<Plus class="size-4" />
|
|
248
|
+
<span>Add repository...</span>
|
|
249
|
+
</button>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
<!-- Add Repository Sheet -->
|
|
254
|
+
<Sheet :open="showAddDialog" @update:open="showAddDialog = $event">
|
|
255
|
+
<SheetContent side="right" class="flex h-full flex-col" @escape-key-down="handleDialogClose">
|
|
256
|
+
<SheetHeader class="px-6">
|
|
257
|
+
<SheetTitle>Add Repository</SheetTitle>
|
|
258
|
+
<SheetDescription>
|
|
259
|
+
Enter the absolute path to a repository containing PRD documents.
|
|
260
|
+
</SheetDescription>
|
|
261
|
+
</SheetHeader>
|
|
262
|
+
|
|
263
|
+
<form id="add-repo-form" class="min-h-0 flex-1 space-y-4 overflow-y-auto px-6" @submit.prevent="handleAddRepo">
|
|
264
|
+
<div class="space-y-2">
|
|
265
|
+
<label for="repo-path" class="text-sm font-medium">
|
|
266
|
+
Repository Path
|
|
267
|
+
</label>
|
|
268
|
+
<div class="flex gap-2">
|
|
269
|
+
<Input
|
|
270
|
+
id="repo-path"
|
|
271
|
+
v-model="newRepoPath"
|
|
272
|
+
placeholder="/path/to/your/project"
|
|
273
|
+
:aria-invalid="!!addError"
|
|
274
|
+
:disabled="isAdding"
|
|
275
|
+
class="flex-1"
|
|
276
|
+
/>
|
|
277
|
+
<Button
|
|
278
|
+
type="button"
|
|
279
|
+
variant="outline"
|
|
280
|
+
size="icon"
|
|
281
|
+
:disabled="isAdding"
|
|
282
|
+
@click="openBrowser"
|
|
283
|
+
>
|
|
284
|
+
<Folder class="size-4" />
|
|
285
|
+
</Button>
|
|
286
|
+
</div>
|
|
287
|
+
<p v-if="addError" class="text-sm text-destructive">
|
|
288
|
+
{{ addError }}
|
|
289
|
+
</p>
|
|
290
|
+
<p class="text-xs text-muted-foreground">
|
|
291
|
+
The repository should contain a <code class="rounded bg-muted px-1">docs/prd/</code> directory with markdown files.
|
|
292
|
+
</p>
|
|
293
|
+
</div>
|
|
294
|
+
|
|
295
|
+
<!-- Directory Browser -->
|
|
296
|
+
<div v-if="showBrowser" class="space-y-2 rounded-md border p-3">
|
|
297
|
+
<div class="flex items-center gap-2 text-sm">
|
|
298
|
+
<button
|
|
299
|
+
type="button"
|
|
300
|
+
class="hover:text-foreground text-muted-foreground"
|
|
301
|
+
:disabled="browserPath === '/'"
|
|
302
|
+
@click="navigateUp"
|
|
303
|
+
>
|
|
304
|
+
..
|
|
305
|
+
</button>
|
|
306
|
+
<span class="flex-1 truncate font-mono text-xs text-muted-foreground">
|
|
307
|
+
{{ browserPath }}
|
|
308
|
+
</span>
|
|
309
|
+
<Button
|
|
310
|
+
type="button"
|
|
311
|
+
variant="ghost"
|
|
312
|
+
size="sm"
|
|
313
|
+
class="h-7 text-xs"
|
|
314
|
+
@click="selectDirectory(browserPath)"
|
|
315
|
+
>
|
|
316
|
+
Select
|
|
317
|
+
</Button>
|
|
318
|
+
</div>
|
|
319
|
+
<div class="max-h-[200px] overflow-y-auto">
|
|
320
|
+
<div v-if="browserLoading" class="py-2 text-center text-sm text-muted-foreground">
|
|
321
|
+
Loading...
|
|
322
|
+
</div>
|
|
323
|
+
<div v-else-if="!browserDirs.length" class="py-2 text-center text-sm text-muted-foreground">
|
|
324
|
+
No subdirectories
|
|
325
|
+
</div>
|
|
326
|
+
<button
|
|
327
|
+
v-for="dir in browserDirs"
|
|
328
|
+
v-else
|
|
329
|
+
:key="dir.path"
|
|
330
|
+
type="button"
|
|
331
|
+
class="flex w-full items-center gap-2 rounded px-2 py-1 text-sm hover:bg-accent"
|
|
332
|
+
@click="browseDirectory(dir.path)"
|
|
333
|
+
>
|
|
334
|
+
<Folder class="size-4 text-muted-foreground" />
|
|
335
|
+
<span class="truncate">{{ dir.name }}</span>
|
|
336
|
+
</button>
|
|
337
|
+
</div>
|
|
338
|
+
</div>
|
|
339
|
+
</form>
|
|
340
|
+
|
|
341
|
+
<SheetFooter class="px-6 pb-6">
|
|
342
|
+
<Button
|
|
343
|
+
type="button"
|
|
344
|
+
variant="outline"
|
|
345
|
+
:disabled="isAdding"
|
|
346
|
+
@click="handleDialogClose"
|
|
347
|
+
>
|
|
348
|
+
Cancel
|
|
349
|
+
</Button>
|
|
350
|
+
<Button type="submit" form="add-repo-form" :disabled="isAdding">
|
|
351
|
+
<span v-if="isAdding">Adding...</span>
|
|
352
|
+
<span v-else>Add Repository</span>
|
|
353
|
+
</Button>
|
|
354
|
+
</SheetFooter>
|
|
355
|
+
</SheetContent>
|
|
356
|
+
</Sheet>
|
|
357
|
+
</div>
|
|
358
|
+
</template>
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { FileText, Loader2, AlertCircle, RefreshCw } from 'lucide-vue-next'
|
|
3
|
+
import { ScrollArea } from '~/components/ui/scroll-area'
|
|
4
|
+
import { Badge } from '~/components/ui/badge'
|
|
5
|
+
import { Button } from '~/components/ui/button'
|
|
6
|
+
|
|
7
|
+
const route = useRoute()
|
|
8
|
+
const { prds, prdsStatus, refreshPrds } = usePrd()
|
|
9
|
+
const { currentRepoId } = useRepos()
|
|
10
|
+
|
|
11
|
+
// Determine which PRD is currently selected based on route
|
|
12
|
+
const currentPrdSlug = computed(() => {
|
|
13
|
+
return route.params.prd as string | undefined
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
// Check if a PRD is the current one
|
|
17
|
+
function isActive(slug: string): boolean {
|
|
18
|
+
return currentPrdSlug.value === slug
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<template>
|
|
23
|
+
<aside class="flex h-full w-64 flex-col border-r border-border bg-background">
|
|
24
|
+
<!-- PRD List -->
|
|
25
|
+
<ScrollArea class="flex-1">
|
|
26
|
+
<div class="p-2">
|
|
27
|
+
<!-- Documents Header -->
|
|
28
|
+
<h2 class="flex h-10 items-center px-2 text-sm font-medium text-muted-foreground">Documents</h2>
|
|
29
|
+
<!-- Loading state -->
|
|
30
|
+
<div v-if="prdsStatus === 'pending'" class="flex items-center justify-center py-8">
|
|
31
|
+
<Loader2 class="size-5 animate-spin text-muted-foreground" />
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- No repo selected -->
|
|
35
|
+
<div v-else-if="!currentRepoId" class="px-2 py-8 text-center">
|
|
36
|
+
<p class="text-sm text-muted-foreground">
|
|
37
|
+
Select a repository to view PRDs
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<!-- Error state -->
|
|
42
|
+
<div v-else-if="prdsStatus === 'error'" class="px-2 py-8 text-center">
|
|
43
|
+
<AlertCircle class="mx-auto size-8 text-destructive/50" />
|
|
44
|
+
<p class="mt-2 text-sm text-muted-foreground">
|
|
45
|
+
Failed to load PRDs
|
|
46
|
+
</p>
|
|
47
|
+
<Button variant="ghost" size="sm" class="mt-2" @click="refreshPrds">
|
|
48
|
+
<RefreshCw class="mr-1 size-3" />
|
|
49
|
+
Retry
|
|
50
|
+
</Button>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<!-- Empty state -->
|
|
54
|
+
<div v-else-if="!prds?.length" class="px-2 py-8 text-center">
|
|
55
|
+
<FileText class="mx-auto size-8 text-muted-foreground/50" />
|
|
56
|
+
<p class="mt-2 text-sm text-muted-foreground">
|
|
57
|
+
No PRDs found
|
|
58
|
+
</p>
|
|
59
|
+
<p class="mt-1 text-xs text-muted-foreground/70">
|
|
60
|
+
Add .md files to docs/prd/
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<!-- PRD items -->
|
|
65
|
+
<nav v-else class="space-y-1">
|
|
66
|
+
<NuxtLink
|
|
67
|
+
v-for="prd in prds"
|
|
68
|
+
:key="prd.slug"
|
|
69
|
+
:to="`/${currentRepoId}/${prd.slug}`"
|
|
70
|
+
class="group flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors"
|
|
71
|
+
:class="[
|
|
72
|
+
isActive(prd.slug)
|
|
73
|
+
? 'bg-accent text-accent-foreground'
|
|
74
|
+
: 'text-muted-foreground hover:bg-accent/50 hover:text-foreground'
|
|
75
|
+
]"
|
|
76
|
+
>
|
|
77
|
+
<FileText class="size-4 shrink-0" />
|
|
78
|
+
<span class="flex-1 truncate">{{ prd.name }}</span>
|
|
79
|
+
<Badge
|
|
80
|
+
v-if="prd.hasState && prd.taskCount"
|
|
81
|
+
variant="secondary"
|
|
82
|
+
class="shrink-0 text-xs"
|
|
83
|
+
>
|
|
84
|
+
{{ prd.completedCount ?? 0 }}/{{ prd.taskCount }}
|
|
85
|
+
</Badge>
|
|
86
|
+
</NuxtLink>
|
|
87
|
+
</nav>
|
|
88
|
+
</div>
|
|
89
|
+
</ScrollArea>
|
|
90
|
+
</aside>
|
|
91
|
+
</template>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { User, Calendar, CircleDot, ExternalLink } from 'lucide-vue-next'
|
|
3
|
+
import { Badge } from '~/components/ui/badge'
|
|
4
|
+
import type { PrdMetadata } from '~/types/prd'
|
|
5
|
+
|
|
6
|
+
const props = defineProps<{
|
|
7
|
+
metadata: PrdMetadata
|
|
8
|
+
}>()
|
|
9
|
+
|
|
10
|
+
// Determine status color based on status text
|
|
11
|
+
const statusVariant = computed(() => {
|
|
12
|
+
const status = props.metadata.status?.toLowerCase()
|
|
13
|
+
if (!status) return 'secondary'
|
|
14
|
+
if (status.includes('complete') || status.includes('done')) return 'default'
|
|
15
|
+
if (status.includes('progress') || status.includes('active')) return 'default'
|
|
16
|
+
if (status.includes('draft')) return 'secondary'
|
|
17
|
+
if (status.includes('blocked') || status.includes('paused')) return 'destructive'
|
|
18
|
+
return 'secondary'
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// Check if there's any metadata to display
|
|
22
|
+
const hasMetadata = computed(() => {
|
|
23
|
+
return props.metadata.author ||
|
|
24
|
+
props.metadata.date ||
|
|
25
|
+
props.metadata.status ||
|
|
26
|
+
props.metadata.shortcutStory
|
|
27
|
+
})
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<div v-if="hasMetadata" class="flex flex-wrap items-center gap-3 text-sm">
|
|
32
|
+
<!-- Author -->
|
|
33
|
+
<div v-if="metadata.author" class="flex items-center gap-1.5 text-muted-foreground">
|
|
34
|
+
<User class="size-3.5" />
|
|
35
|
+
<span>{{ metadata.author }}</span>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<!-- Date -->
|
|
39
|
+
<div v-if="metadata.date" class="flex items-center gap-1.5 text-muted-foreground">
|
|
40
|
+
<Calendar class="size-3.5" />
|
|
41
|
+
<span>{{ metadata.date }}</span>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<!-- Status -->
|
|
45
|
+
<Badge v-if="metadata.status" :variant="statusVariant" class="gap-1">
|
|
46
|
+
<CircleDot class="size-3" />
|
|
47
|
+
{{ metadata.status }}
|
|
48
|
+
</Badge>
|
|
49
|
+
|
|
50
|
+
<!-- Shortcut Story Link -->
|
|
51
|
+
<a
|
|
52
|
+
v-if="metadata.shortcutStory && metadata.shortcutUrl"
|
|
53
|
+
:href="metadata.shortcutUrl"
|
|
54
|
+
target="_blank"
|
|
55
|
+
rel="noopener noreferrer"
|
|
56
|
+
class="inline-flex items-center gap-1.5 text-primary hover:underline"
|
|
57
|
+
>
|
|
58
|
+
<ExternalLink class="size-3.5" />
|
|
59
|
+
<span>{{ metadata.shortcutStory }}</span>
|
|
60
|
+
</a>
|
|
61
|
+
<span
|
|
62
|
+
v-else-if="metadata.shortcutStory"
|
|
63
|
+
class="flex items-center gap-1.5 text-muted-foreground"
|
|
64
|
+
>
|
|
65
|
+
<ExternalLink class="size-3.5" />
|
|
66
|
+
<span>{{ metadata.shortcutStory }}</span>
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|