Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 0 additions & 25 deletions agents/yjs-fs-agents/package.json

This file was deleted.

1 change: 0 additions & 1 deletion agents/yjs-fs-agents/src/index.ts

This file was deleted.

353 changes: 326 additions & 27 deletions bun.lock

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions examples/yjs-rich-markdown-demo/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Rich Markdown Learning Demo</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body, #root {
height: 100%;
width: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.tiptap {
min-height: 200px;
padding: 16px;
outline: none;
}
.tiptap:focus {
outline: none;
}
.tiptap p.is-editor-empty:first-child::before {
color: #adb5bd;
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
.collaboration-cursor__caret {
position: relative;
margin-left: -1px;
margin-right: -1px;
border-left: 1px solid;
border-right: 1px solid;
word-break: normal;
pointer-events: none;
}
.collaboration-cursor__label {
position: absolute;
top: -1.4em;
left: -1px;
font-size: 12px;
font-style: normal;
font-weight: 600;
line-height: normal;
user-select: none;
color: #fff;
padding: 0.1rem 0.3rem;
border-radius: 3px 3px 3px 0;
white-space: nowrap;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
37 changes: 37 additions & 0 deletions examples/yjs-rich-markdown-demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@humanlayer/yjs-rich-markdown-demo",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"dev": "bun run --parallel dev:server dev:vite",
"dev:vite": "vite --port 5176",
"dev:server": "bun run server/index.ts",
"build": "tsc -b && vite build",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@durable-streams/server": "^0.3.1",
"@durable-streams/y-durable-streams": "^0.2.4",
"@humanlayer/yjs-rich-markdown": "workspace:*",
"@tiptap/extension-collaboration": "3.22.5",
"@tiptap/extension-collaboration-caret": "3.22.5",
"@tiptap/pm": "3.22.5",
"@tiptap/react": "3.22.5",
"@tiptap/starter-kit": "3.22.5",
"@tanstack/react-router": "^1.93.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"yjs": "^13.6.30",
"y-protocols": "1.0.7"
},
"devDependencies": {
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@tanstack/router-plugin": "^1.93.0",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.6.3",
"vite": "^6.0.0"
}
}
37 changes: 37 additions & 0 deletions examples/yjs-rich-markdown-demo/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { DurableStreamTestServer } from '@durable-streams/server'
import { YjsServer } from '@durable-streams/y-durable-streams/server'

const DS_PORT = 4447
const YJS_PORT = 4438

async function main() {
console.log('Starting Durable Streams server...')
const dsServer = new DurableStreamTestServer({
port: DS_PORT,
host: '127.0.0.1',
})
await dsServer.start()
console.log(`Durable Streams server running at http://127.0.0.1:${DS_PORT}`)

console.log('Starting Y.js server...')
const yjsServer = new YjsServer({
port: YJS_PORT,
host: '127.0.0.1',
dsServerUrl: `http://127.0.0.1:${DS_PORT}`,
compactionThreshold: 1024 * 1024,
})
const yjsUrl = await yjsServer.start()
console.log(`Y.js server running at ${yjsUrl}`)

console.log('\nServers ready. Press Ctrl+C to stop.')

process.on('SIGINT', async () => {
console.log('\nShutting down...')
process.exit(0)
})
}

main().catch((err) => {
console.error('Failed to start servers:', err)
process.exit(1)
})
69 changes: 69 additions & 0 deletions examples/yjs-rich-markdown-demo/src/components/ArtifactEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { artifactCollaborationExtension } from '@humanlayer/yjs-rich-markdown'
import CollaborationCaret from '@tiptap/extension-collaboration-caret'
import { EditorContent, useEditor } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import { useEffect } from 'react'
import { useArtifactSession } from '../providers/ArtifactProvider'

type ArtifactEditorProps = {
path: string
mode?: 'fragment' | 'field'
}

