@prmichaelsen/acp-visualizer 0.6.1 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prmichaelsen/acp-visualizer",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "Browser-based dashboard for visualizing ACP progress.yaml data",
6
6
  "bin": {
@@ -1,12 +1,73 @@
1
1
  import ReactMarkdown from 'react-markdown'
2
2
  import rehypeHighlight from 'rehype-highlight'
3
+ import { Link } from '@tanstack/react-router'
4
+ import type { ProgressData } from '../lib/types'
3
5
 
4
6
  interface MarkdownContentProps {
5
7
  content: string
6
8
  className?: string
9
+ basePath?: string
10
+ linkMap?: Record<string, string>
7
11
  }
8
12
 
9
- export function MarkdownContent({ content, className }: MarkdownContentProps) {
13
+ /**
14
+ * Resolve a relative path against a base file path.
15
+ * e.g. resolvePath("agent/milestones/milestone-3.md", "../tasks/task-11.md")
16
+ * → "agent/tasks/task-11.md"
17
+ */
18
+ export function resolvePath(base: string, relative: string): string {
19
+ const dir = base.split('/').slice(0, -1)
20
+ const relParts = relative.split('/')
21
+ const result = [...dir]
22
+ for (const part of relParts) {
23
+ if (part === '..') result.pop()
24
+ else if (part !== '.') result.push(part)
25
+ }
26
+ return result.join('/')
27
+ }
28
+
29
+ /**
30
+ * Build a map from file paths to visualizer routes.
31
+ */
32
+ export function buildLinkMap(data: ProgressData): Record<string, string> {
33
+ const map: Record<string, string> = {}
34
+ for (const ms of data.milestones) {
35
+ for (const task of data.tasks[ms.id] || []) {
36
+ if (task.file) {
37
+ map[task.file] = `/tasks/${task.id}`
38
+ }
39
+ }
40
+ }
41
+ return map
42
+ }
43
+
44
+ function createMarkdownLink(basePath: string, linkMap: Record<string, string>) {
45
+ return function MarkdownLink({
46
+ href,
47
+ children,
48
+ ...props
49
+ }: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
50
+ if (href && !href.startsWith('http') && !href.startsWith('#')) {
51
+ const resolved = resolvePath(basePath, href)
52
+ const route = linkMap[resolved]
53
+ if (route) {
54
+ return <Link to={route}>{children}</Link>
55
+ }
56
+ }
57
+ return (
58
+ <a href={href} {...props}>
59
+ {children}
60
+ </a>
61
+ )
62
+ }
63
+ }
64
+
65
+ export function MarkdownContent({ content, className, basePath, linkMap }: MarkdownContentProps) {
66
+ const components =
67
+ basePath && linkMap
68
+ ? { a: createMarkdownLink(basePath, linkMap) }
69
+ : undefined
70
+
10
71
  return (
11
72
  <div
12
73
  className={`prose prose-invert prose-sm max-w-none
@@ -22,7 +83,7 @@ export function MarkdownContent({ content, className }: MarkdownContentProps) {
22
83
  prose-blockquote:border-gray-700 prose-blockquote:text-gray-400
23
84
  ${className ?? ''}`}
24
85
  >
25
- <ReactMarkdown rehypePlugins={[rehypeHighlight]}>
86
+ <ReactMarkdown rehypePlugins={[rehypeHighlight]} components={components}>
26
87
  {content}
27
88
  </ReactMarkdown>
28
89
  </div>
@@ -1,11 +1,11 @@
1
1
  import { createFileRoute, Link } from '@tanstack/react-router'
2
- import { useState, useEffect } from 'react'
2
+ import { useState, useEffect, useMemo } from 'react'
3
3
  import { useProgressData } from '../contexts/ProgressContext'
4
4
  import { Breadcrumb } from '../components/Breadcrumb'
5
5
  import { DetailHeader } from '../components/DetailHeader'
6
6
  import { ProgressBar } from '../components/ProgressBar'
7
7
  import { StatusDot } from '../components/StatusDot'
8
- import { MarkdownContent } from '../components/MarkdownContent'
8
+ import { MarkdownContent, buildLinkMap } from '../components/MarkdownContent'
9
9
  import { getMarkdownContent, resolveMilestoneFile } from '../services/markdown.service'
10
10
  import type { MarkdownResult, ResolveFileResult } from '../services/markdown.service'
11
11
 
@@ -29,10 +29,12 @@ function MilestoneDetailPage() {
29
29
  const data = useProgressData()
30
30
  const [markdown, setMarkdown] = useState<string | null>(null)
31
31
  const [markdownError, setMarkdownError] = useState<string | null>(null)
32
+ const [markdownFilePath, setMarkdownFilePath] = useState<string | null>(null)
32
33
  const [loading, setLoading] = useState(true)
33
34
 
34
35
  const milestone = data?.milestones.find((m) => m.id === milestoneId)
35
36
  const tasks = data?.tasks[milestoneId] || []
37
+ const linkMap = useMemo(() => (data ? buildLinkMap(data) : {}), [data])
36
38
 
37
39
  useEffect(() => {
38
40
  if (!milestoneId) return
@@ -40,6 +42,7 @@ function MilestoneDetailPage() {
40
42
  setLoading(true)
41
43
  setMarkdown(null)
42
44
  setMarkdownError(null)
45
+ setMarkdownFilePath(null)
43
46
 
44
47
  const github = getGitHubParams()
45
48
 
@@ -51,6 +54,7 @@ function MilestoneDetailPage() {
51
54
  return
52
55
  }
53
56
 
57
+ setMarkdownFilePath(resolveResult.filePath)
54
58
  return getMarkdownContent({ data: { filePath: resolveResult.filePath, github } })
55
59
  .then((mdResult: MarkdownResult) => {
56
60
  if (mdResult.ok) {
@@ -111,7 +115,7 @@ function MilestoneDetailPage() {
111
115
  {loading ? (
112
116
  <p className="text-sm text-gray-600">Loading document...</p>
113
117
  ) : markdown ? (
114
- <MarkdownContent content={markdown} />
118
+ <MarkdownContent content={markdown} basePath={markdownFilePath ?? undefined} linkMap={linkMap} />
115
119
  ) : markdownError ? (
116
120
  <div className="bg-gray-900/50 border border-gray-800 rounded-xl p-4 text-sm text-gray-500">
117
121
  No document found — {markdownError}
@@ -3,7 +3,7 @@ import { useState, useEffect, useMemo } from 'react'
3
3
  import { useProgressData } from '../contexts/ProgressContext'
4
4
  import { Breadcrumb } from '../components/Breadcrumb'
5
5
  import { DetailHeader } from '../components/DetailHeader'
6
- import { MarkdownContent } from '../components/MarkdownContent'
6
+ import { MarkdownContent, buildLinkMap } from '../components/MarkdownContent'
7
7
  import { getMarkdownContent } from '../services/markdown.service'
8
8
  import { resolveTaskFile } from '../services/markdown.service'
9
9
  import type { MarkdownResult } from '../services/markdown.service'
@@ -83,6 +83,9 @@ function TaskDetailPage() {
83
83
  })
84
84
  }, [task])
85
85
 
86
+ const linkMap = useMemo(() => (data ? buildLinkMap(data) : {}), [data])
87
+ const taskFilePath = useMemo(() => resolveTaskFile(task), [task])
88
+
86
89
  if (!data || !task || !milestone) {
87
90
  return (
88
91
  <div className="p-6">
@@ -130,7 +133,7 @@ function TaskDetailPage() {
130
133
  {loading ? (
131
134
  <p className="text-sm text-gray-600">Loading document...</p>
132
135
  ) : markdown ? (
133
- <MarkdownContent content={markdown} />
136
+ <MarkdownContent content={markdown} basePath={taskFilePath ?? undefined} linkMap={linkMap} />
134
137
  ) : markdownError ? (
135
138
  <div className="bg-gray-900/50 border border-gray-800 rounded-xl p-4 text-sm text-gray-500">
136
139
  No document found — {markdownError}