deepspace 0.18.0 → 0.19.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.
@@ -59,7 +59,8 @@
59
59
  "The feature is served at /docs on the app's existing hostname.",
60
60
  "A hostname listed in documentation.json domains serves the same release at that hostname's root after you explicitly attach it to the app.",
61
61
  "Documentation builds only when documentation.json exists; apps without this feature are unaffected.",
62
- "MDX may import app components directly. documentation.tsx is the single optional shell customization point."
62
+ "MDX may import app components directly. documentation.tsx is the single optional shell customization point.",
63
+ "Agent discovery is available at /docs/llms.txt, /docs/llms-full.txt, /docs/skill.md, /docs/.well-known/mcp, and /docs/mcp."
63
64
  ],
64
65
  "agentNotes": [
65
66
  "Treat documentation.json, documentation/**/*.md, documentation/**/*.mdx, and optional documentation.tsx as one app-owned documentation feature.",
@@ -3,7 +3,7 @@
3
3
  "name": "Documents",
4
4
  "category": "data",
5
5
  "description": "Real-time collaborative document editing with Tiptap + Yjs",
6
- "details": "Document library with folders & favorites. Collaborative editor built on Tiptap (ProseMirror) with Y.XmlFragment via @tiptap/extension-collaboration and live remote cursors via @tiptap/extension-collaboration-caret — buttery typing, no innerHTML rebuilds, no contenteditable quirks. Invite-only documents with viewer/editor roles: owner shares via an in-app dialog that resolves an email to a DeepSpace user. RBAC is enforced by the documents schema and the starter template's built-in Yjs route. Includes a one-shot migration shim that converts documents created with the legacy contenteditable editor (HTML stored in Y.Text('content')) into Tiptap on first open.",
6
+ "details": "Document library with folders & favorites. Collaborative editor built on Tiptap (ProseMirror) with Y.XmlFragment via @tiptap/extension-collaboration and live remote cursors via @tiptap/extension-collaboration-caret — buttery typing, no innerHTML rebuilds, no contenteditable quirks. Invite-only documents with viewer/editor roles: owner shares via an in-app dialog that resolves an email to a DeepSpace user. RBAC is enforced by the documents schema and the starter template's built-in Yjs route.",
7
7
  "files": [
8
8
  {
9
9
  "src": "src/DocumentsListPage.tsx",
@@ -6,11 +6,6 @@
6
6
  * applied as granular PM transactions (no `innerHTML` swap), the local
7
7
  * caret is migrated through concurrent edits by PM's position mapping,
8
8
  * and remote cursors are painted by `@tiptap/extension-collaboration-caret`.
9
- *
10
- * Legacy documents created with the old editor stored HTML in Y.Text('content').
11
- * On first open by an editor/owner we migrate that into the new XmlFragment
12
- * via `editor.commands.setContent(html)` and clear the legacy field. The
13
- * migration is a single Yjs transaction so all peers converge.
14
9
  */
15
10
 
16
11
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -286,20 +281,12 @@ export default function DocumentsEditorPage() {
286
281
 
287
282
  const { put } = useMutations<DocumentsDocumentFields>('documents')
288
283
 
289
- /**
290
- * `'content'` is kept as the field name only so legacy-doc migration can
291
- * read the old HTML out of Y.Text('content') below. The Tiptap editor
292
- * itself binds to a separate Y.XmlFragment('default') inside the same
293
- * Y.Doc — they don't collide.
294
- */
295
- const {
296
- doc,
297
- text: legacyText,
298
- synced,
299
- canWrite,
300
- writeAuthResolved,
301
- awareness,
302
- } = useYjsRoom(docId ?? 'noop', 'content')
284
+ // The text helper remains available to consumers of useYjsRoom; this editor
285
+ // binds Tiptap directly to Y.XmlFragment('default') on the returned document.
286
+ const { doc, synced, connected, canWrite, writeAuthResolved, awareness } = useYjsRoom(
287
+ docId ?? 'noop',
288
+ 'content',
289
+ )
303
290
 
304
291
  const presenceAccess = useDocumentsPresenceAccess({
305
292
  docId,
@@ -388,37 +375,6 @@ export default function DocumentsEditorPage() {
388
375
  placeholder: showReadOnlyDocUx ? 'View only' : 'Start typing — toolbar above for formatting…',
389
376
  })
390
377
 
391
- // Legacy migration: if doc.getXmlFragment('default') is empty but the
392
- // legacy Y.Text('content') field has HTML, parse it once into Tiptap.
393
- // Only the first writer to open does this; viewers never see partial state
394
- // because both the wipe and the parse run inside a single Yjs transaction.
395
- const migratedRef = useRef(false)
396
- useEffect(() => {
397
- if (migratedRef.current) return
398
- if (!editor || !synced || !effectiveCanWrite) return
399
- const fragment = doc.getXmlFragment('default')
400
- if (fragment.length > 0) {
401
- migratedRef.current = true
402
- return
403
- }
404
- const yText = doc.getText('content')
405
- const legacy = (legacyText ?? yText.toString()).trim()
406
- if (!legacy) {
407
- migratedRef.current = true
408
- return
409
- }
410
- migratedRef.current = true
411
- /**
412
- * Tiptap parses the HTML against its schema, then writes the resulting
413
- * nodes into the bound Y.XmlFragment. We then clear the legacy field
414
- * inside the same transaction so peers see one atomic step.
415
- */
416
- doc.transact(() => {
417
- editor.commands.setContent(legacy, { emitUpdate: true })
418
- if (yText.length > 0) yText.delete(0, yText.length)
419
- })
420
- }, [editor, synced, effectiveCanWrite, doc, legacyText])
421
-
422
378
  const { participants: presenceParticipants, typingNames } = useDocumentsEditorPresence({
423
379
  editor,
424
380
  document: selectedDoc,
@@ -522,7 +478,7 @@ export default function DocumentsEditorPage() {
522
478
  {effectiveRole === 'editor' ? 'Shared editor' : 'Shared viewer'}
523
479
  </span>
524
480
  ) : null}
525
- {synced ? (
481
+ {connected && synced ? (
526
482
  <span
527
483
  className="flex items-center gap-1.5 text-xs tabular-nums"
528
484
  style={{ color: 'var(--documents-el-muted)' }}
@@ -530,7 +486,7 @@ export default function DocumentsEditorPage() {
530
486
  <span className="h-1.5 w-1.5 rounded-full bg-emerald-500" aria-hidden />
531
487
  Synced
532
488
  </span>
533
- ) : (
489
+ ) : connected ? (
534
490
  <span
535
491
  className="flex items-center gap-1.5 text-xs tabular-nums"
536
492
  style={{ color: 'var(--documents-el-muted)' }}
@@ -538,6 +494,14 @@ export default function DocumentsEditorPage() {
538
494
  <span className="h-1.5 w-1.5 animate-pulse rounded-full bg-amber-500" aria-hidden />
539
495
  Connecting…
540
496
  </span>
497
+ ) : (
498
+ <span
499
+ className="flex items-center gap-1.5 text-xs tabular-nums"
500
+ style={{ color: 'var(--documents-el-muted)' }}
501
+ >
502
+ <span className="h-1.5 w-1.5 rounded-full bg-slate-400" aria-hidden />
503
+ Offline
504
+ </span>
541
505
  )}
542
506
  {effectiveRole === 'owner' ? (
543
507
  <Badge variant="success">Owner</Badge>
@@ -120,7 +120,7 @@ export function useDocEditor({
120
120
  canWrite,
121
121
  placeholder = 'Start typing — toolbar above for formatting…',
122
122
  }: UseDocEditorOptions): Editor | null {
123
- /** Structured CRDT field that ProseMirror binds to — distinct from the legacy Y.Text('content'). */
123
+ /** Structured CRDT field that ProseMirror binds to. */
124
124
  const fragment = useMemo(() => doc.getXmlFragment('default'), [doc])
125
125
 
126
126
  /** CollaborationCaret expects a provider-shaped `{ awareness }` object. */
@@ -96,13 +96,11 @@ function peerToDocumentsParticipant(peer: PresencePeerClient): DocumentsPresence
96
96
  const participant: DocumentsPresenceParticipant = {
97
97
  clientId: 0,
98
98
  userId: peer.userId,
99
- name: peer.userName?.trim() || peer.userEmail?.trim() || 'Guest',
99
+ name: peer.userName?.trim() || 'Guest',
100
100
  mode,
101
101
  typing: state.typing === true,
102
102
  isSelf: false,
103
103
  }
104
- if (peer.userEmail) participant.email = peer.userEmail
105
- if (peer.userImageUrl) participant.imageUrl = peer.userImageUrl
106
104
  if (lastTypedAt != null) participant.lastTypedAt = lastTypedAt
107
105
  return participant
108
106
  }
@@ -460,7 +460,10 @@ export default function TasksPage({ className }: TasksPageProps) {
460
460
  const handleUnclaim = async (challenge: { recordId: string; data: Challenge }) => {
461
461
  await put(challenge.recordId, {
462
462
  ...challenge.data,
463
- claimedById: undefined,
463
+ // Empty strings are normalized to SQL NULL by the record writer. The
464
+ // read model omits null database fields, so this also keeps Challenge's
465
+ // optional-string shape honest.
466
+ claimedById: '',
464
467
  })
465
468
  }
466
469
 
@@ -469,8 +472,8 @@ export default function TasksPage({ className }: TasksPageProps) {
469
472
  await put(submitChallenge.recordId, {
470
473
  ...submitChallenge.data,
471
474
  submitted: true,
472
- submissionUrl: url || undefined,
473
- submissionNotes: notes || undefined,
475
+ submissionUrl: url,
476
+ submissionNotes: notes,
474
477
  })
475
478
  setSubmitChallenge(null)
476
479
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepspace",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "engines": {
5
5
  "node": ">=22.15.0 <23 || >=24 <25 || >=26 <27"
6
6
  },