@prmichaelsen/acp-visualizer 0.5.0 → 0.5.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/acp-visualizer",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "description": "Browser-based dashboard for visualizing ACP progress.yaml data",
6
6
  "bin": {
@@ -16,7 +16,7 @@
16
16
  "dev": "vite dev --port 3400 --host",
17
17
  "build": "vite build",
18
18
  "serve": "vite preview",
19
- "deploy": "export $(cat .env.cloudflare.local | xargs) && npm run build && wrangler deploy",
19
+ "deploy": "export $(cat .env.cloudflare.local | xargs) && VITE_HOSTED=true npm run build && wrangler deploy",
20
20
  "tail": "export $(cat .env.cloudflare.local | xargs) && wrangler tail",
21
21
  "test": "vitest",
22
22
  "test:run": "vitest run",
@@ -35,7 +35,7 @@ const statusColors: Record<Status, { bg: string; border: string; text: string }>
35
35
 
36
36
  function buildGraph(data: ProgressData): { nodes: GraphNode[]; edges: GraphEdge[]; width: number; height: number } {
37
37
  const g = new dagre.graphlib.Graph()
38
- g.setGraph({ rankdir: 'LR', nodesep: 30, ranksep: 60, marginx: 20, marginy: 20 })
38
+ g.setGraph({ rankdir: 'TB', nodesep: 30, ranksep: 60, marginx: 20, marginy: 20 })
39
39
  g.setDefaultEdgeLabel(() => ({}))
40
40
 
41
41
  // Collect all tasks with their milestone context
@@ -1,5 +1,5 @@
1
- import { HeadContent, Scripts, createRootRoute, Outlet, useRouter } from '@tanstack/react-router'
2
- import { useState, useCallback } from 'react'
1
+ import { HeadContent, Scripts, createRootRoute, Outlet, useRouter, useRouterState } from '@tanstack/react-router'
2
+ import { useState, useCallback, useEffect } from 'react'
3
3
  import { useAutoRefresh } from '../lib/useAutoRefresh'
4
4
  import { Sidebar } from '../components/Sidebar'
5
5
  import { Header } from '../components/Header'
@@ -21,17 +21,20 @@ export const Route = createRootRoute({
21
21
  let progressData: ProgressData | null = null
22
22
  let projects: AcpProject[] = []
23
23
 
24
- try {
25
- const [result, projectList] = await Promise.all([
26
- getProgressData({ data: {} }),
27
- listProjects(),
28
- ])
29
- if (result.ok) {
30
- progressData = result.data
24
+ // VITE_HOSTED mode: skip filesystem access entirely (Cloudflare Workers)
25
+ if (!import.meta.env.VITE_HOSTED) {
26
+ try {
27
+ const [result, projectList] = await Promise.all([
28
+ getProgressData({ data: {} }),
29
+ listProjects(),
30
+ ])
31
+ if (result.ok) {
32
+ progressData = result.data
33
+ }
34
+ projects = projectList
35
+ } catch {
36
+ // No filesystem available
31
37
  }
32
- projects = projectList
33
- } catch {
34
- // Cloudflare Workers or other environment without filesystem
35
38
  }
36
39
 
37
40
  return { progressData, projects }
@@ -74,18 +77,59 @@ function AutoRefresh() {
74
77
  return null
75
78
  }
76
79
 
80
+ /** Read ?repo=owner/repo from current URL search params */
81
+ function getRepoFromUrl(): { owner: string; repo: string } | null {
82
+ if (typeof window === 'undefined') return null
83
+ const params = new URLSearchParams(window.location.search)
84
+ const repo = params.get('repo')
85
+ if (!repo) return null
86
+ const parts = repo.split('/')
87
+ if (parts.length < 2) return null
88
+ return { owner: parts[0], repo: parts[1] }
89
+ }
90
+
91
+ /** Update ?repo= search param without full navigation */
92
+ function setRepoInUrl(ownerRepo: string | null) {
93
+ if (typeof window === 'undefined') return
94
+ const url = new URL(window.location.href)
95
+ if (ownerRepo) {
96
+ url.searchParams.set('repo', ownerRepo)
97
+ } else {
98
+ url.searchParams.delete('repo')
99
+ }
100
+ window.history.replaceState({}, '', url.toString())
101
+ }
102
+
77
103
  function RootLayout() {
78
104
  const context = Route.useRouteContext()
79
105
  const [progressData, setProgressData] = useState(context.progressData)
80
106
  const [currentProject, setCurrentProject] = useState<string | null>(
81
107
  context.progressData?.project.name || null,
82
108
  )
109
+ const [initialLoadDone, setInitialLoadDone] = useState(false)
110
+
111
+ // On mount, check for ?repo= param and auto-load
112
+ useEffect(() => {
113
+ if (initialLoadDone) return
114
+ setInitialLoadDone(true)
115
+
116
+ const repoParam = getRepoFromUrl()
117
+ if (repoParam && !progressData) {
118
+ fetchGitHubProgress({ data: repoParam }).then((result) => {
119
+ if (result.ok) {
120
+ setProgressData(result.data)
121
+ setCurrentProject(`${repoParam.owner}/${repoParam.repo}`)
122
+ }
123
+ })
124
+ }
125
+ }, [initialLoadDone, progressData])
83
126
 
84
127
  const handleGitHubLoad = useCallback(async (owner: string, repo: string) => {
85
128
  const result = await fetchGitHubProgress({ data: { owner, repo } })
86
129
  if (result.ok) {
87
130
  setProgressData(result.data)
88
131
  setCurrentProject(`${owner}/${repo}`)
132
+ setRepoInUrl(`${owner}/${repo}`)
89
133
  } else {
90
134
  throw new Error(result.message)
91
135
  }
@@ -99,6 +143,7 @@ function RootLayout() {
99
143
  if (result.ok) {
100
144
  setProgressData(result.data)
101
145
  setCurrentProject(projectId)
146
+ setRepoInUrl(null) // Clear repo param for local projects
102
147
  }
103
148
  }
104
149
  } catch {
@@ -12,13 +12,29 @@ function HomePage() {
12
12
  const progressData = useProgressData()
13
13
 
14
14
  if (!progressData) {
15
+ const isHosted = import.meta.env.VITE_HOSTED
16
+
15
17
  return (
16
18
  <div className="p-6">
17
- <div className="p-4 bg-red-900/20 border border-red-800 rounded-lg">
18
- <p className="text-red-400 font-medium">Failed to load progress data</p>
19
- <p className="text-red-400/70 text-sm mt-1">
20
- Check that progress.yaml exists and is valid YAML
21
- </p>
19
+ <div className="p-5 bg-gray-900/50 border border-gray-800 rounded-xl text-center max-w-md mx-auto mt-12">
20
+ <h2 className="text-lg font-semibold mb-2">ACP Progress Visualizer</h2>
21
+ {isHosted ? (
22
+ <>
23
+ <p className="text-gray-400 text-sm mb-4">
24
+ Enter a GitHub repository in the sidebar to visualize its ACP progress.
25
+ </p>
26
+ <div className="text-xs text-gray-600 space-y-1">
27
+ <p>Example: <span className="font-mono text-gray-400">prmichaelsen/agent-context-protocol</span></p>
28
+ <p>The repo must have <span className="font-mono text-gray-400">agent/progress.yaml</span></p>
29
+ </div>
30
+ </>
31
+ ) : (
32
+ <>
33
+ <p className="text-red-400/70 text-sm">
34
+ No progress.yaml found. Make sure you're in an ACP project directory.
35
+ </p>
36
+ </>
37
+ )}
22
38
  </div>
23
39
  </div>
24
40
  )
@@ -6,7 +6,7 @@ export type GitHubResult =
6
6
  | { ok: false; error: string; message: string }
7
7
 
8
8
  export const fetchGitHubProgress = createServerFn({ method: 'GET' })
9
- .validator((input: { owner: string; repo: string; branch?: string; token?: string }) => input)
9
+ .inputValidator((input: { owner: string; repo: string; branch?: string; token?: string }) => input)
10
10
  .handler(async ({ data: input }): Promise<GitHubResult> => {
11
11
  const { parseProgressYaml } = await import('../lib/yaml-loader')
12
12
 
@@ -6,7 +6,7 @@ export type ProgressResult =
6
6
  | { ok: false; error: 'FILE_NOT_FOUND' | 'PARSE_ERROR' | 'NO_FILESYSTEM'; message: string; path: string }
7
7
 
8
8
  export const getProgressData = createServerFn({ method: 'GET' })
9
- .validator((input: { path?: string }) => input)
9
+ .inputValidator((input: { path?: string }) => input)
10
10
  .handler(async ({ data: input }): Promise<ProgressResult> => {
11
11
  try {
12
12
  const fs = await import('fs')
@@ -44,7 +44,7 @@ export const listProjects = createServerFn({ method: 'GET' }).handler(
44
44
  )
45
45
 
46
46
  export const getProjectProgressPath = createServerFn({ method: 'GET' })
47
- .validator((input: { projectId: string }) => input)
47
+ .inputValidator((input: { projectId: string }) => input)
48
48
  .handler(async ({ data }): Promise<string | null> => {
49
49
  try {
50
50
  const { readFileSync, existsSync } = await import('fs')