export function ArtifactEditor({ path, mode = 'fragment' }: ArtifactEditorProps) {
const { doc, store, awareness, provider, localUser } = useArtifactSession()

store.ensureArtifact(path)

const editor = useEditor(
{
extensions: [
StarterKit.configure({
undoRedo: false,
}),
artifactCollaborationExtension({ doc, path, mode }),
CollaborationCaret.configure({
provider: provider as any,
user: {
name: localUser.name,
color: localUser.color,
},
}),
],
editorProps: {
attributes: {
class: 'tiptap',
},
},
},
[path, mode],
)

useEffect(() => {
awareness.setLocalStateField('presence', { artifactPath: path })

return () => {
awareness.setLocalStateField('presence', { artifactPath: undefined })
}
}, [path, awareness])

useEffect(() => {
return () => {
editor?.destroy()
}
}, [editor])

return (
<div
style={{
border: '1px solid #e0e0e0',
borderRadius: 4,
background: '#fff',
height: '100%',
overflow: 'auto',
}}
>
<EditorContent editor={editor} style={{ height: '100%' }} />
</div>
)
}
52 changes: 52 additions & 0 deletions examples/yjs-rich-markdown-demo/src/components/ArtifactList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import type { ArtifactMetadata } from '@humanlayer/yjs-rich-markdown'
import { useEffect, useState } from 'react'
import { useArtifactSession } from '../providers/ArtifactProvider'

type ArtifactListProps = {
onSelect?: (path: string) => void
selectedPath?: string
}

export function ArtifactList({ onSelect, selectedPath }: ArtifactListProps) {
const { store } = useArtifactSession()
const [artifacts, setArtifacts] = useState<ArtifactMetadata[]>([])

useEffect(() => {
const update = () => {
setArtifacts(store.listArtifacts())
}

update()
store.registry.observe(update)
return () => store.registry.unobserve(update)
}, [store])

return (
<div style={{ padding: 8 }}>
<h4 style={{ margin: '0 0 8px' }}>Artifacts ({artifacts.length})</h4>
{artifacts.length === 0 ? (
<div style={{ color: '#666', fontSize: 14 }}>No artifacts yet</div>
) : (
<ul style={{ margin: 0, padding: 0, listStyle: 'none' }}>
{artifacts.map((artifact) => (
<li
key={artifact.path}
onClick={() => onSelect?.(artifact.path)}
style={{
padding: '6px 8px',
cursor: 'pointer',
background: selectedPath === artifact.path ? '#e3f2fd' : 'transparent',
borderRadius: 4,
marginBottom: 2,
fontSize: 14,
}}
>
{artifact.path}
{artifact.title && <span style={{ color: '#666', marginLeft: 8 }}>({artifact.title})</span>}
</li>
))}
</ul>
)}
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { useEffect, useState } from 'react'
import { type Collaborator, getCollaborators } from '../lib/collaboration'
import { useArtifactSession } from '../providers/ArtifactProvider'

type AwarenessDebugPanelProps = {
filterByArtifact?: string
}

export function AwarenessDebugPanel({ filterByArtifact }: AwarenessDebugPanelProps) {
const { awareness } = useArtifactSession()
const [states, setStates] = useState<Map<number, unknown>>(new Map())
const [collaborators, setCollaborators] = useState<Collaborator[]>([])

useEffect(() => {
const update = () => {
const newStates = new Map(awareness.getStates())
setStates(newStates)
setCollaborators(
getCollaborators(newStates, awareness.clientID, {
includeSelf: true,
filterByArtifact,
}),
)
}

update()
awareness.on('change', update)
return () => awareness.off('change', update)
}, [awareness, filterByArtifact])

return (
<div
style={{
padding: 12,
background: '#f5f5f5',
borderRadius: 4,
fontSize: 12,
fontFamily: 'monospace',
overflow: 'auto',
}}
>
<h4 style={{ margin: '0 0 8px' }}>
Awareness Debug {filterByArtifact ? `(filtered: ${filterByArtifact})` : '(all)'}
</h4>
<div style={{ marginBottom: 12 }}>
<strong>Local Client ID:</strong> {awareness.clientID}
</div>

<div style={{ marginBottom: 12 }}>
<strong>Collaborators ({collaborators.length}):</strong>
{collaborators.map((c) => (
<div
key={c.clientId}
style={{
marginTop: 4,
padding: 4,
background: c.isSelf ? '#e3f2fd' : '#fff',
borderRadius: 2,
borderLeft: `3px solid ${c.user.color}`,
}}
>
<div>
{c.user.name} {c.isSelf ? '(you)' : ''}
</div>
<div style={{ color: '#666' }}>
clientId: {c.clientId}, path: {c.presence.artifactPath ?? 'none'}
</div>
</div>
))}
</div>

<details>
<summary style={{ cursor: 'pointer', marginBottom: 8 }}>
<strong>Raw States ({states.size})</strong>
</summary>
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all', margin: 0 }}>
{JSON.stringify(Array.from(states.entries()), null, 2)}
</pre>
</details>
</div>
)
}
Loading