@prmichaelsen/acp-visualizer 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/README.md +68 -0
- package/agent/commands/acp.clarification-address.md +417 -0
- package/agent/commands/acp.clarification-capture.md +386 -0
- package/agent/commands/acp.clarification-create.md +437 -0
- package/agent/commands/acp.clarifications-research.md +326 -0
- package/agent/commands/acp.command-create.md +432 -0
- package/agent/commands/acp.design-create.md +286 -0
- package/agent/commands/acp.design-reference.md +355 -0
- package/agent/commands/acp.handoff.md +270 -0
- package/agent/commands/acp.index.md +423 -0
- package/agent/commands/acp.init.md +546 -0
- package/agent/commands/acp.package-create.md +895 -0
- package/agent/commands/acp.package-info.md +212 -0
- package/agent/commands/acp.package-install.md +539 -0
- package/agent/commands/acp.package-list.md +280 -0
- package/agent/commands/acp.package-publish.md +541 -0
- package/agent/commands/acp.package-remove.md +293 -0
- package/agent/commands/acp.package-search.md +307 -0
- package/agent/commands/acp.package-update.md +361 -0
- package/agent/commands/acp.package-validate.md +540 -0
- package/agent/commands/acp.pattern-create.md +386 -0
- package/agent/commands/acp.plan.md +587 -0
- package/agent/commands/acp.proceed.md +882 -0
- package/agent/commands/acp.project-create.md +675 -0
- package/agent/commands/acp.project-info.md +312 -0
- package/agent/commands/acp.project-list.md +226 -0
- package/agent/commands/acp.project-remove.md +379 -0
- package/agent/commands/acp.project-set.md +227 -0
- package/agent/commands/acp.project-update.md +307 -0
- package/agent/commands/acp.projects-restore.md +228 -0
- package/agent/commands/acp.projects-sync.md +347 -0
- package/agent/commands/acp.report.md +407 -0
- package/agent/commands/acp.resume.md +239 -0
- package/agent/commands/acp.sessions.md +301 -0
- package/agent/commands/acp.status.md +293 -0
- package/agent/commands/acp.sync.md +364 -0
- package/agent/commands/acp.task-create.md +500 -0
- package/agent/commands/acp.update.md +302 -0
- package/agent/commands/acp.validate.md +466 -0
- package/agent/commands/acp.version-check-for-updates.md +276 -0
- package/agent/commands/acp.version-check.md +191 -0
- package/agent/commands/acp.version-update.md +289 -0
- package/agent/commands/command.template.md +339 -0
- package/agent/commands/git.commit.md +526 -0
- package/agent/commands/git.init.md +514 -0
- package/agent/commands/tanstack-cloudflare.deploy.md +272 -0
- package/agent/commands/tanstack-cloudflare.tail.md +275 -0
- package/agent/design/.gitkeep +0 -0
- package/agent/design/design.template.md +154 -0
- package/agent/design/local.dashboard-layout-routing.md +288 -0
- package/agent/design/local.data-model-yaml-parsing.md +310 -0
- package/agent/design/local.search-filtering.md +331 -0
- package/agent/design/local.server-api-auto-refresh.md +235 -0
- package/agent/design/local.table-tree-views.md +299 -0
- package/agent/design/local.visualizer-requirements.md +349 -0
- package/agent/design/requirements.template.md +387 -0
- package/agent/index/.gitkeep +0 -0
- package/agent/index/acp.core.yaml +137 -0
- package/agent/index/local.main.template.yaml +37 -0
- package/agent/manifest.template.yaml +13 -0
- package/agent/manifest.yaml +302 -0
- package/agent/milestones/.gitkeep +0 -0
- package/agent/milestones/milestone-1-project-scaffold-data-pipeline.md +67 -0
- package/agent/milestones/milestone-1-{title}.template.md +206 -0
- package/agent/milestones/milestone-2-dashboard-views-interaction.md +79 -0
- package/agent/package.template.yaml +86 -0
- package/agent/patterns/.gitkeep +0 -0
- package/agent/patterns/bootstrap.template.md +1237 -0
- package/agent/patterns/pattern.template.md +382 -0
- package/agent/patterns/tanstack-cloudflare.acl-permissions.md +332 -0
- package/agent/patterns/tanstack-cloudflare.action-bar-item.md +416 -0
- package/agent/patterns/tanstack-cloudflare.api-route-handlers.md +401 -0
- package/agent/patterns/tanstack-cloudflare.auth-session-management.md +387 -0
- package/agent/patterns/tanstack-cloudflare.card-and-list.md +271 -0
- package/agent/patterns/tanstack-cloudflare.chat-engine.md +353 -0
- package/agent/patterns/tanstack-cloudflare.confirmation-tokens.md +346 -0
- package/agent/patterns/tanstack-cloudflare.durable-objects-websocket.md +516 -0
- package/agent/patterns/tanstack-cloudflare.email-service.md +431 -0
- package/agent/patterns/tanstack-cloudflare.expander.md +98 -0
- package/agent/patterns/tanstack-cloudflare.fcm-push.md +115 -0
- package/agent/patterns/tanstack-cloudflare.firebase-anonymous-sessions.md +441 -0
- package/agent/patterns/tanstack-cloudflare.firebase-auth.md +348 -0
- package/agent/patterns/tanstack-cloudflare.firebase-firestore.md +550 -0
- package/agent/patterns/tanstack-cloudflare.firebase-storage.md +369 -0
- package/agent/patterns/tanstack-cloudflare.form-controls.md +145 -0
- package/agent/patterns/tanstack-cloudflare.global-search-context.md +93 -0
- package/agent/patterns/tanstack-cloudflare.image-carousel.md +126 -0
- package/agent/patterns/tanstack-cloudflare.library-services.md +553 -0
- package/agent/patterns/tanstack-cloudflare.lightbox.md +169 -0
- package/agent/patterns/tanstack-cloudflare.markdown-content.md +115 -0
- package/agent/patterns/tanstack-cloudflare.mention-suggestions.md +98 -0
- package/agent/patterns/tanstack-cloudflare.modal.md +156 -0
- package/agent/patterns/tanstack-cloudflare.nextjs-to-tanstack-routing.md +461 -0
- package/agent/patterns/tanstack-cloudflare.notifications-engine.md +151 -0
- package/agent/patterns/tanstack-cloudflare.oauth-token-refresh.md +90 -0
- package/agent/patterns/tanstack-cloudflare.og-metadata.md +296 -0
- package/agent/patterns/tanstack-cloudflare.pagination.md +442 -0
- package/agent/patterns/tanstack-cloudflare.pill-input.md +220 -0
- package/agent/patterns/tanstack-cloudflare.provider-adapter.md +401 -0
- package/agent/patterns/tanstack-cloudflare.rate-limiting.md +323 -0
- package/agent/patterns/tanstack-cloudflare.scheduled-tasks.md +338 -0
- package/agent/patterns/tanstack-cloudflare.searchable-settings.md +375 -0
- package/agent/patterns/tanstack-cloudflare.slide-over.md +129 -0
- package/agent/patterns/tanstack-cloudflare.ssr-preload.md +571 -0
- package/agent/patterns/tanstack-cloudflare.third-party-api-integration.md +508 -0
- package/agent/patterns/tanstack-cloudflare.toast-system.md +142 -0
- package/agent/patterns/tanstack-cloudflare.unified-header.md +280 -0
- package/agent/patterns/tanstack-cloudflare.user-scoped-collections.md +628 -0
- package/agent/patterns/tanstack-cloudflare.websocket-manager.md +237 -0
- package/agent/patterns/tanstack-cloudflare.wrangler-configuration.md +358 -0
- package/agent/patterns/tanstack-cloudflare.zod-schema-validation.md +336 -0
- package/agent/progress.template.yaml +161 -0
- package/agent/progress.yaml +145 -0
- package/agent/schemas/package.schema.yaml +276 -0
- package/agent/scripts/acp.common.sh +1781 -0
- package/agent/scripts/acp.install.sh +333 -0
- package/agent/scripts/acp.package-create.sh +924 -0
- package/agent/scripts/acp.package-info.sh +288 -0
- package/agent/scripts/acp.package-install.sh +893 -0
- package/agent/scripts/acp.package-list.sh +311 -0
- package/agent/scripts/acp.package-publish.sh +420 -0
- package/agent/scripts/acp.package-remove.sh +348 -0
- package/agent/scripts/acp.package-search.sh +156 -0
- package/agent/scripts/acp.package-update.sh +517 -0
- package/agent/scripts/acp.package-validate.sh +1018 -0
- package/agent/scripts/acp.uninstall.sh +85 -0
- package/agent/scripts/acp.version-check-for-updates.sh +98 -0
- package/agent/scripts/acp.version-check.sh +47 -0
- package/agent/scripts/acp.version-update.sh +176 -0
- package/agent/scripts/acp.yaml-parser.sh +985 -0
- package/agent/scripts/acp.yaml-validate.sh +205 -0
- package/agent/tasks/.gitkeep +0 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-1-initialize-tanstack-start-project.md +210 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-2-implement-data-model-yaml-parser.md +294 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-3-build-server-api-data-loading.md +193 -0
- package/agent/tasks/milestone-1-project-scaffold-data-pipeline/task-4-add-auto-refresh-sse.md +262 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-10-polish-integration-testing.md +156 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-5-build-dashboard-layout-routing.md +178 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-6-build-overview-page.md +141 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-7-implement-milestone-table-view.md +153 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-8-implement-milestone-tree-view.md +174 -0
- package/agent/tasks/milestone-2-dashboard-views-interaction/task-9-implement-search-filtering.md +233 -0
- package/agent/tasks/task-1-{title}.template.md +244 -0
- package/bin/visualize.mjs +84 -0
- package/package.json +48 -0
- package/src/components/ExtraFieldsBadge.tsx +15 -0
- package/src/components/FilterBar.tsx +33 -0
- package/src/components/Header.tsx +23 -0
- package/src/components/MilestoneTable.tsx +167 -0
- package/src/components/MilestoneTree.tsx +84 -0
- package/src/components/ProgressBar.tsx +20 -0
- package/src/components/SearchInput.tsx +22 -0
- package/src/components/Sidebar.tsx +54 -0
- package/src/components/StatusBadge.tsx +23 -0
- package/src/components/StatusDot.tsx +12 -0
- package/src/components/TaskList.tsx +36 -0
- package/src/components/ViewToggle.tsx +31 -0
- package/src/lib/config.ts +8 -0
- package/src/lib/file-watcher.ts +43 -0
- package/src/lib/search.ts +48 -0
- package/src/lib/types.ts +73 -0
- package/src/lib/useAutoRefresh.ts +31 -0
- package/src/lib/useCollapse.ts +31 -0
- package/src/lib/useFilteredData.ts +55 -0
- package/src/lib/yaml-loader-real.spec.ts +47 -0
- package/src/lib/yaml-loader.spec.ts +201 -0
- package/src/lib/yaml-loader.ts +265 -0
- package/src/routeTree.gen.ts +140 -0
- package/src/router.tsx +10 -0
- package/src/routes/__root.tsx +75 -0
- package/src/routes/api/watch.ts +29 -0
- package/src/routes/index.tsx +115 -0
- package/src/routes/milestones.tsx +50 -0
- package/src/routes/search.tsx +84 -0
- package/src/routes/tasks.tsx +63 -0
- package/src/services/progress-database.service.ts +46 -0
- package/src/styles.css +25 -0
- package/tsconfig.json +24 -0
- package/vite.config.ts +16 -0
- package/vitest.config.ts +27 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { Fragment, useState } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
createColumnHelper,
|
|
4
|
+
useReactTable,
|
|
5
|
+
getCoreRowModel,
|
|
6
|
+
getSortedRowModel,
|
|
7
|
+
flexRender,
|
|
8
|
+
type SortingState,
|
|
9
|
+
} from '@tanstack/react-table'
|
|
10
|
+
import { ChevronDown, ChevronRight, ArrowUpDown } from 'lucide-react'
|
|
11
|
+
import { StatusBadge } from './StatusBadge'
|
|
12
|
+
import { ProgressBar } from './ProgressBar'
|
|
13
|
+
import { TaskList } from './TaskList'
|
|
14
|
+
import type { Milestone, Task } from '@/lib/types'
|
|
15
|
+
|
|
16
|
+
const columnHelper = createColumnHelper<Milestone>()
|
|
17
|
+
|
|
18
|
+
interface MilestoneTableProps {
|
|
19
|
+
milestones: Milestone[]
|
|
20
|
+
tasks: Record<string, Task[]>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function MilestoneTable({ milestones, tasks }: MilestoneTableProps) {
|
|
24
|
+
const [sorting, setSorting] = useState<SortingState>([])
|
|
25
|
+
const [expanded, setExpanded] = useState<Set<string>>(new Set())
|
|
26
|
+
|
|
27
|
+
const toggle = (id: string) => {
|
|
28
|
+
setExpanded((prev) => {
|
|
29
|
+
const next = new Set(prev)
|
|
30
|
+
next.has(id) ? next.delete(id) : next.add(id)
|
|
31
|
+
return next
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const columns = [
|
|
36
|
+
columnHelper.display({
|
|
37
|
+
id: 'expand',
|
|
38
|
+
cell: (info) => (
|
|
39
|
+
<button
|
|
40
|
+
onClick={() => toggle(info.row.original.id)}
|
|
41
|
+
className="p-0.5 text-gray-500 hover:text-gray-300"
|
|
42
|
+
>
|
|
43
|
+
{expanded.has(info.row.original.id) ? (
|
|
44
|
+
<ChevronDown className="w-4 h-4" />
|
|
45
|
+
) : (
|
|
46
|
+
<ChevronRight className="w-4 h-4" />
|
|
47
|
+
)}
|
|
48
|
+
</button>
|
|
49
|
+
),
|
|
50
|
+
size: 32,
|
|
51
|
+
}),
|
|
52
|
+
columnHelper.accessor('name', {
|
|
53
|
+
header: 'Milestone',
|
|
54
|
+
cell: (info) => (
|
|
55
|
+
<span className="text-sm font-medium">{info.getValue()}</span>
|
|
56
|
+
),
|
|
57
|
+
}),
|
|
58
|
+
columnHelper.accessor('status', {
|
|
59
|
+
header: 'Status',
|
|
60
|
+
cell: (info) => <StatusBadge status={info.getValue()} />,
|
|
61
|
+
size: 120,
|
|
62
|
+
}),
|
|
63
|
+
columnHelper.accessor('progress', {
|
|
64
|
+
header: 'Progress',
|
|
65
|
+
cell: (info) => (
|
|
66
|
+
<div className="flex items-center gap-2">
|
|
67
|
+
<div className="w-20">
|
|
68
|
+
<ProgressBar value={info.getValue()} size="sm" />
|
|
69
|
+
</div>
|
|
70
|
+
<span className="text-xs text-gray-400 font-mono w-8">
|
|
71
|
+
{info.getValue()}%
|
|
72
|
+
</span>
|
|
73
|
+
</div>
|
|
74
|
+
),
|
|
75
|
+
size: 140,
|
|
76
|
+
}),
|
|
77
|
+
columnHelper.display({
|
|
78
|
+
id: 'tasks',
|
|
79
|
+
header: 'Tasks',
|
|
80
|
+
cell: (info) => (
|
|
81
|
+
<span className="font-mono text-xs text-gray-400">
|
|
82
|
+
{info.row.original.tasks_completed}/{info.row.original.tasks_total}
|
|
83
|
+
</span>
|
|
84
|
+
),
|
|
85
|
+
size: 80,
|
|
86
|
+
}),
|
|
87
|
+
columnHelper.accessor('started', {
|
|
88
|
+
header: 'Started',
|
|
89
|
+
cell: (info) => (
|
|
90
|
+
<span className="text-xs text-gray-500">{info.getValue() || '—'}</span>
|
|
91
|
+
),
|
|
92
|
+
size: 100,
|
|
93
|
+
}),
|
|
94
|
+
columnHelper.accessor('estimated_weeks', {
|
|
95
|
+
header: 'Est.',
|
|
96
|
+
cell: (info) => (
|
|
97
|
+
<span className="text-xs text-gray-500 font-mono">
|
|
98
|
+
{info.getValue()}w
|
|
99
|
+
</span>
|
|
100
|
+
),
|
|
101
|
+
size: 60,
|
|
102
|
+
}),
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
const table = useReactTable({
|
|
106
|
+
data: milestones,
|
|
107
|
+
columns,
|
|
108
|
+
state: { sorting },
|
|
109
|
+
onSortingChange: setSorting,
|
|
110
|
+
getCoreRowModel: getCoreRowModel(),
|
|
111
|
+
getSortedRowModel: getSortedRowModel(),
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
if (milestones.length === 0) {
|
|
115
|
+
return <p className="text-gray-600 text-sm">No milestones</p>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="border border-gray-800 rounded-lg overflow-hidden">
|
|
120
|
+
<table className="w-full">
|
|
121
|
+
<thead>
|
|
122
|
+
{table.getHeaderGroups().map((headerGroup) => (
|
|
123
|
+
<tr key={headerGroup.id} className="border-b border-gray-800 bg-gray-900/50">
|
|
124
|
+
{headerGroup.headers.map((header) => (
|
|
125
|
+
<th
|
|
126
|
+
key={header.id}
|
|
127
|
+
className="text-left text-xs text-gray-400 font-medium px-3 py-2 cursor-pointer select-none hover:text-gray-200"
|
|
128
|
+
style={{ width: header.getSize() !== 150 ? header.getSize() : undefined }}
|
|
129
|
+
onClick={header.column.getToggleSortingHandler()}
|
|
130
|
+
>
|
|
131
|
+
<div className="flex items-center gap-1">
|
|
132
|
+
{header.isPlaceholder
|
|
133
|
+
? null
|
|
134
|
+
: flexRender(header.column.columnDef.header, header.getContext())}
|
|
135
|
+
{header.column.getCanSort() && (
|
|
136
|
+
<ArrowUpDown className="w-3 h-3 text-gray-600" />
|
|
137
|
+
)}
|
|
138
|
+
</div>
|
|
139
|
+
</th>
|
|
140
|
+
))}
|
|
141
|
+
</tr>
|
|
142
|
+
))}
|
|
143
|
+
</thead>
|
|
144
|
+
<tbody>
|
|
145
|
+
{table.getRowModel().rows.map((row) => (
|
|
146
|
+
<Fragment key={row.id}>
|
|
147
|
+
<tr className="border-b border-gray-800/50 hover:bg-gray-800/30 transition-colors">
|
|
148
|
+
{row.getVisibleCells().map((cell) => (
|
|
149
|
+
<td key={cell.id} className="px-3 py-2">
|
|
150
|
+
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
151
|
+
</td>
|
|
152
|
+
))}
|
|
153
|
+
</tr>
|
|
154
|
+
{expanded.has(row.original.id) && (
|
|
155
|
+
<tr>
|
|
156
|
+
<td colSpan={columns.length} className="bg-gray-900/30 px-3 py-1">
|
|
157
|
+
<TaskList tasks={tasks[row.original.id] || []} />
|
|
158
|
+
</td>
|
|
159
|
+
</tr>
|
|
160
|
+
)}
|
|
161
|
+
</Fragment>
|
|
162
|
+
))}
|
|
163
|
+
</tbody>
|
|
164
|
+
</table>
|
|
165
|
+
</div>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { ChevronDown, ChevronRight } from 'lucide-react'
|
|
3
|
+
import { StatusBadge } from './StatusBadge'
|
|
4
|
+
import { ProgressBar } from './ProgressBar'
|
|
5
|
+
import { TaskList } from './TaskList'
|
|
6
|
+
import { useCollapse } from '@/lib/useCollapse'
|
|
7
|
+
import type { Milestone, Task } from '@/lib/types'
|
|
8
|
+
|
|
9
|
+
interface MilestoneTreeProps {
|
|
10
|
+
milestones: Milestone[]
|
|
11
|
+
tasks: Record<string, Task[]>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function MilestoneTreeRow({
|
|
15
|
+
milestone,
|
|
16
|
+
tasks,
|
|
17
|
+
expanded,
|
|
18
|
+
onToggle,
|
|
19
|
+
}: {
|
|
20
|
+
milestone: Milestone
|
|
21
|
+
tasks: Task[]
|
|
22
|
+
expanded: boolean
|
|
23
|
+
onToggle: () => void
|
|
24
|
+
}) {
|
|
25
|
+
const collapse = useCollapse(expanded)
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div className="border-b border-gray-800/50">
|
|
29
|
+
<button
|
|
30
|
+
onClick={onToggle}
|
|
31
|
+
className="w-full flex items-center gap-3 px-4 py-3 hover:bg-gray-800/30 transition-colors text-left"
|
|
32
|
+
>
|
|
33
|
+
{expanded ? (
|
|
34
|
+
<ChevronDown className="w-4 h-4 text-gray-500 shrink-0" />
|
|
35
|
+
) : (
|
|
36
|
+
<ChevronRight className="w-4 h-4 text-gray-500 shrink-0" />
|
|
37
|
+
)}
|
|
38
|
+
<span className="flex-1 text-sm font-medium">{milestone.name}</span>
|
|
39
|
+
<StatusBadge status={milestone.status} />
|
|
40
|
+
<div className="w-20">
|
|
41
|
+
<ProgressBar value={milestone.progress} size="sm" />
|
|
42
|
+
</div>
|
|
43
|
+
<span className="text-xs text-gray-500 font-mono w-12 text-right">
|
|
44
|
+
{milestone.tasks_completed}/{milestone.tasks_total}
|
|
45
|
+
</span>
|
|
46
|
+
</button>
|
|
47
|
+
<div ref={collapse.ref} style={collapse.style}>
|
|
48
|
+
<div className="px-4 pb-2">
|
|
49
|
+
<TaskList tasks={tasks} />
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function MilestoneTree({ milestones, tasks }: MilestoneTreeProps) {
|
|
57
|
+
const [expanded, setExpanded] = useState<Set<string>>(new Set())
|
|
58
|
+
|
|
59
|
+
const toggle = (id: string) => {
|
|
60
|
+
setExpanded((prev) => {
|
|
61
|
+
const next = new Set(prev)
|
|
62
|
+
next.has(id) ? next.delete(id) : next.add(id)
|
|
63
|
+
return next
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (milestones.length === 0) {
|
|
68
|
+
return <p className="text-gray-600 text-sm">No milestones</p>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<div className="border border-gray-800 rounded-lg overflow-hidden">
|
|
73
|
+
{milestones.map((milestone) => (
|
|
74
|
+
<MilestoneTreeRow
|
|
75
|
+
key={milestone.id}
|
|
76
|
+
milestone={milestone}
|
|
77
|
+
tasks={tasks[milestone.id] || []}
|
|
78
|
+
expanded={expanded.has(milestone.id)}
|
|
79
|
+
onToggle={() => toggle(milestone.id)}
|
|
80
|
+
/>
|
|
81
|
+
))}
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface ProgressBarProps {
|
|
2
|
+
value: number
|
|
3
|
+
size?: 'sm' | 'md'
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function ProgressBar({ value, size = 'md' }: ProgressBarProps) {
|
|
7
|
+
const height = size === 'sm' ? 'h-1.5' : 'h-2.5'
|
|
8
|
+
const clamped = Math.min(100, Math.max(0, value))
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className={`w-full bg-gray-800 rounded-full ${height} overflow-hidden`}>
|
|
12
|
+
<div
|
|
13
|
+
className={`${height} rounded-full transition-all duration-300 ${
|
|
14
|
+
clamped === 100 ? 'bg-green-500' : 'bg-blue-500'
|
|
15
|
+
}`}
|
|
16
|
+
style={{ width: `${clamped}%` }}
|
|
17
|
+
/>
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Search } from 'lucide-react'
|
|
2
|
+
|
|
3
|
+
interface SearchInputProps {
|
|
4
|
+
value: string
|
|
5
|
+
onChange: (value: string) => void
|
|
6
|
+
placeholder?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function SearchInput({ value, onChange, placeholder = 'Search...' }: SearchInputProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className="relative">
|
|
12
|
+
<Search className="absolute left-2.5 top-2 w-4 h-4 text-gray-500" />
|
|
13
|
+
<input
|
|
14
|
+
type="text"
|
|
15
|
+
value={value}
|
|
16
|
+
onChange={(e) => onChange(e.target.value)}
|
|
17
|
+
placeholder={placeholder}
|
|
18
|
+
className="w-full bg-gray-900 border border-gray-800 rounded-md pl-8 pr-3 py-1.5 text-sm text-gray-200 placeholder-gray-600 focus:outline-none focus:border-gray-600 transition-colors"
|
|
19
|
+
/>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Link, useRouterState } from '@tanstack/react-router'
|
|
2
|
+
import { LayoutDashboard, Flag, CheckSquare, Search } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
const navItems = [
|
|
5
|
+
{ to: '/' as const, icon: LayoutDashboard, label: 'Overview' },
|
|
6
|
+
{ to: '/milestones' as const, icon: Flag, label: 'Milestones' },
|
|
7
|
+
{ to: '/tasks' as const, icon: CheckSquare, label: 'Tasks' },
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
export function Sidebar() {
|
|
11
|
+
const location = useRouterState({ select: (s) => s.location })
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<nav className="w-56 border-r border-gray-800 bg-gray-950 flex flex-col shrink-0">
|
|
15
|
+
<div className="p-4 border-b border-gray-800">
|
|
16
|
+
<span className="text-sm font-semibold text-gray-300 tracking-wide">
|
|
17
|
+
ACP Visualizer
|
|
18
|
+
</span>
|
|
19
|
+
</div>
|
|
20
|
+
<div className="flex-1 py-2">
|
|
21
|
+
{navItems.map((item) => {
|
|
22
|
+
const isActive =
|
|
23
|
+
item.to === '/'
|
|
24
|
+
? location.pathname === '/'
|
|
25
|
+
: location.pathname.startsWith(item.to)
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<Link
|
|
29
|
+
key={item.to}
|
|
30
|
+
to={item.to}
|
|
31
|
+
className={`flex items-center gap-3 px-4 py-2 text-sm transition-colors ${
|
|
32
|
+
isActive
|
|
33
|
+
? 'text-gray-100 bg-gray-800/50'
|
|
34
|
+
: 'text-gray-400 hover:text-gray-200 hover:bg-gray-800/30'
|
|
35
|
+
}`}
|
|
36
|
+
>
|
|
37
|
+
<item.icon className="w-4 h-4" />
|
|
38
|
+
{item.label}
|
|
39
|
+
</Link>
|
|
40
|
+
)
|
|
41
|
+
})}
|
|
42
|
+
</div>
|
|
43
|
+
<div className="p-3 border-t border-gray-800">
|
|
44
|
+
<Link
|
|
45
|
+
to="/search"
|
|
46
|
+
className="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-500 bg-gray-900 border border-gray-800 rounded-md hover:text-gray-300 hover:border-gray-600 transition-colors"
|
|
47
|
+
>
|
|
48
|
+
<Search className="w-4 h-4" />
|
|
49
|
+
Search...
|
|
50
|
+
</Link>
|
|
51
|
+
</div>
|
|
52
|
+
</nav>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Status } from '@/lib/types'
|
|
2
|
+
|
|
3
|
+
const statusStyles: Record<Status, string> = {
|
|
4
|
+
completed: 'bg-green-500/15 text-green-400 border-green-500/20',
|
|
5
|
+
in_progress: 'bg-blue-500/15 text-blue-400 border-blue-500/20',
|
|
6
|
+
not_started: 'bg-gray-500/15 text-gray-500 border-gray-500/20',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const statusLabels: Record<Status, string> = {
|
|
10
|
+
completed: 'Completed',
|
|
11
|
+
in_progress: 'In Progress',
|
|
12
|
+
not_started: 'Not Started',
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function StatusBadge({ status }: { status: Status }) {
|
|
16
|
+
return (
|
|
17
|
+
<span
|
|
18
|
+
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs border ${statusStyles[status]}`}
|
|
19
|
+
>
|
|
20
|
+
{statusLabels[status]}
|
|
21
|
+
</span>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Status } from '@/lib/types'
|
|
2
|
+
|
|
3
|
+
const dotStyles: Record<Status, { symbol: string; color: string }> = {
|
|
4
|
+
completed: { symbol: '✓', color: 'text-green-400' },
|
|
5
|
+
in_progress: { symbol: '●', color: 'text-blue-400' },
|
|
6
|
+
not_started: { symbol: '○', color: 'text-gray-500' },
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function StatusDot({ status }: { status: Status }) {
|
|
10
|
+
const { symbol, color } = dotStyles[status]
|
|
11
|
+
return <span className={`${color} text-xs w-4 text-center`}>{symbol}</span>
|
|
12
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { StatusDot } from './StatusDot'
|
|
2
|
+
import { ExtraFieldsBadge } from './ExtraFieldsBadge'
|
|
3
|
+
import type { Task } from '@/lib/types'
|
|
4
|
+
|
|
5
|
+
export function TaskList({ tasks }: { tasks: Task[] }) {
|
|
6
|
+
if (tasks.length === 0) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="pl-6 py-2">
|
|
9
|
+
<span className="text-xs text-gray-600">No tasks</span>
|
|
10
|
+
</div>
|
|
11
|
+
)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className="pl-6 py-1 space-y-0.5">
|
|
16
|
+
{tasks.map((task) => (
|
|
17
|
+
<div key={task.id} className="flex items-center gap-2 py-1 text-sm">
|
|
18
|
+
<StatusDot status={task.status} />
|
|
19
|
+
<span
|
|
20
|
+
className={
|
|
21
|
+
task.status === 'completed' ? 'text-gray-500' : 'text-gray-200'
|
|
22
|
+
}
|
|
23
|
+
>
|
|
24
|
+
{task.name}
|
|
25
|
+
</span>
|
|
26
|
+
{task.notes && (
|
|
27
|
+
<span className="text-xs text-gray-600 ml-auto truncate max-w-[200px]">
|
|
28
|
+
{task.notes}
|
|
29
|
+
</span>
|
|
30
|
+
)}
|
|
31
|
+
<ExtraFieldsBadge fields={task.extra} />
|
|
32
|
+
</div>
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
interface ViewToggleProps {
|
|
2
|
+
value: 'table' | 'tree'
|
|
3
|
+
onChange: (view: 'table' | 'tree') => void
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function ViewToggle({ value, onChange }: ViewToggleProps) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="flex gap-1 bg-gray-900 border border-gray-800 rounded-lg p-0.5">
|
|
9
|
+
<button
|
|
10
|
+
onClick={() => onChange('table')}
|
|
11
|
+
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
|
12
|
+
value === 'table'
|
|
13
|
+
? 'bg-gray-700 text-gray-100'
|
|
14
|
+
: 'text-gray-500 hover:text-gray-300'
|
|
15
|
+
}`}
|
|
16
|
+
>
|
|
17
|
+
Table
|
|
18
|
+
</button>
|
|
19
|
+
<button
|
|
20
|
+
onClick={() => onChange('tree')}
|
|
21
|
+
className={`px-3 py-1 text-xs rounded-md transition-colors ${
|
|
22
|
+
value === 'tree'
|
|
23
|
+
? 'bg-gray-700 text-gray-100'
|
|
24
|
+
: 'text-gray-500 hover:text-gray-300'
|
|
25
|
+
}`}
|
|
26
|
+
>
|
|
27
|
+
Tree
|
|
28
|
+
</button>
|
|
29
|
+
</div>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the path to progress.yaml from (in order):
|
|
3
|
+
* 1. PROGRESS_YAML_PATH environment variable
|
|
4
|
+
* 2. Default: ./agent/progress.yaml (relative to cwd)
|
|
5
|
+
*/
|
|
6
|
+
export function getProgressYamlPath(): string {
|
|
7
|
+
return process.env.PROGRESS_YAML_PATH || './agent/progress.yaml'
|
|
8
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { watch } from 'fs'
|
|
2
|
+
import { getProgressYamlPath } from './config'
|
|
3
|
+
|
|
4
|
+
type Controller = ReadableStreamDefaultController
|
|
5
|
+
|
|
6
|
+
let watcher: {
|
|
7
|
+
addClient: (controller: Controller) => void
|
|
8
|
+
removeClient: (controller: Controller) => void
|
|
9
|
+
} | null = null
|
|
10
|
+
|
|
11
|
+
export function getFileWatcher() {
|
|
12
|
+
if (watcher) return watcher
|
|
13
|
+
|
|
14
|
+
const filePath = getProgressYamlPath()
|
|
15
|
+
const clients = new Set<Controller>()
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
watch(filePath, (eventType) => {
|
|
19
|
+
if (eventType === 'change') {
|
|
20
|
+
for (const controller of clients) {
|
|
21
|
+
try {
|
|
22
|
+
controller.enqueue(new TextEncoder().encode('data: refresh\n\n'))
|
|
23
|
+
} catch {
|
|
24
|
+
clients.delete(controller)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.warn('[FileWatcher] Could not watch file:', err)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
watcher = {
|
|
34
|
+
addClient(controller: Controller) {
|
|
35
|
+
clients.add(controller)
|
|
36
|
+
},
|
|
37
|
+
removeClient(controller: Controller) {
|
|
38
|
+
clients.delete(controller)
|
|
39
|
+
},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return watcher
|
|
43
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import Fuse from 'fuse.js'
|
|
2
|
+
import type { ProgressData, Milestone, Task } from './types'
|
|
3
|
+
|
|
4
|
+
export interface SearchItem {
|
|
5
|
+
type: 'milestone' | 'task'
|
|
6
|
+
milestone: Milestone
|
|
7
|
+
task?: Task
|
|
8
|
+
name: string
|
|
9
|
+
notes: string
|
|
10
|
+
extra: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildSearchIndex(data: ProgressData) {
|
|
14
|
+
const items: SearchItem[] = []
|
|
15
|
+
|
|
16
|
+
for (const milestone of data.milestones) {
|
|
17
|
+
items.push({
|
|
18
|
+
type: 'milestone',
|
|
19
|
+
milestone,
|
|
20
|
+
name: milestone.name,
|
|
21
|
+
notes: milestone.notes,
|
|
22
|
+
extra: JSON.stringify(milestone.extra),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const tasks = data.tasks[milestone.id] || []
|
|
26
|
+
for (const task of tasks) {
|
|
27
|
+
items.push({
|
|
28
|
+
type: 'task',
|
|
29
|
+
milestone,
|
|
30
|
+
task,
|
|
31
|
+
name: task.name,
|
|
32
|
+
notes: task.notes,
|
|
33
|
+
extra: JSON.stringify(task.extra),
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return new Fuse(items, {
|
|
39
|
+
keys: [
|
|
40
|
+
{ name: 'name', weight: 2 },
|
|
41
|
+
{ name: 'notes', weight: 1 },
|
|
42
|
+
{ name: 'extra', weight: 0.5 },
|
|
43
|
+
],
|
|
44
|
+
threshold: 0.4,
|
|
45
|
+
includeScore: true,
|
|
46
|
+
ignoreLocation: true,
|
|
47
|
+
})
|
|
48
|
+
}
|
package/src/lib/types.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Status values for milestones and tasks */
|
|
2
|
+
export type Status = 'completed' | 'in_progress' | 'not_started'
|
|
3
|
+
|
|
4
|
+
/** Unknown properties from agent-maintained YAML are preserved here */
|
|
5
|
+
export type ExtraFields = Record<string, unknown>
|
|
6
|
+
|
|
7
|
+
export interface ProgressData {
|
|
8
|
+
project: ProjectMetadata
|
|
9
|
+
milestones: Milestone[]
|
|
10
|
+
tasks: Record<string, Task[]> // keyed by milestone_id
|
|
11
|
+
recent_work: WorkEntry[]
|
|
12
|
+
next_steps: string[]
|
|
13
|
+
notes: string[]
|
|
14
|
+
current_blockers: string[]
|
|
15
|
+
documentation: DocumentationStats
|
|
16
|
+
progress: ProgressSummary
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ProjectMetadata {
|
|
20
|
+
name: string
|
|
21
|
+
version: string
|
|
22
|
+
started: string
|
|
23
|
+
status: Status
|
|
24
|
+
current_milestone?: string
|
|
25
|
+
description: string
|
|
26
|
+
extra: ExtraFields
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Milestone {
|
|
30
|
+
id: string
|
|
31
|
+
name: string
|
|
32
|
+
status: Status
|
|
33
|
+
progress: number // 0-100
|
|
34
|
+
started: string | null
|
|
35
|
+
completed: string | null
|
|
36
|
+
estimated_weeks: string
|
|
37
|
+
tasks_completed: number
|
|
38
|
+
tasks_total: number
|
|
39
|
+
notes: string
|
|
40
|
+
extra: ExtraFields
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface Task {
|
|
44
|
+
id: string
|
|
45
|
+
name: string
|
|
46
|
+
status: Status
|
|
47
|
+
milestone_id: string
|
|
48
|
+
file: string
|
|
49
|
+
estimated_hours: string
|
|
50
|
+
completed_date: string | null
|
|
51
|
+
notes: string
|
|
52
|
+
extra: ExtraFields
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface WorkEntry {
|
|
56
|
+
date: string
|
|
57
|
+
description: string
|
|
58
|
+
items: string[]
|
|
59
|
+
extra: ExtraFields
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface DocumentationStats {
|
|
63
|
+
design_documents: number
|
|
64
|
+
milestone_documents: number
|
|
65
|
+
pattern_documents: number
|
|
66
|
+
task_documents: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ProgressSummary {
|
|
70
|
+
planning: number
|
|
71
|
+
implementation: number
|
|
72
|
+
overall: number
|
|
73
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useRouter } from '@tanstack/react-router'
|
|
2
|
+
import { useEffect } from 'react'
|
|
3
|
+
|
|
4
|
+
export function useAutoRefresh() {
|
|
5
|
+
const router = useRouter()
|
|
6
|
+
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const eventSource = new EventSource('/api/watch')
|
|
9
|
+
|
|
10
|
+
eventSource.onmessage = () => {
|
|
11
|
+
router.invalidate()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
eventSource.onerror = () => {
|
|
15
|
+
// EventSource handles reconnection automatically
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Reconnect on tab visibility change
|
|
19
|
+
const handleVisibility = () => {
|
|
20
|
+
if (document.visibilityState === 'visible') {
|
|
21
|
+
router.invalidate()
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
document.addEventListener('visibilitychange', handleVisibility)
|
|
25
|
+
|
|
26
|
+
return () => {
|
|
27
|
+
eventSource.close()
|
|
28
|
+
document.removeEventListener('visibilitychange', handleVisibility)
|
|
29
|
+
}
|
|
30
|
+
}, [router])
|
|
31
|
+
}
|