Skip to content
Closed
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,5 @@ opencode.json
plannotator-local
# Local research/reference docs (not for repo)
reference/

.env
45 changes: 40 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ plannotator/
│ │ ├── integrations.ts # Obsidian, Bear integrations
│ │ └── project.ts # Project name detection for tags
│ ├── ui/ # Shared React components
│ │ ├── components/ # Viewer, Toolbar, Settings, etc.
│ │ ├── utils/ # parser.ts, sharing.ts, storage.ts, planSave.ts, agentSwitch.ts
│ │ ├── hooks/ # useSharing.ts
│ │ ├── components/ # Viewer, Toolbar, Settings, SyncStatusIndicator, etc.
│ │ ├── utils/ # parser.ts, sharing.ts, sessionSharing.ts, storage.ts, etc.
│ │ ├── hooks/ # useSharing.ts, useCollaborativeSession.ts
│ │ ├── lib/ # supabase.ts (client setup)
│ │ └── types.ts
│ └── editor/ # Main App.tsx
├── supabase/
│ └── schema.sql # Database schema for collaborative sessions
├── .claude-plugin/marketplace.json # For marketplace install
└── legacy/ # Old pre-monorepo code (reference only)
```
Expand Down Expand Up @@ -155,11 +158,11 @@ interface Block {

Text highlighting uses `web-highlighter` library. Code blocks use manual `<mark>` wrapping (web-highlighter can't select inside `<pre>`).

## URL Sharing
## URL Sharing (Quick Share)

**Location:** `packages/ui/utils/sharing.ts`, `packages/ui/hooks/useSharing.ts`

Shares full plan + annotations via URL hash using deflate compression.
Shares full plan + annotations via URL hash using deflate compression. This is a one-time snapshot - recipients get a frozen copy.

**Payload format:**

Expand Down Expand Up @@ -191,6 +194,38 @@ type ShareableAnnotation =
3. Apply `<mark>` highlights
4. Clear hash from URL (prevents re-parse on refresh)

## Collaborative Sessions (Live Share)

**Location:** `packages/ui/utils/sessionSharing.ts`, `packages/ui/hooks/useCollaborativeSession.ts`

Real-time collaboration via Supabase. Multiple users can annotate the same plan and see updates instantly.

**Requires Supabase setup:**

1. Create a Supabase project at https://supabase.com
2. Run the schema in `supabase/schema.sql`
3. Set environment variables:
```
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
```

**Session URL format:** `share.plannotator.ai/session/{uuid}`

**Flow:**

1. User clicks "Create Live Session" in Export modal
2. Session created in Supabase with plan markdown
3. Shareable URL generated
4. Recipients open URL → auto-subscribe to real-time updates
5. Annotations sync bidirectionally via Supabase Realtime

**Key files:**
- `packages/ui/lib/supabase.ts` - Supabase client singleton
- `packages/ui/hooks/useCollaborativeSession.ts` - Real-time sync hook
- `packages/ui/utils/sessionSharing.ts` - Session CRUD operations
- `packages/ui/components/SyncStatusIndicator.tsx` - Connection status display

## Settings Persistence

**Location:** `packages/ui/utils/storage.ts`, `planSave.ts`, `agentSwitch.ts`
Expand Down
2 changes: 2 additions & 0 deletions apps/hook/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import tailwindcss from '@tailwindcss/vite';
import pkg from '../../package.json';

export default defineConfig({
// Load .env from monorepo root
envDir: path.resolve(__dirname, '../..'),
server: {
port: 3000,
host: '0.0.0.0',
Expand Down
1 change: 1 addition & 0 deletions apps/portal/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.vercel
8 changes: 8 additions & 0 deletions apps/portal/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ import tailwindcss from '@tailwindcss/vite';
import pkg from '../../package.json';

export default defineConfig({
// Load .env from monorepo root
envDir: path.resolve(__dirname, '../..'),
server: {
port: 3001,
host: '0.0.0.0',
},
// Enable SPA fallback for client-side routing (e.g., /session/:id)
appType: 'spa',
preview: {
port: 3001,
host: '0.0.0.0',
},
define: {
__APP_VERSION__: JSON.stringify(pkg.version),
},
Expand Down
27 changes: 25 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions packages/editor/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { TaterSpriteRunning } from '@plannotator/ui/components/TaterSpriteRunnin
import { TaterSpritePullup } from '@plannotator/ui/components/TaterSpritePullup';
import { Settings } from '@plannotator/ui/components/Settings';
import { useSharing } from '@plannotator/ui/hooks/useSharing';
import { useCollaborativeSession } from '@plannotator/ui/hooks/useCollaborativeSession';
import { SyncStatusIndicator } from '@plannotator/ui/components/SyncStatusIndicator';
import { storage } from '@plannotator/ui/utils/storage';
import { UpdateBanner } from '@plannotator/ui/components/UpdateBanner';
import { getObsidianSettings } from '@plannotator/ui/utils/obsidian';
Expand Down Expand Up @@ -342,6 +344,21 @@ const App: React.FC = () => {
}
);

// Collaborative session for real-time sync
const {
sessionId,
isCollaborativeAvailable,
syncStatus,
syncError,
sessionUrl,
createCollaborativeSession,
syncAddAnnotation,
syncRemoveAnnotation,
pendingRemoteAnnotations,
pendingRemoteRemovals,
clearPendingRemote,
} = useCollaborativeSession(setMarkdown, annotations, setAnnotations);

// Apply shared annotations to DOM after they're loaded
useEffect(() => {
if (pendingSharedAnnotations && pendingSharedAnnotations.length > 0) {
Expand All @@ -356,6 +373,27 @@ const App: React.FC = () => {
}
}, [pendingSharedAnnotations, clearPendingSharedAnnotations]);

// Apply remote annotations from collaborative session to DOM
useEffect(() => {
if (pendingRemoteAnnotations && pendingRemoteAnnotations.length > 0) {
const timer = setTimeout(() => {
viewerRef.current?.applySharedAnnotations(pendingRemoteAnnotations);
clearPendingRemote();
}, 100);
return () => clearTimeout(timer);
}
}, [pendingRemoteAnnotations, clearPendingRemote]);

// Handle remote annotation removals
useEffect(() => {
if (pendingRemoteRemovals && pendingRemoteRemovals.length > 0) {
pendingRemoteRemovals.forEach(id => {
viewerRef.current?.removeHighlight(id);
});
clearPendingRemote();
}
}, [pendingRemoteRemovals, clearPendingRemote]);

const handleTaterModeChange = (enabled: boolean) => {
setTaterMode(enabled);
storage.setItem('plannotator-tater-mode', String(enabled));
Expand Down Expand Up @@ -522,12 +560,24 @@ const App: React.FC = () => {
setAnnotations(prev => [...prev, ann]);
setSelectedAnnotationId(ann.id);
setIsPanelOpen(true);
// Sync to collaborative session if active
if (sessionId) {
syncAddAnnotation(ann);
}
};

const handleDeleteAnnotation = (id: string) => {
viewerRef.current?.removeHighlight(id);
setAnnotations(prev => prev.filter(a => a.id !== id));
if (selectedAnnotationId === id) setSelectedAnnotationId(null);
// Sync to collaborative session if active
if (sessionId) {
syncRemoveAnnotation(id);
}
};

const handleCreateCollaborativeSession = async () => {
await createCollaborativeSession(markdown);
};

const handleIdentityChange = (oldIdentity: string, newIdentity: string) => {
Expand Down Expand Up @@ -585,6 +635,13 @@ const App: React.FC = () => {
{agentName}
</span>
)}
{sessionId && (
<SyncStatusIndicator
status={syncStatus}
error={syncError}
sessionUrl={sessionUrl}
/>
)}
</div>

<div className="flex items-center gap-1 md:gap-2">
Expand Down Expand Up @@ -725,6 +782,11 @@ const App: React.FC = () => {
diffOutput={diffOutput}
annotationCount={annotations.length}
taterSprite={taterMode ? <TaterSpritePullup /> : undefined}
isCollaborativeAvailable={isCollaborativeAvailable}
sessionId={sessionId}
sessionUrl={sessionUrl}
syncStatus={syncStatus}
onCreateSession={handleCreateCollaborativeSession}
/>

{/* Feedback prompt dialog */}
Expand Down
Loading