coaia-visualizer 1.6.3 ā 1.6.5
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/Dockerfile +61 -0
- package/Dockerfile.app +50 -0
- package/README.md +2 -1
- package/cli.ts +11 -5
- package/components/asterion-foundation-card.tsx +175 -0
- package/components/asterion-session-context-badge.tsx +122 -0
- package/components/asterion-session-lineage-card.tsx +119 -0
- package/components/chart-detail-editable.tsx +2 -0
- package/components/chart-detail.tsx +9 -1
- package/components/edit-action-step.tsx +2 -0
- package/components/github-provenance.tsx +226 -0
- package/components/metadata-projections.tsx +50 -16
- package/components/narrative-beats.tsx +16 -3
- package/components/relation-graph.tsx +91 -27
- package/dist/cli.js +9 -5
- package/docker-build-push.sh +20 -0
- package/docker-entrypoint.sh +27 -0
- package/index.tsx +39 -2
- package/lib/asterion-metadata.ts +673 -0
- package/lib/chart-editor.ts +8 -3
- package/lib/github-provenance.ts +316 -0
- package/lib/jsonl-parser.ts +7 -2
- package/lib/types.ts +112 -2
- package/mcp/test_mcp/.gemini/settings.json +18 -0
- package/next-env.d.ts +1 -1
- package/package.json +14 -13
- package/rispecs/README.md +170 -0
- package/rispecs/accountability-responsibility.rispec.md +110 -0
- package/rispecs/api-mcp-interface.spec.md +287 -0
- package/rispecs/app.spec.md +364 -0
- package/rispecs/chart-editing-workflow.spec.md +297 -0
- package/rispecs/chart-visualization-hierarchy.spec.md +235 -0
- package/rispecs/cli-mode.spec.md +224 -0
- package/rispecs/github-project-runtime-memory-integration.spec.md +381 -0
- package/rispecs/jsonl-parsing-data-types.spec.md +243 -0
- package/rispecs/narrative-beats-display.spec.md +189 -0
- package/rispecs/pde-integration.spec.md +280 -0
- package/rispecs/planning-integration.spec.md +329 -0
- package/rispecs/relation-graph-visualization.spec.md +171 -0
- package/rispecs/relation-to-mcp-structural-thinking.kin.md +65 -0
- package/rispecs/ui-component-library.spec.md +258 -0
- package/mcp/dist/api-client.d.ts +0 -138
- package/mcp/dist/api-client.d.ts.map +0 -1
- package/mcp/dist/api-client.js +0 -115
- package/mcp/dist/api-client.js.map +0 -1
- package/mcp/dist/index.d.ts +0 -2
- package/mcp/dist/index.d.ts.map +0 -1
- package/mcp/dist/index.js +0 -286
- package/mcp/dist/index.js.map +0 -1
- package/mcp/dist/tools/index.d.ts +0 -18
- package/mcp/dist/tools/index.d.ts.map +0 -1
- package/mcp/dist/tools/index.js +0 -322
- package/mcp/dist/tools/index.js.map +0 -1
- package/mcp/package-lock.json +0 -210
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState } from "react"
|
|
4
|
+
import type { LucideIcon } from "lucide-react"
|
|
5
|
+
import { AlertTriangle, CheckCircle2, Cloud, ExternalLink, FolderOpen, Github, GitBranch, XCircle } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
import { Badge } from "@/components/ui/badge"
|
|
8
|
+
import type { EntityRecord } from "@/lib/types"
|
|
9
|
+
import {
|
|
10
|
+
formatGithubIssueRef,
|
|
11
|
+
formatProjectItemRef,
|
|
12
|
+
getGithubEntityProvenance,
|
|
13
|
+
getGithubIssueRef,
|
|
14
|
+
getGithubSyncState,
|
|
15
|
+
getProjectFieldEntries,
|
|
16
|
+
type GithubSyncState,
|
|
17
|
+
} from "@/lib/github-provenance"
|
|
18
|
+
import { cn } from "@/lib/utils"
|
|
19
|
+
|
|
20
|
+
interface SyncStatePillProps {
|
|
21
|
+
syncState?: GithubSyncState
|
|
22
|
+
className?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const syncStateConfig: Record<GithubSyncState, { label: string; className: string; Icon: LucideIcon }> = {
|
|
26
|
+
synced: {
|
|
27
|
+
label: "synced",
|
|
28
|
+
className: "border-emerald-500/40 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
|
|
29
|
+
Icon: CheckCircle2,
|
|
30
|
+
},
|
|
31
|
+
diverged: {
|
|
32
|
+
label: "diverged",
|
|
33
|
+
className: "border-amber-500/40 bg-amber-500/10 text-amber-700 dark:text-amber-300",
|
|
34
|
+
Icon: AlertTriangle,
|
|
35
|
+
},
|
|
36
|
+
conflict: {
|
|
37
|
+
label: "conflict",
|
|
38
|
+
className: "border-destructive/40 bg-destructive/10 text-destructive",
|
|
39
|
+
Icon: XCircle,
|
|
40
|
+
},
|
|
41
|
+
"project-only": {
|
|
42
|
+
label: "project-only",
|
|
43
|
+
className: "border-slate-500/40 bg-slate-500/10 text-slate-700 dark:text-slate-300",
|
|
44
|
+
Icon: Cloud,
|
|
45
|
+
},
|
|
46
|
+
"chart-only": {
|
|
47
|
+
label: "chart-only",
|
|
48
|
+
className: "border-slate-500/40 bg-slate-500/10 text-slate-700 dark:text-slate-300",
|
|
49
|
+
Icon: FolderOpen,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function SyncStatePill({ syncState, className }: SyncStatePillProps) {
|
|
54
|
+
if (!syncState) return null
|
|
55
|
+
|
|
56
|
+
const { label, className: stateClassName, Icon } = syncStateConfig[syncState]
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Badge variant="outline" className={cn("gap-1 text-xs", stateClassName, className)}>
|
|
60
|
+
<Icon className="w-3 h-3" />
|
|
61
|
+
{label}
|
|
62
|
+
</Badge>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface ProjectSourceBadgeProps {
|
|
67
|
+
entity: EntityRecord
|
|
68
|
+
className?: string
|
|
69
|
+
compact?: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function ProjectSourceBadge({ entity, className, compact = false }: ProjectSourceBadgeProps) {
|
|
73
|
+
const [selectedLensIndex, setSelectedLensIndex] = useState(0)
|
|
74
|
+
const provenance = getGithubEntityProvenance(entity)
|
|
75
|
+
|
|
76
|
+
if (!provenance.hasGithubMetadata) return null
|
|
77
|
+
|
|
78
|
+
const projectItems = provenance.projectItems
|
|
79
|
+
const activeLensIndex = projectItems.length > 0 ? Math.min(selectedLensIndex, projectItems.length - 1) : 0
|
|
80
|
+
const activeProjectItem = projectItems[activeLensIndex]
|
|
81
|
+
const projectRef = formatProjectItemRef(activeProjectItem)
|
|
82
|
+
const issueRef = formatGithubIssueRef(provenance.issue)
|
|
83
|
+
const projectTitle = activeProjectItem?.projectTitle
|
|
84
|
+
const fieldEntries = getProjectFieldEntries(entity, activeProjectItem)
|
|
85
|
+
const sourceLabel = getSourceLabel(provenance.source?.system, provenance.source?.toolName, provenance.lastSyncedAt)
|
|
86
|
+
const projectTooltip = [
|
|
87
|
+
projectTitle,
|
|
88
|
+
activeProjectItem?.itemId && `item ${activeProjectItem.itemId}`,
|
|
89
|
+
activeProjectItem?.projectId && `project ${activeProjectItem.projectId}`,
|
|
90
|
+
activeProjectItem?.url,
|
|
91
|
+
]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join(" Ā· ")
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div
|
|
97
|
+
className={cn(
|
|
98
|
+
"rounded-md border border-indigo-500/20 bg-indigo-500/5 p-3 text-sm",
|
|
99
|
+
compact && "p-2",
|
|
100
|
+
className,
|
|
101
|
+
)}
|
|
102
|
+
>
|
|
103
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
104
|
+
{activeProjectItem?.url ? (
|
|
105
|
+
<a href={activeProjectItem.url} target="_blank" rel="noreferrer" title={projectTooltip}>
|
|
106
|
+
<ProjectBadge label={projectRef ?? "Project bridge"} />
|
|
107
|
+
</a>
|
|
108
|
+
) : (
|
|
109
|
+
<ProjectBadge label={projectRef ?? "Project bridge"} title={projectTooltip} />
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{provenance.issue &&
|
|
113
|
+
(provenance.issue.url ? (
|
|
114
|
+
<a href={provenance.issue.url} target="_blank" rel="noreferrer" title={issueRef}>
|
|
115
|
+
<Badge variant="outline" className="gap-1 text-xs">
|
|
116
|
+
<Github className="w-3 h-3" />#{provenance.issue.number}
|
|
117
|
+
<ExternalLink className="w-3 h-3" />
|
|
118
|
+
</Badge>
|
|
119
|
+
</a>
|
|
120
|
+
) : (
|
|
121
|
+
<Badge variant="outline" className="gap-1 text-xs" title={issueRef}>
|
|
122
|
+
<Github className="w-3 h-3" />#{provenance.issue.number}
|
|
123
|
+
</Badge>
|
|
124
|
+
))}
|
|
125
|
+
|
|
126
|
+
<SyncStatePill syncState={provenance.syncState} />
|
|
127
|
+
|
|
128
|
+
{projectItems.length > 1 && (
|
|
129
|
+
<select
|
|
130
|
+
aria-label="Active GitHub project lens"
|
|
131
|
+
value={activeLensIndex}
|
|
132
|
+
onChange={(event) => setSelectedLensIndex(Number(event.target.value))}
|
|
133
|
+
className="h-7 rounded-md border border-border bg-background px-2 text-xs text-foreground"
|
|
134
|
+
>
|
|
135
|
+
{projectItems.map((item, index) => (
|
|
136
|
+
<option key={`${formatProjectItemRef(item) ?? item.itemId ?? "project"}-${index}`} value={index}>
|
|
137
|
+
{item.projectTitle ?? formatProjectItemRef(item) ?? `Project ${index + 1}`}
|
|
138
|
+
</option>
|
|
139
|
+
))}
|
|
140
|
+
</select>
|
|
141
|
+
)}
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{!compact && (projectTitle || sourceLabel || fieldEntries.length > 0) && (
|
|
145
|
+
<div className="mt-2 space-y-2 text-xs text-muted-foreground">
|
|
146
|
+
{(projectTitle || sourceLabel) && (
|
|
147
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
148
|
+
{projectTitle && <span className="font-medium text-foreground">{projectTitle}</span>}
|
|
149
|
+
{sourceLabel && <span>{sourceLabel}</span>}
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
{fieldEntries.length > 0 && (
|
|
154
|
+
<div className="flex flex-wrap gap-1.5">
|
|
155
|
+
{fieldEntries.map((field) => (
|
|
156
|
+
<span
|
|
157
|
+
key={field.key}
|
|
158
|
+
className="inline-flex max-w-full items-center gap-1 rounded-md border border-border bg-background px-2 py-1"
|
|
159
|
+
title={`${field.label}: ${field.value}`}
|
|
160
|
+
>
|
|
161
|
+
<span className="font-medium text-foreground">{field.label}</span>
|
|
162
|
+
<span className="max-w-[18rem] truncate">{field.value}</span>
|
|
163
|
+
</span>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function ProjectBadge({ label, title }: { label: string; title?: string }) {
|
|
174
|
+
return (
|
|
175
|
+
<Badge className="gap-1 bg-indigo-600 text-white hover:bg-indigo-600/90" title={title}>
|
|
176
|
+
<GitBranch className="w-3 h-3" />
|
|
177
|
+
Project: {label}
|
|
178
|
+
</Badge>
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
interface ActionStepIssueBadgeProps {
|
|
183
|
+
action: EntityRecord
|
|
184
|
+
className?: string
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function ActionStepIssueBadge({ action, className }: ActionStepIssueBadgeProps) {
|
|
188
|
+
const issue = getGithubIssueRef(action)
|
|
189
|
+
const syncState = getGithubSyncState(action)
|
|
190
|
+
const issueRef = formatGithubIssueRef(issue)
|
|
191
|
+
|
|
192
|
+
if (!issue && !syncState) return null
|
|
193
|
+
|
|
194
|
+
return (
|
|
195
|
+
<span className={cn("inline-flex flex-wrap items-center gap-1", className)}>
|
|
196
|
+
{issue &&
|
|
197
|
+
(issue.url ? (
|
|
198
|
+
<a href={issue.url} target="_blank" rel="noreferrer" title={issueRef}>
|
|
199
|
+
<Badge variant="outline" className="gap-1 text-xs">
|
|
200
|
+
<Github className="w-3 h-3" />#{issue.number}
|
|
201
|
+
<ExternalLink className="w-3 h-3" />
|
|
202
|
+
</Badge>
|
|
203
|
+
</a>
|
|
204
|
+
) : (
|
|
205
|
+
<Badge variant="outline" className="gap-1 text-xs" title={issueRef}>
|
|
206
|
+
<Github className="w-3 h-3" />#{issue.number}
|
|
207
|
+
</Badge>
|
|
208
|
+
))}
|
|
209
|
+
<SyncStatePill syncState={syncState} />
|
|
210
|
+
</span>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function getSourceLabel(system?: string, toolName?: string, lastSyncedAt?: string): string | undefined {
|
|
215
|
+
const parts = [system, toolName, formatDateTime(lastSyncedAt)].filter(Boolean)
|
|
216
|
+
return parts.length > 0 ? parts.join(" Ā· ") : undefined
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function formatDateTime(value?: string): string | undefined {
|
|
220
|
+
if (!value) return undefined
|
|
221
|
+
|
|
222
|
+
const date = new Date(value)
|
|
223
|
+
if (Number.isNaN(date.getTime())) return value
|
|
224
|
+
|
|
225
|
+
return date.toLocaleString()
|
|
226
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import { AlertTriangle, Blocks, FileJson, Github, ListChecks, Sparkles, Target } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
import { AsterionFoundationCard } from "@/components/asterion-foundation-card"
|
|
6
|
+
import { AsterionSessionLineageCard } from "@/components/asterion-session-lineage-card"
|
|
4
7
|
import { Badge } from "@/components/ui/badge"
|
|
5
8
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
|
6
|
-
import type { Chart, EntityRecord, ParsedData } from "@/lib/types"
|
|
9
|
+
import type { Chart, EntityRecord, MetadataWarning, ParsedData } from "@/lib/types"
|
|
7
10
|
|
|
8
11
|
interface MetadataProjectionsProps {
|
|
9
12
|
chart: Chart
|
|
@@ -33,6 +36,17 @@ const KNOWN_METADATA_KEYS = new Set([
|
|
|
33
36
|
"isTelescopedChart",
|
|
34
37
|
"telescopedChartId",
|
|
35
38
|
"github",
|
|
39
|
+
"asterion",
|
|
40
|
+
"asterionMetadata",
|
|
41
|
+
"asterionFoundation",
|
|
42
|
+
"foundation",
|
|
43
|
+
"foundationMetadata",
|
|
44
|
+
"sessionLineage",
|
|
45
|
+
"sessionLineageMetadata",
|
|
46
|
+
"sessionContext",
|
|
47
|
+
"sessionContextMetadata",
|
|
48
|
+
"livedContext",
|
|
49
|
+
"chronicle",
|
|
36
50
|
])
|
|
37
51
|
|
|
38
52
|
function JsonBlock({ value }: { value: unknown }) {
|
|
@@ -83,6 +97,16 @@ function ProjectionRecord({ entity, label }: { entity?: EntityRecord; label: str
|
|
|
83
97
|
)
|
|
84
98
|
}
|
|
85
99
|
|
|
100
|
+
function dedupeWarnings(warnings: MetadataWarning[]): MetadataWarning[] {
|
|
101
|
+
const seen = new Set<string>()
|
|
102
|
+
return warnings.filter((warning) => {
|
|
103
|
+
const key = `${warning.recordName || ""}|${warning.recordType || ""}|${warning.message}`
|
|
104
|
+
if (seen.has(key)) return false
|
|
105
|
+
seen.add(key)
|
|
106
|
+
return true
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
|
|
86
110
|
export function MetadataProjections({ chart, data }: MetadataProjectionsProps) {
|
|
87
111
|
const riseSpec = (data.riseSpecs || []).find((spec) => spec.chartId === chart.id)
|
|
88
112
|
const chartRecordNames = new Set(
|
|
@@ -94,9 +118,11 @@ export function MetadataProjections({ chart, data }: MetadataProjectionsProps) {
|
|
|
94
118
|
...chart.narrativeBeats.map((beat) => beat.name),
|
|
95
119
|
].filter(Boolean),
|
|
96
120
|
)
|
|
97
|
-
const warnings = (
|
|
98
|
-
(warning) => !warning.recordName || chartRecordNames.has(warning.recordName),
|
|
99
|
-
|
|
121
|
+
const warnings = dedupeWarnings([
|
|
122
|
+
...(data.preservationWarnings || []).filter((warning) => !warning.recordName || chartRecordNames.has(warning.recordName)),
|
|
123
|
+
...(chart.foundationProjection?.warnings || []),
|
|
124
|
+
...(chart.sessionLineageProjection?.warnings || []),
|
|
125
|
+
])
|
|
100
126
|
|
|
101
127
|
return (
|
|
102
128
|
<div className="space-y-6">
|
|
@@ -105,13 +131,13 @@ export function MetadataProjections({ chart, data }: MetadataProjectionsProps) {
|
|
|
105
131
|
<CardHeader>
|
|
106
132
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
107
133
|
<AlertTriangle className="h-5 w-5 text-amber-600" />
|
|
108
|
-
|
|
134
|
+
Metadata Warnings
|
|
109
135
|
</CardTitle>
|
|
110
136
|
</CardHeader>
|
|
111
137
|
<CardContent className="space-y-2">
|
|
112
138
|
{warnings.map((warning, index) => (
|
|
113
139
|
<div key={`${warning.recordName}-${index}`} className="rounded-md border border-amber-500/30 bg-amber-500/5 p-3 text-sm">
|
|
114
|
-
<div className="font-medium">{warning.recordName || warning.recordType || "
|
|
140
|
+
<div className="font-medium">{warning.recordName || warning.recordType || "Chart metadata"}</div>
|
|
115
141
|
<div className="text-muted-foreground">{warning.message}</div>
|
|
116
142
|
{warning.upstreamIssue && (
|
|
117
143
|
<a className="text-xs underline" href={warning.upstreamIssue} target="_blank" rel="noreferrer">
|
|
@@ -124,6 +150,9 @@ export function MetadataProjections({ chart, data }: MetadataProjectionsProps) {
|
|
|
124
150
|
</Card>
|
|
125
151
|
)}
|
|
126
152
|
|
|
153
|
+
{chart.foundationProjection && <AsterionFoundationCard projection={chart.foundationProjection} />}
|
|
154
|
+
{chart.sessionLineageProjection && <AsterionSessionLineageCard projection={chart.sessionLineageProjection} />}
|
|
155
|
+
|
|
127
156
|
<Card>
|
|
128
157
|
<CardHeader>
|
|
129
158
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
@@ -178,19 +207,24 @@ export function MetadataProjections({ chart, data }: MetadataProjectionsProps) {
|
|
|
178
207
|
<CardHeader>
|
|
179
208
|
<CardTitle className="flex items-center gap-2 text-base">
|
|
180
209
|
<FileJson className="h-5 w-5" />
|
|
181
|
-
|
|
210
|
+
Raw Metadata
|
|
182
211
|
</CardTitle>
|
|
183
212
|
</CardHeader>
|
|
184
213
|
<CardContent className="space-y-3">
|
|
185
|
-
<
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
214
|
+
<details>
|
|
215
|
+
<summary className="cursor-pointer text-sm font-medium">Show preserved JSONL metadata</summary>
|
|
216
|
+
<div className="mt-3 space-y-3">
|
|
217
|
+
<ProjectionRecord label="Chart" entity={chart.chartEntity} />
|
|
218
|
+
<ProjectionRecord label="Desired Outcome" entity={chart.desiredOutcome} />
|
|
219
|
+
<ProjectionRecord label="Current Reality" entity={chart.currentReality} />
|
|
220
|
+
{chart.actions.map((action) => (
|
|
221
|
+
<ProjectionRecord key={action.name} label="Action Step" entity={action} />
|
|
222
|
+
))}
|
|
223
|
+
{chart.narrativeBeats.map((beat) => (
|
|
224
|
+
<ProjectionRecord key={beat.name} label="Narrative Beat" entity={beat} />
|
|
225
|
+
))}
|
|
226
|
+
</div>
|
|
227
|
+
</details>
|
|
194
228
|
<div className="flex flex-wrap gap-2 pt-1 text-sm text-muted-foreground">
|
|
195
229
|
<Badge variant="outline" className="gap-1">
|
|
196
230
|
<ListChecks className="h-3 w-3" />
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Clock, Compass, Drama, Link2, Sparkles } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
import { AsterionSessionContextBadge } from "@/components/asterion-session-context-badge"
|
|
4
6
|
import { Badge } from "@/components/ui/badge"
|
|
7
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
8
|
+
import { getNarrativeBeatAnchorId, parseSessionContextMetadata } from "@/lib/asterion-metadata"
|
|
5
9
|
import type { EntityRecord } from "@/lib/types"
|
|
6
|
-
import { Clock, Compass, Drama, Sparkles } from "lucide-react"
|
|
7
10
|
|
|
8
11
|
interface NarrativeBeatsProps {
|
|
9
12
|
beats: EntityRecord[]
|
|
@@ -29,8 +32,11 @@ export function NarrativeBeats({ beats }: NarrativeBeatsProps) {
|
|
|
29
32
|
const universes = beat.metadata.universes || []
|
|
30
33
|
const timestamp = beat.metadata.timestamp ? new Date(beat.metadata.timestamp).toLocaleString() : null
|
|
31
34
|
|
|
35
|
+
const anchorId = getNarrativeBeatAnchorId(beat, idx)
|
|
36
|
+
const sessionContextProjection = parseSessionContextMetadata(beat, idx)
|
|
37
|
+
|
|
32
38
|
return (
|
|
33
|
-
<Card key={beat.name} className="border-l-4 border-l-chart-3">
|
|
39
|
+
<Card id={anchorId} key={beat.name} className="scroll-mt-24 border-l-4 border-l-chart-3">
|
|
34
40
|
<CardHeader className="bg-chart-3/5">
|
|
35
41
|
<div className="flex items-start justify-between gap-4">
|
|
36
42
|
<div className="flex-1">
|
|
@@ -42,6 +48,10 @@ export function NarrativeBeats({ beats }: NarrativeBeatsProps) {
|
|
|
42
48
|
<Badge variant="outline" className="border-chart-3 text-chart-3">
|
|
43
49
|
Beat {idx + 1}
|
|
44
50
|
</Badge>
|
|
51
|
+
<a href={`#${anchorId}`} className="inline-flex items-center gap-1 text-xs text-muted-foreground underline">
|
|
52
|
+
<Link2 className="h-3 w-3" />
|
|
53
|
+
anchor
|
|
54
|
+
</a>
|
|
45
55
|
</div>
|
|
46
56
|
{timestamp && (
|
|
47
57
|
<CardDescription className="flex items-center gap-1">
|
|
@@ -53,6 +63,9 @@ export function NarrativeBeats({ beats }: NarrativeBeatsProps) {
|
|
|
53
63
|
</div>
|
|
54
64
|
</CardHeader>
|
|
55
65
|
<CardContent className="space-y-4 pt-4">
|
|
66
|
+
{/* Session Context Badge */}
|
|
67
|
+
{sessionContextProjection && <AsterionSessionContextBadge projection={sessionContextProjection} />}
|
|
68
|
+
|
|
56
69
|
{/* Universes */}
|
|
57
70
|
<div className="flex items-center gap-2 flex-wrap">
|
|
58
71
|
<Compass className="w-4 h-4 text-muted-foreground" />
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
4
4
|
import { Badge } from "@/components/ui/badge"
|
|
5
|
-
import type { ParsedData, Chart, RelationRecord } from "@/lib/types"
|
|
6
|
-
import { ArrowRight } from "lucide-react"
|
|
5
|
+
import type { ParsedData, Chart, EntityRecord, RelationRecord } from "@/lib/types"
|
|
6
|
+
import { ArrowRight, Github } from "lucide-react"
|
|
7
|
+
import { getGithubBridgeRelationType, getGithubSyncState } from "@/lib/github-provenance"
|
|
8
|
+
import { SyncStatePill } from "./github-provenance"
|
|
7
9
|
|
|
8
10
|
interface RelationGraphProps {
|
|
9
11
|
chart: Chart
|
|
@@ -23,7 +25,10 @@ export function RelationGraph({ chart, data }: RelationGraphProps) {
|
|
|
23
25
|
)
|
|
24
26
|
}
|
|
25
27
|
|
|
26
|
-
const
|
|
28
|
+
const githubBridgeRelations = relations.filter((relation) => getGithubBridgeRelationType(relation))
|
|
29
|
+
const standardRelations = relations.filter((relation) => !getGithubBridgeRelationType(relation))
|
|
30
|
+
|
|
31
|
+
const relationsByType = standardRelations.reduce(
|
|
27
32
|
(acc, rel) => {
|
|
28
33
|
if (!acc[rel.relationType]) {
|
|
29
34
|
acc[rel.relationType] = []
|
|
@@ -41,6 +46,28 @@ export function RelationGraph({ chart, data }: RelationGraphProps) {
|
|
|
41
46
|
<CardDescription>Connections between chart entities</CardDescription>
|
|
42
47
|
</CardHeader>
|
|
43
48
|
<CardContent className="space-y-6">
|
|
49
|
+
{githubBridgeRelations.length > 0 && (
|
|
50
|
+
<div>
|
|
51
|
+
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
|
52
|
+
<Badge className="gap-1 bg-indigo-600 text-white">
|
|
53
|
+
<Github className="w-3 h-3" />
|
|
54
|
+
GitHub bridge
|
|
55
|
+
</Badge>
|
|
56
|
+
<span className="text-muted-foreground">({githubBridgeRelations.length})</span>
|
|
57
|
+
</h3>
|
|
58
|
+
<div className="space-y-2">
|
|
59
|
+
{githubBridgeRelations.map((rel, idx) => (
|
|
60
|
+
<RelationRow
|
|
61
|
+
key={`github-${idx}`}
|
|
62
|
+
relation={rel}
|
|
63
|
+
data={data}
|
|
64
|
+
bridgeLabel={getGithubBridgeRelationType(rel)}
|
|
65
|
+
/>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
|
|
44
71
|
{Object.entries(relationsByType).map(([type, rels]) => (
|
|
45
72
|
<div key={type}>
|
|
46
73
|
<h3 className="text-sm font-semibold mb-3 flex items-center gap-2">
|
|
@@ -48,30 +75,9 @@ export function RelationGraph({ chart, data }: RelationGraphProps) {
|
|
|
48
75
|
<span className="text-muted-foreground">({rels.length})</span>
|
|
49
76
|
</h3>
|
|
50
77
|
<div className="space-y-2">
|
|
51
|
-
{rels.map((rel, idx) =>
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<div key={idx} className="flex items-center gap-3 p-3 bg-muted/30 rounded-lg text-sm">
|
|
57
|
-
<div className="flex-1 min-w-0">
|
|
58
|
-
<div className="font-mono text-xs text-muted-foreground mb-1">{rel.from}</div>
|
|
59
|
-
{fromEntity && (
|
|
60
|
-
<div className="text-xs line-clamp-1">
|
|
61
|
-
{fromEntity.observations[0] || fromEntity.entityType}
|
|
62
|
-
</div>
|
|
63
|
-
)}
|
|
64
|
-
</div>
|
|
65
|
-
<ArrowRight className="w-4 h-4 text-muted-foreground flex-shrink-0" />
|
|
66
|
-
<div className="flex-1 min-w-0">
|
|
67
|
-
<div className="font-mono text-xs text-muted-foreground mb-1">{rel.to}</div>
|
|
68
|
-
{toEntity && (
|
|
69
|
-
<div className="text-xs line-clamp-1">{toEntity.observations[0] || toEntity.entityType}</div>
|
|
70
|
-
)}
|
|
71
|
-
</div>
|
|
72
|
-
</div>
|
|
73
|
-
)
|
|
74
|
-
})}
|
|
78
|
+
{rels.map((rel, idx) => (
|
|
79
|
+
<RelationRow key={idx} relation={rel} data={data} />
|
|
80
|
+
))}
|
|
75
81
|
</div>
|
|
76
82
|
</div>
|
|
77
83
|
))}
|
|
@@ -79,3 +85,61 @@ export function RelationGraph({ chart, data }: RelationGraphProps) {
|
|
|
79
85
|
</Card>
|
|
80
86
|
)
|
|
81
87
|
}
|
|
88
|
+
|
|
89
|
+
interface RelationRowProps {
|
|
90
|
+
relation: RelationRecord
|
|
91
|
+
data: ParsedData
|
|
92
|
+
bridgeLabel?: string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function RelationRow({ relation, data, bridgeLabel }: RelationRowProps) {
|
|
96
|
+
const fromEntity = data.entities.get(relation.from)
|
|
97
|
+
const toEntity = data.entities.get(relation.to)
|
|
98
|
+
const syncState = getGithubSyncState(relation.metadata)
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div className="flex flex-col gap-2 p-3 bg-muted/30 rounded-lg text-sm sm:flex-row sm:items-center sm:gap-3">
|
|
102
|
+
<RelationEndpoint name={relation.from} entity={fromEntity} />
|
|
103
|
+
|
|
104
|
+
<div className="flex items-center gap-2 text-muted-foreground sm:flex-col">
|
|
105
|
+
<ArrowRight className="w-4 h-4 flex-shrink-0 rotate-90 sm:rotate-0" />
|
|
106
|
+
{(bridgeLabel || syncState) && (
|
|
107
|
+
<div className="flex flex-wrap justify-center gap-1">
|
|
108
|
+
{bridgeLabel && (
|
|
109
|
+
<Badge variant="outline" className="text-xs">
|
|
110
|
+
{bridgeLabel.replace(/_/g, " ")}
|
|
111
|
+
</Badge>
|
|
112
|
+
)}
|
|
113
|
+
<SyncStatePill syncState={syncState} />
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<RelationEndpoint name={relation.to} entity={toEntity} />
|
|
119
|
+
</div>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function RelationEndpoint({
|
|
124
|
+
name,
|
|
125
|
+
entity,
|
|
126
|
+
}: {
|
|
127
|
+
name: string
|
|
128
|
+
entity?: EntityRecord
|
|
129
|
+
}) {
|
|
130
|
+
const isGithubVirtualNode = name.startsWith("gh:")
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div className="flex-1 min-w-0">
|
|
134
|
+
<div className="font-mono text-xs text-muted-foreground mb-1 break-all">{name}</div>
|
|
135
|
+
{entity ? (
|
|
136
|
+
<div className="text-xs line-clamp-1">{entity.observations[0] || entity.entityType}</div>
|
|
137
|
+
) : isGithubVirtualNode ? (
|
|
138
|
+
<Badge variant="outline" className="gap-1 text-xs">
|
|
139
|
+
<Github className="w-3 h-3" />
|
|
140
|
+
GitHub issue
|
|
141
|
+
</Badge>
|
|
142
|
+
) : null}
|
|
143
|
+
</div>
|
|
144
|
+
)
|
|
145
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -67,7 +67,7 @@ function loadConfig(args) {
|
|
|
67
67
|
if (args['port'] || args['p']) {
|
|
68
68
|
config.port = args['port'] || args['p'];
|
|
69
69
|
}
|
|
70
|
-
if (args['no-open']) {
|
|
70
|
+
if (args['no-open'] || args.open === false) {
|
|
71
71
|
config.noOpen = true;
|
|
72
72
|
}
|
|
73
73
|
if (args['live']) {
|
|
@@ -208,11 +208,13 @@ EXAMPLES:
|
|
|
208
208
|
spawn(openCmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
209
209
|
}, 4000);
|
|
210
210
|
}
|
|
211
|
-
|
|
211
|
+
const shutdownDocker = () => {
|
|
212
212
|
console.log('\nš Shutting down visualizer...');
|
|
213
213
|
dockerProcess.kill();
|
|
214
214
|
process.exit(0);
|
|
215
|
-
}
|
|
215
|
+
};
|
|
216
|
+
process.on('SIGINT', shutdownDocker);
|
|
217
|
+
process.on('SIGTERM', shutdownDocker);
|
|
216
218
|
dockerProcess.on('exit', (code) => { process.exit(code || 0); });
|
|
217
219
|
return;
|
|
218
220
|
}
|
|
@@ -253,11 +255,13 @@ EXAMPLES:
|
|
|
253
255
|
}, 3000);
|
|
254
256
|
}
|
|
255
257
|
// Handle shutdown
|
|
256
|
-
|
|
258
|
+
const shutdownLocal = () => {
|
|
257
259
|
console.log('\nš Shutting down visualizer...');
|
|
258
260
|
nextProcess.kill();
|
|
259
261
|
process.exit(0);
|
|
260
|
-
}
|
|
262
|
+
};
|
|
263
|
+
process.on('SIGINT', shutdownLocal);
|
|
264
|
+
process.on('SIGTERM', shutdownLocal);
|
|
261
265
|
nextProcess.on('exit', (code) => {
|
|
262
266
|
process.exit(code || 0);
|
|
263
267
|
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Build and push jgwill/coaia:visualizer to Docker Hub
|
|
3
|
+
# Usage: ./docker-build-push.sh [tag]
|
|
4
|
+
# Requires: docker login jgwill (or DOCKERHUB_TOKEN env)
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
TAG="${1:-visualizer}"
|
|
9
|
+
IMAGE="jgwill/coaia:$TAG"
|
|
10
|
+
|
|
11
|
+
echo "š³ Building $IMAGE..."
|
|
12
|
+
docker build -t "$IMAGE" .
|
|
13
|
+
|
|
14
|
+
echo "š¤ Pushing $IMAGE..."
|
|
15
|
+
docker push "$IMAGE"
|
|
16
|
+
|
|
17
|
+
echo "ā
Done: $IMAGE"
|
|
18
|
+
echo ""
|
|
19
|
+
echo "Run with:"
|
|
20
|
+
echo " docker run --rm -p 4321:4321 -v /abs/path/to/file.jsonl:/data/memory.jsonl $IMAGE"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
set -e
|
|
3
|
+
|
|
4
|
+
# COAIA Visualizer Docker Entrypoint
|
|
5
|
+
# Serves the pre-built Next.js app with the user-supplied JSONL file.
|
|
6
|
+
#
|
|
7
|
+
# The JSONL file must be mounted into /data/ in the container.
|
|
8
|
+
# COAIAV_MEMORY_PATH defaults to /data/memory.jsonl but can be overridden.
|
|
9
|
+
|
|
10
|
+
MEMORY_PATH="${COAIAV_MEMORY_PATH:-/data/memory.jsonl}"
|
|
11
|
+
PORT="${PORT:-4321}"
|
|
12
|
+
|
|
13
|
+
# Validate that the memory file exists (warn but don't block ā user may mount it later)
|
|
14
|
+
if [ ! -f "$MEMORY_PATH" ]; then
|
|
15
|
+
echo "ā ļø Warning: memory file not found at $MEMORY_PATH"
|
|
16
|
+
echo " Mount your JSONL file with: -v /your/file.jsonl:$MEMORY_PATH"
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
echo "šØ COAIA Visualizer"
|
|
20
|
+
echo "š Memory file: $MEMORY_PATH"
|
|
21
|
+
echo "š http://localhost:$PORT"
|
|
22
|
+
echo ""
|
|
23
|
+
|
|
24
|
+
export COAIAV_MEMORY_PATH="$MEMORY_PATH"
|
|
25
|
+
export PORT="$PORT"
|
|
26
|
+
|
|
27
|
+
exec node_modules/.bin/next start -p "$PORT"
|