deepspace 0.19.0 → 0.19.2

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.
@@ -182,6 +182,7 @@ export interface LibrarySidebarProps {
182
182
  selection: LibraryNavSelection
183
183
  onSelect: (s: LibraryNavSelection) => void
184
184
  folders: RecordData<DocFolderFields>[]
185
+ canManageFolders: boolean
185
186
  collapsed: boolean
186
187
  onToggleCollapsed: () => void
187
188
  onCreateFolder: (name: string) => void | Promise<void>
@@ -198,6 +199,7 @@ export function LibrarySidebar({
198
199
  selection,
199
200
  onSelect,
200
201
  folders,
202
+ canManageFolders,
201
203
  collapsed,
202
204
  onToggleCollapsed,
203
205
  onCreateFolder,
@@ -337,7 +339,7 @@ export function LibrarySidebar({
337
339
  </span>
338
340
  <div className="space-y-0.5">
339
341
  {sortedFolders.map((f) =>
340
- renamingFolderId === f.recordId ? (
342
+ canManageFolders && renamingFolderId === f.recordId ? (
341
343
  <div
342
344
  key={f.recordId}
343
345
  className="group flex w-full min-w-0 items-center gap-0.5 rounded-md bg-[color:var(--documents-el-accent)]/10 py-0.5 pl-0.5 dark:bg-[color:var(--documents-el-accent)]/15"
@@ -369,96 +371,102 @@ export function LibrarySidebar({
369
371
  onClick={() => onSelect({ kind: 'folder', folderId: f.recordId })}
370
372
  collapsed={collapsed}
371
373
  trailing={
372
- <span className="relative z-[1] flex shrink-0 items-center gap-0.5">
373
- <button
374
- type="button"
375
- data-testid={`rename-folder-${f.recordId}`}
376
- onMouseDown={(e) => {
377
- e.preventDefault()
378
- e.stopPropagation()
379
- }}
380
- onClick={(e) => {
381
- e.stopPropagation()
382
- onStartRenameFolder(f)
383
- }}
384
- className="rounded p-1 text-[color:var(--documents-el-muted)] opacity-0 transition-all hover:bg-black/[0.04] hover:text-[color:var(--documents-el-accent)] group-hover:opacity-100 dark:hover:bg-white/[0.06]"
385
- title="Rename folder"
386
- aria-label={`Rename folder ${f.data.name}`}
387
- >
388
- <Pencil className="h-3.5 w-3.5" />
389
- </button>
390
- <button
391
- type="button"
392
- data-testid={`delete-folder-${f.recordId}`}
393
- onClick={(e) => {
394
- e.stopPropagation()
395
- if (
396
- confirm(
397
- `Delete folder “${f.data.name}”? Documents inside will move to uncategorized.`,
398
- )
399
- ) {
400
- void onDeleteFolder(f.recordId)
401
- }
402
- }}
403
- className="rounded p-1 text-[color:var(--documents-el-muted)] opacity-0 transition-all hover:bg-red-500/10 hover:text-red-600 group-hover:opacity-100"
404
- title="Delete folder"
405
- aria-label={`Delete folder ${f.data.name}`}
406
- >
407
- <Trash2 className="h-3.5 w-3.5" />
408
- </button>
409
- </span>
374
+ canManageFolders ? (
375
+ <span className="relative z-[1] flex shrink-0 items-center gap-0.5">
376
+ <button
377
+ type="button"
378
+ data-testid={`rename-folder-${f.recordId}`}
379
+ onMouseDown={(e) => {
380
+ e.preventDefault()
381
+ e.stopPropagation()
382
+ }}
383
+ onClick={(e) => {
384
+ e.stopPropagation()
385
+ onStartRenameFolder(f)
386
+ }}
387
+ className="rounded p-1 text-[color:var(--documents-el-muted)] opacity-0 transition-all hover:bg-black/[0.04] hover:text-[color:var(--documents-el-accent)] group-hover:opacity-100 dark:hover:bg-white/[0.06]"
388
+ title="Rename folder"
389
+ aria-label={`Rename folder ${f.data.name}`}
390
+ >
391
+ <Pencil className="h-3.5 w-3.5" />
392
+ </button>
393
+ <button
394
+ type="button"
395
+ data-testid={`delete-folder-${f.recordId}`}
396
+ onClick={(e) => {
397
+ e.stopPropagation()
398
+ if (
399
+ confirm(
400
+ `Delete folder “${f.data.name}”? Documents inside will move to uncategorized.`,
401
+ )
402
+ ) {
403
+ void onDeleteFolder(f.recordId)
404
+ }
405
+ }}
406
+ className="rounded p-1 text-[color:var(--documents-el-muted)] opacity-0 transition-all hover:bg-red-500/10 hover:text-red-600 group-hover:opacity-100"
407
+ title="Delete folder"
408
+ aria-label={`Delete folder ${f.data.name}`}
409
+ >
410
+ <Trash2 className="h-3.5 w-3.5" />
411
+ </button>
412
+ </span>
413
+ ) : undefined
410
414
  }
411
415
  />
412
416
  ),
413
417
  )}
414
418
 
415
- {addingFolder ? (
416
- <div className="py-1">
417
- <input
418
- autoFocus
419
- value={newFolderName}
420
- onChange={(e) => setNewFolderName(e.target.value)}
421
- onBlur={() => void submitNewFolder()}
422
- onKeyDown={(e) => {
423
- if (e.key === 'Enter') void submitNewFolder()
424
- if (e.key === 'Escape') {
425
- setAddingFolder(false)
426
- setNewFolderName('')
419
+ {canManageFolders &&
420
+ (addingFolder ? (
421
+ <div className="py-1">
422
+ <input
423
+ autoFocus
424
+ value={newFolderName}
425
+ onChange={(e) => setNewFolderName(e.target.value)}
426
+ onBlur={() => void submitNewFolder()}
427
+ onKeyDown={(e) => {
428
+ if (e.key === 'Enter') void submitNewFolder()
429
+ if (e.key === 'Escape') {
430
+ setAddingFolder(false)
431
+ setNewFolderName('')
432
+ }
433
+ }}
434
+ placeholder="Folder name"
435
+ data-testid="new-folder-input"
436
+ className="w-full rounded-md border px-2 py-1.5 text-[13px] outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
437
+ style={{
438
+ borderColor: 'var(--documents-el-line)',
439
+ backgroundColor: 'var(--documents-el-bg)',
440
+ color: 'var(--documents-el-text)',
441
+ }}
442
+ />
443
+ </div>
444
+ ) : (
445
+ <button
446
+ type="button"
447
+ data-testid="new-folder-btn"
448
+ onClick={() => {
449
+ if (!collapsed) {
450
+ setAddingFolder(true)
451
+ return
427
452
  }
453
+ const name = window.prompt('Folder name')?.trim()
454
+ if (name) void onCreateFolder(name)
428
455
  }}
429
- placeholder="Folder name"
430
- data-testid="new-folder-input"
431
- className="w-full rounded-md border px-2 py-1.5 text-[13px] outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
432
- style={{
433
- borderColor: 'var(--documents-el-line)',
434
- backgroundColor: 'var(--documents-el-bg)',
435
- color: 'var(--documents-el-text)',
436
- }}
437
- />
438
- </div>
439
- ) : (
440
- <button
441
- type="button"
442
- data-testid="new-folder-btn"
443
- onClick={() => {
444
- if (!collapsed) {
445
- setAddingFolder(true)
446
- return
447
- }
448
- const name = window.prompt('Folder name')?.trim()
449
- if (name) void onCreateFolder(name)
450
- }}
451
- title={collapsed ? 'New folder' : undefined}
452
- className="flex h-9 w-full min-w-0 items-center gap-0 rounded-md pl-0.5 pr-0 text-left text-[13px] text-[color:var(--documents-el-muted)] transition-colors hover:bg-black/[0.04] hover:text-[color:var(--documents-el-accent)] dark:hover:bg-white/[0.06]"
453
- >
454
- <span className="flex h-9 w-10 shrink-0 items-center justify-center" aria-hidden>
455
- <FolderPlus className="h-3.5 w-3.5 shrink-0" />
456
- </span>
457
- <span className={collapsed ? 'sr-only' : 'min-w-0 flex-1 truncate pr-2'}>
458
- New folder
459
- </span>
460
- </button>
461
- )}
456
+ title={collapsed ? 'New folder' : undefined}
457
+ className="flex h-9 w-full min-w-0 items-center gap-0 rounded-md pl-0.5 pr-0 text-left text-[13px] text-[color:var(--documents-el-muted)] transition-colors hover:bg-black/[0.04] hover:text-[color:var(--documents-el-accent)] dark:hover:bg-white/[0.06]"
458
+ >
459
+ <span
460
+ className="flex h-9 w-10 shrink-0 items-center justify-center"
461
+ aria-hidden
462
+ >
463
+ <FolderPlus className="h-3.5 w-3.5 shrink-0" />
464
+ </span>
465
+ <span className={collapsed ? 'sr-only' : 'min-w-0 flex-1 truncate pr-2'}>
466
+ New folder
467
+ </span>
468
+ </button>
469
+ ))}
462
470
  </div>
463
471
  </div>
464
472
  </nav>
@@ -5,6 +5,15 @@ import {
5
5
  isDocumentsOwner,
6
6
  resolveDocumentsAccessRole,
7
7
  } from '../use-documents-presence-access'
8
+ import { recordsReadyForMutation } from '../documents-library-types'
9
+
10
+ describe('document mutation readiness', () => {
11
+ it('requires both a ready query and a ready mutation transport', () => {
12
+ expect(recordsReadyForMutation('loading', true)).toBe(false)
13
+ expect(recordsReadyForMutation('ready', false)).toBe(false)
14
+ expect(recordsReadyForMutation('ready', true)).toBe(true)
15
+ })
16
+ })
8
17
 
9
18
  describe('document access policy', () => {
10
19
  const document = {
@@ -42,8 +51,12 @@ describe('document access policy', () => {
42
51
  expect(deriveDocumentsAccessChange('viewer', 'editor', null)).toBe('upgrade')
43
52
  expect(deriveDocumentsAccessChange('viewer', 'none', null)).toBe('revoked')
44
53
  expect(deriveDocumentsAccessChange('owner', 'owner', null)).toBeNull()
45
- expect(deriveDocumentsAccessChange(null, 'viewer', { kind: 'downgrade', at: 20 })).toBe('downgrade')
46
- expect(deriveDocumentsAccessChange('editor', 'viewer', { kind: 'revoked', at: 20 })).toBe('revoked')
54
+ expect(deriveDocumentsAccessChange(null, 'viewer', { kind: 'downgrade', at: 20 })).toBe(
55
+ 'downgrade',
56
+ )
57
+ expect(deriveDocumentsAccessChange('editor', 'viewer', { kind: 'revoked', at: 20 })).toBe(
58
+ 'revoked',
59
+ )
47
60
  })
48
61
 
49
62
  it('accepts only a fresh ACL signal addressed to the current user', () => {
@@ -29,6 +29,12 @@ export type LibraryNavSelection =
29
29
  | { kind: 'uncategorized' }
30
30
  | { kind: 'folder'; folderId: string }
31
31
 
32
+ /** Record controls are usable only after both the initial query and mutation
33
+ * transport are ready. Keep that boundary identical across feature pages. */
34
+ export function recordsReadyForMutation(queryStatus: string, mutationsReady: boolean): boolean {
35
+ return queryStatus === 'ready' && mutationsReady
36
+ }
37
+
32
38
  export function parseDocumentsIdList(raw: string | undefined): string[] {
33
39
  if (!raw) return []
34
40
  try {
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Demonstrates:
5
5
  * - 'unclaimed-or-own' permission for claimable tasks
6
- * - writableFields for restricting which fields can be updated
6
+ * - writableFields for restricting which fields can be created or updated
7
7
  * - timestampTrigger for automatic timestamps
8
8
  * - Admin grading workflow
9
9
  */
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * A claimable task system demonstrating:
5
5
  * - 'unclaimed-or-own' permission for claiming unclaimed items
6
- * - writableFields for restricting which fields users can update
6
+ * - writableFields for restricting which fields users can create or update
7
7
  * - timestampTrigger for automatic timestamp fields
8
8
  * - Admin-only grading workflow
9
9
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepspace",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
4
4
  "engines": {
5
5
  "node": ">=22.15.0 <23 || >=24 <25 || >=26 <27"
6
6
  },
@@ -56,13 +56,13 @@
56
56
  "prepublishOnly": "pnpm build"
57
57
  },
58
58
  "dependencies": {
59
- "@ai-sdk/anthropic": "^2.0.0",
60
- "@ai-sdk/openai": "^2.0.0",
61
- "@ai-sdk/openai-compatible": "^1.0.0",
59
+ "@ai-sdk/anthropic": "2.0.91",
60
+ "@ai-sdk/openai": "2.0.116",
61
+ "@ai-sdk/openai-compatible": "1.0.46",
62
62
  "@clack/prompts": "^1.2.0",
63
63
  "@mdx-js/mdx": "^3.1.1",
64
64
  "acorn": "^8.16.0",
65
- "ai": "^5.0.0",
65
+ "ai": "5.0.222",
66
66
  "better-auth": "1.6.23",
67
67
  "citty": "^0.2.0",
68
68
  "cross-spawn": "^7.0.6",
@@ -1,25 +0,0 @@
1
- /**
2
- * Cron task definitions for the heartbeat reference feature.
3
- *
4
- * AppCronRoom (declared in worker.ts) reads `tasks` at construction time
5
- * and validates them. The DO alarm fires `runTask(name, env)` on the
6
- * configured schedule; each fire is recorded in the DO's `cron_history`
7
- * table and pushed to subscribers over `/ws/cron/:roomId`.
8
- *
9
- * The heartbeat task here is the low-cost liveness probe used by both
10
- * the cron e2e spec and as a quick sanity check after a deploy. To add
11
- * your own task, append a {name, intervalMinutes} or {name, schedule,
12
- * timezone} entry to `tasks` and dispatch on `name` inside `runTask`.
13
- */
14
-
15
- import type { CronTask } from 'deepspace/worker'
16
-
17
- export const tasks: CronTask[] = [
18
- { name: 'heartbeat', intervalMinutes: 1 },
19
- ]
20
-
21
- export async function runTask(_name: string, _env: unknown): Promise<void> {
22
- // No-op work — the DO records every invocation in cron_history regardless,
23
- // which is what the e2e spec asserts against. Real apps would dispatch on
24
- // `_name` here and call into integrations / records via buildCronContext.
25
- }