@@ -110,6 +110,31 @@ type DeviceWire = {
110110 display_name : string
111111}
112112
113+ type SharedFolderTreeWire = {
114+ revision_id : string | null
115+ file_count : number
116+ entries : SharedFolderTreeEntryWire [ ]
117+ revisions : SharedFolderRevisionWire [ ]
118+ }
119+
120+ type SharedFolderTreeEntryWire = {
121+ path : string
122+ name : string
123+ kind : "directory" | "file" | "symlink" | "unsupported"
124+ parent_path : string | null
125+ size_bytes : number | null
126+ updated_at : string
127+ object_id : string | null
128+ }
129+
130+ type SharedFolderRevisionWire = {
131+ id : string
132+ parent_id : string | null
133+ boundary : string
134+ created_at : string
135+ changed_files : number
136+ }
137+
113138type BindhubSessionWire = {
114139 account_id : string
115140 session_id : string
@@ -315,12 +340,20 @@ async function dataFromApi(
315340 fetchJson < SharedFolderWire [ ] > ( `${ baseUrl } /v1/shared-folders` , headers ) ,
316341 fetchJson < DeviceWire [ ] > ( `${ baseUrl } /v1/devices` , headers ) ,
317342 ] )
343+ const trees = await Promise . all (
344+ folders . map ( ( folder ) =>
345+ fetchJson < SharedFolderTreeWire > (
346+ `${ baseUrl } /v1/loom/shared-folders/${ encodeURIComponent ( folder . id ) } /tree` ,
347+ headers
348+ ) . catch ( ( ) => emptyHostedTree ( ) )
349+ )
350+ )
318351
319352 return {
320353 identity,
321354 source,
322355 overview : overview ( folders , machines ) ,
323- folders : folders . map ( ( folder ) => ( {
356+ folders : folders . map ( ( folder , index ) => ( {
324357 id : folder . id ,
325358 displayName : folder . display_name ,
326359 description : "Hosted shared folder" ,
@@ -330,25 +363,26 @@ async function dataFromApi(
330363 visibility : folder . role === "viewer" ? "team" : "private" ,
331364 syncStatus : "synced" ,
332365 hydrationState : "fully-local" ,
333- lastCheckpoint : "Synced from Bindhub API ",
334- updatedAt : "Known to Bindhub API" ,
366+ lastCheckpoint : trees [ index ] . revision_id ?? "No synced revision yet ",
367+ updatedAt : latestTreeTimestamp ( trees [ index ] ) ?? "Known to Bindhub API" ,
335368 sizeLabel : "Remote metadata" ,
336- fileCount : 0 ,
369+ fileCount : trees [ index ] . file_count ,
337370 machineCount : machines . length ,
338- entries : hostedPlaceholderEntries ( folder . display_name ) ,
339- revisions : hostedPlaceholderRevisions ( ) ,
371+ entries : hostedEntriesFromTree ( trees [ index ] ) ,
372+ revisions : hostedRevisionsFromTree ( trees [ index ] ) ,
340373 settings : hostedPlaceholderSettings ( folder . role === "viewer" ? "team" : "private" ) ,
341374 recentActivity : [
342375 {
343376 id : `${ folder . id } -api-sync` ,
344377 title : "Synced from hosted API" ,
345378 detail :
346- "Folder membership is live. File-tree metadata will come from Loom revisions." ,
379+ trees [ index ] . revision_id
380+ ? `${ trees [ index ] . file_count } files are visible from the latest Loom revision.`
381+ : "Folder membership is live. No Loom revision has synced yet." ,
347382 timestamp : "Now" ,
348383 } ,
349384 ] ,
350- readme :
351- "This folder is backed by hosted bindhub metadata. File browsing will become exact once the API exposes Loom folder revision trees." ,
385+ readme : hostedReadmeFromTree ( folder . display_name , trees [ index ] ) ,
352386 } ) ) ,
353387 machines : machines . map ( ( machine ) => ( {
354388 id : machine . id ,
@@ -732,37 +766,109 @@ function fixtureDashboardData(
732766 }
733767}
734768
735- function hostedPlaceholderEntries ( displayName : string ) : SharedFolderEntry [ ] {
736- return [
737- {
738- path : "README.md" ,
739- name : "README.md" ,
740- kind : "file" ,
741- parentPath : null ,
742- sizeLabel : "remote" ,
743- updatedAt : "Awaiting Loom tree" ,
769+ function emptyHostedTree ( ) : SharedFolderTreeWire {
770+ return {
771+ revision_id : null ,
772+ file_count : 0 ,
773+ entries : [ ] ,
774+ revisions : [ ] ,
775+ }
776+ }
777+
778+ function hostedEntriesFromTree ( tree : SharedFolderTreeWire ) : SharedFolderEntry [ ] {
779+ return tree . entries
780+ . filter (
781+ (
782+ entry
783+ ) : entry is SharedFolderTreeEntryWire & { kind : "file" | "directory" } =>
784+ entry . kind === "file" || entry . kind === "directory"
785+ )
786+ . map ( ( entry ) => ( {
787+ path : entry . path ,
788+ name : entry . name ,
789+ kind : entry . kind ,
790+ parentPath : entry . parent_path ,
791+ sizeLabel :
792+ entry . kind === "directory" ? "folder" : formatBytes ( entry . size_bytes ?? 0 ) ,
793+ updatedAt : entry . updated_at ,
744794 hydrationState : "remote-only" ,
745- language : "Markdown" ,
746- summary : ` ${ displayName } file tree metadata is not exposed by the API yet.` ,
747- content :
748- "This hosted folder exists, but the hosted API does not expose Loom file-tree metadata yet." ,
749- } ,
750- ]
795+ language : languageForPath ( entry . path ) ,
796+ summary :
797+ entry . kind === "directory"
798+ ? "Folder in the latest hosted revision."
799+ : "File in the latest hosted revision." ,
800+ } ) )
751801}
752802
753- function hostedPlaceholderRevisions ( ) : FolderRevision [ ] {
754- return [
755- {
756- id : "hosted-current" ,
757- label : "Hosted cursor" ,
758- message : "Folder membership is live; Loom revision history is pending API support." ,
759- kind : "auto" ,
760- createdAt : "Known to Bindhub API" ,
761- author : "Bindhub hosted" ,
762- changedFiles : 0 ,
763- pinned : false ,
764- } ,
765- ]
803+ function hostedRevisionsFromTree ( tree : SharedFolderTreeWire ) : FolderRevision [ ] {
804+ return tree . revisions . map ( ( revision ) => ( {
805+ id : revision . id ,
806+ label : revision . id ,
807+ message : `Loom ${ revision . boundary } revision` ,
808+ kind : "auto" ,
809+ createdAt : revision . created_at ,
810+ author : "Bindhub hosted" ,
811+ changedFiles : revision . changed_files ,
812+ pinned : false ,
813+ } ) )
814+ }
815+
816+ function hostedReadmeFromTree ( displayName : string , tree : SharedFolderTreeWire ) {
817+ if ( ! tree . revision_id ) {
818+ return `${ displayName } is known to Bindhub, but no synced folder revision is available yet.`
819+ }
820+ return `${ displayName } is backed by hosted Loom revision ${ tree . revision_id } .`
821+ }
822+
823+ function latestTreeTimestamp ( tree : SharedFolderTreeWire ) {
824+ return tree . revisions . at ( - 1 ) ?. created_at ?? tree . entries [ 0 ] ?. updated_at ?? null
825+ }
826+
827+ function formatBytes ( bytes : number ) {
828+ if ( bytes >= 1024 * 1024 * 1024 ) {
829+ return `${ ( bytes / ( 1024 * 1024 * 1024 ) ) . toFixed ( 1 ) } GiB`
830+ }
831+ if ( bytes >= 1024 * 1024 ) {
832+ return `${ ( bytes / ( 1024 * 1024 ) ) . toFixed ( 1 ) } MiB`
833+ }
834+ if ( bytes >= 1024 ) {
835+ return `${ ( bytes / 1024 ) . toFixed ( 1 ) } KiB`
836+ }
837+ return `${ bytes } bytes`
838+ }
839+
840+ function languageForPath ( path : string ) {
841+ const extension = path . split ( "." ) . pop ( ) ?. toLowerCase ( )
842+ switch ( extension ) {
843+ case "astro" :
844+ return "Astro"
845+ case "css" :
846+ return "CSS"
847+ case "go" :
848+ return "Go"
849+ case "html" :
850+ return "HTML"
851+ case "js" :
852+ case "jsx" :
853+ return "JavaScript"
854+ case "json" :
855+ return "JSON"
856+ case "md" :
857+ case "mdx" :
858+ return "Markdown"
859+ case "rs" :
860+ return "Rust"
861+ case "ts" :
862+ case "tsx" :
863+ return "TypeScript"
864+ case "toml" :
865+ return "TOML"
866+ case "yml" :
867+ case "yaml" :
868+ return "YAML"
869+ default :
870+ return undefined
871+ }
766872}
767873
768874function hostedPlaceholderSettings (
0 commit comments