@semiont/react-ui 0.4.20 → 0.4.21

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.
Files changed (108) hide show
  1. package/README.md +8 -5
  2. package/dist/{PdfAnnotationCanvas.client-CHDCGQBR.mjs → PdfAnnotationCanvas.client-6ZGFEN2N.mjs} +9 -13
  3. package/dist/PdfAnnotationCanvas.client-6ZGFEN2N.mjs.map +1 -0
  4. package/dist/TranslationManager-9Xj3MIWQ.d.mts +16 -0
  5. package/dist/chunk-KEDFYI6N.mjs +7788 -0
  6. package/dist/chunk-KEDFYI6N.mjs.map +1 -0
  7. package/dist/index.d.mts +171 -1140
  8. package/dist/index.mjs +3263 -13644
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/test-utils.d.mts +46 -21
  11. package/dist/test-utils.mjs +2499 -87
  12. package/dist/test-utils.mjs.map +1 -1
  13. package/package.json +1 -2
  14. package/src/components/AnnotateReferencesProgressWidget.tsx +21 -28
  15. package/src/components/CodeMirrorRenderer.tsx +9 -11
  16. package/src/components/StatusDisplay.tsx +42 -16
  17. package/src/components/Toolbar.tsx +4 -4
  18. package/src/components/__tests__/AnnotateReferencesProgressWidget.test.tsx +34 -20
  19. package/src/components/__tests__/StatusDisplay.test.tsx +47 -64
  20. package/src/components/__tests__/Toolbar.test.tsx +4 -4
  21. package/src/components/annotation/AnnotateToolbar.tsx +8 -7
  22. package/src/components/annotation/__tests__/AnnotateToolbar.test.tsx +31 -77
  23. package/src/components/annotation-popups/__tests__/JsonLdView.test.tsx +0 -1
  24. package/src/components/image-annotation/AnnotationOverlay.tsx +12 -13
  25. package/src/components/image-annotation/SvgDrawingCanvas.tsx +7 -7
  26. package/src/components/modals/PermissionDeniedModal.tsx +11 -11
  27. package/src/components/modals/ReferenceWizardModal.tsx +14 -18
  28. package/src/components/modals/ResourceSearchModal.tsx +10 -6
  29. package/src/components/modals/SearchModal.tsx +10 -6
  30. package/src/components/modals/SessionExpiredModal.tsx +11 -11
  31. package/src/components/modals/__tests__/PermissionDeniedModal.test.tsx +7 -7
  32. package/src/components/modals/__tests__/ResourceSearchModal.test.tsx +10 -8
  33. package/src/components/modals/__tests__/SearchModal.search-wiring.test.tsx +10 -7
  34. package/src/components/modals/__tests__/SessionExpiredModal.test.tsx +5 -5
  35. package/src/components/navigation/CollapsibleResourceNavigation.tsx +10 -10
  36. package/src/components/navigation/ObservableLink.tsx +6 -6
  37. package/src/components/navigation/SimpleNavigation.tsx +4 -4
  38. package/src/components/navigation/__tests__/ObservableLink.test.tsx +4 -4
  39. package/src/components/navigation/__tests__/SimpleNavigation.test.tsx +4 -4
  40. package/src/components/pdf-annotation/PdfAnnotationCanvas.tsx +9 -11
  41. package/src/components/pdf-annotation/__tests__/PdfAnnotationCanvas.test.tsx +0 -1
  42. package/src/components/resource/AnnotateView.tsx +7 -6
  43. package/src/components/resource/AnnotationHistory.tsx +9 -12
  44. package/src/components/resource/BrowseView.tsx +8 -7
  45. package/src/components/resource/ResourceViewer.tsx +17 -25
  46. package/src/components/resource/__tests__/AnnotationHistory.test.tsx +54 -192
  47. package/src/components/resource/__tests__/BrowseView.test.tsx +34 -83
  48. package/src/components/resource/__tests__/ResourceViewer.mode-switch.test.tsx +40 -31
  49. package/src/components/resource/panels/AssessmentEntry.tsx +5 -4
  50. package/src/components/resource/panels/AssessmentPanel.tsx +19 -15
  51. package/src/components/resource/panels/AssistSection.tsx +11 -13
  52. package/src/components/resource/panels/CollaborationPanel.tsx +29 -7
  53. package/src/components/resource/panels/CommentEntry.tsx +5 -4
  54. package/src/components/resource/panels/CommentsPanel.tsx +9 -11
  55. package/src/components/resource/panels/HighlightEntry.tsx +5 -4
  56. package/src/components/resource/panels/HighlightPanel.tsx +10 -11
  57. package/src/components/resource/panels/ReferenceEntry.tsx +8 -8
  58. package/src/components/resource/panels/ReferencesPanel.tsx +13 -12
  59. package/src/components/resource/panels/ResourceInfoPanel.tsx +7 -6
  60. package/src/components/resource/panels/TagEntry.tsx +5 -4
  61. package/src/components/resource/panels/TaggingPanel.tsx +10 -16
  62. package/src/components/resource/panels/UnifiedAnnotationsPanel.tsx +3 -2
  63. package/src/components/resource/panels/__tests__/AssessmentPanel.test.tsx +18 -52
  64. package/src/components/resource/panels/__tests__/CollaborationPanel.test.tsx +51 -20
  65. package/src/components/resource/panels/__tests__/CommentsPanel.test.tsx +18 -56
  66. package/src/components/resource/panels/__tests__/HighlightPanel.annotationProgress.test.tsx +0 -1
  67. package/src/components/resource/panels/__tests__/ReferenceEntry.test.tsx +4 -5
  68. package/src/components/resource/panels/__tests__/ReferencesPanel.observable-flow.test.tsx +153 -0
  69. package/src/components/resource/panels/__tests__/ReferencesPanel.test.tsx +51 -106
  70. package/src/components/resource/panels/__tests__/ResourceInfoPanel.test.tsx +15 -47
  71. package/src/components/resource/panels/__tests__/TaggingPanel.test.tsx +15 -47
  72. package/src/components/settings/SettingsPanel.tsx +8 -8
  73. package/src/components/settings/__tests__/SettingsPanel.test.tsx +12 -12
  74. package/src/features/admin-devops/components/AdminDevOpsPage.tsx +1 -1
  75. package/src/features/admin-exchange/components/AdminExchangePage.tsx +1 -1
  76. package/src/features/admin-exchange/components/ImportCard.tsx +2 -6
  77. package/src/features/admin-security/components/AdminSecurityPage.tsx +1 -1
  78. package/src/features/admin-users/components/AdminUsersPage.tsx +1 -1
  79. package/src/features/moderate-entity-tags/components/EntityTagsPage.tsx +1 -1
  80. package/src/features/moderate-recent/components/RecentDocumentsPage.tsx +1 -1
  81. package/src/features/moderate-tag-schemas/components/TagSchemasPage.tsx +1 -1
  82. package/src/features/moderation-linked-data/components/LinkedDataPage.tsx +1 -1
  83. package/src/features/resource-compose/__tests__/ResourceComposePage.test.tsx +5 -3
  84. package/src/features/resource-compose/components/ResourceComposePage.tsx +5 -22
  85. package/src/features/resource-discovery/__tests__/ResourceDiscoveryPage.test.tsx +4 -3
  86. package/src/features/resource-discovery/components/ResourceDiscoveryPage.tsx +1 -1
  87. package/src/features/resource-viewer/__tests__/ResourceViewerPage.test.tsx +38 -45
  88. package/src/features/resource-viewer/components/ResourceViewerPage.tsx +123 -192
  89. package/dist/KnowledgeBaseSessionContext-BNNunwzO.d.mts +0 -175
  90. package/dist/PdfAnnotationCanvas.client-CHDCGQBR.mjs.map +0 -1
  91. package/dist/chunk-OZICDVH7.mjs +0 -62
  92. package/dist/chunk-OZICDVH7.mjs.map +0 -1
  93. package/dist/chunk-R4CCMFJH.mjs +0 -877
  94. package/dist/chunk-R4CCMFJH.mjs.map +0 -1
  95. package/dist/chunk-VN5NY4SN.mjs +0 -200
  96. package/dist/chunk-VN5NY4SN.mjs.map +0 -1
  97. package/src/components/modals/ProposeEntitiesModal.tsx +0 -179
  98. package/src/components/modals/__tests__/ProposeEntitiesModal.test.tsx +0 -129
  99. package/src/features/resource-viewer/__tests__/AnnotationCreationPending.test.tsx +0 -323
  100. package/src/features/resource-viewer/__tests__/AnnotationDeletionIntegration.test.tsx +0 -245
  101. package/src/features/resource-viewer/__tests__/AnnotationProgressDismissal.test.tsx +0 -303
  102. package/src/features/resource-viewer/__tests__/BindFlowIntegration.test.tsx +0 -150
  103. package/src/features/resource-viewer/__tests__/DetectionFlowBug.test.tsx +0 -243
  104. package/src/features/resource-viewer/__tests__/DetectionFlowIntegration.test.tsx +0 -383
  105. package/src/features/resource-viewer/__tests__/ResourceMutations.test.tsx +0 -299
  106. package/src/features/resource-viewer/__tests__/ToastNotifications.test.tsx +0 -186
  107. package/src/features/resource-viewer/__tests__/YieldFlowIntegration.test.tsx +0 -429
  108. package/src/features/resource-viewer/__tests__/annotation-progress-flow.test.tsx +0 -348
@@ -5,9 +5,9 @@
5
5
  * Only requires minimal props from the framework layer (routing, modals).
6
6
  */
7
7
 
8
- import React, { useState, useEffect, useCallback, useMemo } from 'react';
9
- import { useQueryClient } from '@tanstack/react-query';
8
+ import React, { useState, useEffect, useCallback } from 'react';
10
9
  import type { components, ResourceId, GatheredContext, EventMap } from '@semiont/core';
10
+ import type { ConnectionState } from '@semiont/api-client';
11
11
  import { annotationId } from '@semiont/core';
12
12
  import { getLanguage, getPrimaryRepresentation, getPrimaryMediaType, getMimeCategory } from '@semiont/api-client';
13
13
  import { ANNOTATORS } from '@semiont/react-ui';
@@ -21,34 +21,23 @@ import { Toolbar } from '@semiont/react-ui';
21
21
  import { useResourceLoadingAnnouncements } from '@semiont/react-ui';
22
22
  import { ResourceViewer } from '@semiont/react-ui';
23
23
  import { useObservable } from '@semiont/react-ui';
24
- import { QUERY_KEYS } from '../../../lib/query-keys';
25
- import { useResources, useEntityTypes } from '../../../lib/api-hooks';
26
24
  import { useResourceContent } from '../../../hooks/useResourceContent';
27
25
  import { useMediaToken } from '../../../hooks/useMediaToken';
28
26
  import { useToast } from '../../../components/Toast';
29
27
  import { useTheme } from '../../../contexts/ThemeContext';
30
28
  import { useLineNumbers } from '../../../hooks/useLineNumbers';
31
29
  import { useHoverDelay } from '../../../hooks/useHoverDelay';
32
- import { useResourceEvents } from '../../../hooks/useResourceEvents';
33
- import { useOpenResources } from '../../../contexts/OpenResourcesContext';
34
- // Import EventBus hooks directly from context to avoid mocking issues in tests
35
- import { useEventBus } from '../../../contexts/EventBusContext';
36
30
  import { useEventSubscriptions } from '../../../contexts/useEventSubscription';
37
31
  import { useResourceAnnotations } from '../../../contexts/ResourceAnnotationsContext';
38
- import { useApiClient } from '../../../contexts/ApiClientContext';
39
- import { useBindFlow } from '../../../hooks/useBindFlow';
40
- import { useMarkFlow } from '../../../hooks/useMarkFlow';
41
- import { useBeckonFlow } from '../../../hooks/useBeckonFlow';
42
- import type { StreamStatus } from '../../../hooks/useResourceEvents';
43
- import { usePanelBrowse } from '../../../hooks/usePanelBrowse';
44
- import { useYieldFlow } from '../../../hooks/useYieldFlow';
45
- import { useContextGatherFlow } from '../../../hooks/useContextGatherFlow';
32
+ import { useSemiont } from '../../../session/SemiontProvider';
33
+ import { createResourceViewerPageVM } from '@semiont/api-client';
34
+ import { useViewModel } from '../../../hooks/useViewModel';
35
+ import { useShellVM } from '../../../hooks/useShellVM';
46
36
  import { useTranslations } from '../../../contexts/TranslationContext';
47
37
  import { ReferenceWizardModal } from '../../../components/modals/ReferenceWizardModal';
48
38
  import type { GenerationConfig } from '../../../components/modals/ConfigureGenerationStep';
49
39
 
50
40
  type SemiontResource = components['schemas']['ResourceDescriptor'];
51
- type Annotation = components['schemas']['Annotation'];
52
41
 
53
42
  export interface ResourceViewerPageProps {
54
43
  /**
@@ -87,9 +76,11 @@ export interface ResourceViewerPageProps {
87
76
  refetchDocument: () => Promise<unknown>;
88
77
 
89
78
  /**
90
- * SSE attention stream connection status for the active workspace
79
+ * Bus connection state for the active workspace. Six-valued state
80
+ * machine from `actor.state$`; CollaborationPanel maps it to the
81
+ * "Live" / "Disconnected" visual.
91
82
  */
92
- streamStatus: StreamStatus;
83
+ streamStatus: ConnectionState;
93
84
 
94
85
  /**
95
86
  * Name of the active knowledge base (for display in panels)
@@ -102,7 +93,7 @@ export interface ResourceViewerPageProps {
102
93
  *
103
94
  * Uses hooks directly (NO containers, NO render props, NO ResourceViewerPageContent wrapper)
104
95
  *
105
- * @emits browse:router-push - Navigate to a resource or filtered view
96
+ * @emits nav:push - Navigate to a resource or filtered view
106
97
  * @emits beckon:sparkle - Trigger sparkle animation on an annotation
107
98
  * @emits bind:update-body - Update annotation body content
108
99
  * @subscribes mark:archive - Archive the current resource
@@ -138,23 +129,17 @@ export function ResourceViewerPage({
138
129
  // Translations
139
130
  const tw = useTranslations('ReferenceWizard');
140
131
 
141
- // Get unified event bus for subscribing to UI events
142
- const eventBus = useEventBus();
143
- const semiont = useApiClient();
144
- const queryClient = useQueryClient(); // retained for non-store queries (events log)
132
+ const browser = useSemiont();
133
+ const session = useObservable(browser.activeSession$);
134
+ const semiont = session?.client;
145
135
 
146
136
  // UI state hooks
147
- const { showError, showSuccess } = useToast();
137
+ const { showError, showSuccess, showInfo } = useToast();
148
138
  const { theme, setTheme } = useTheme();
149
139
  const { showLineNumbers, toggleLineNumbers } = useLineNumbers();
150
140
  const { hoverDelayMs } = useHoverDelay();
151
- const { addResource } = useOpenResources();
152
141
  const { triggerSparkleAnimation, clearNewAnnotationId } = useResourceAnnotations();
153
142
 
154
- // API hooks
155
- const resources = useResources();
156
- const entityTypesAPI = useEntityTypes();
157
-
158
143
  // Determine MIME category to choose content path
159
144
  const resourceMediaType = getPrimaryMediaType(resource) || 'text/plain';
160
145
  const isBinary = getMimeCategory(resourceMediaType) === 'image';
@@ -171,56 +156,42 @@ export function ResourceViewerPage({
171
156
  const content = isBinary ? binaryContent : textContent;
172
157
  const contentLoading = isBinary ? mediaTokenLoading : textLoading;
173
158
 
174
- const annotationsData = useObservable(semiont.browse.annotations(rUri));
175
- const annotations = useMemo(
176
- () => annotationsData || [],
177
- [annotationsData]
178
- );
179
-
180
- const { data: referencedByData, isLoading: referencedByLoading } = resources.referencedBy.useQuery(rUri);
181
- const referencedBy = referencedByData?.referencedBy || [];
182
-
183
- const { data: entityTypesData } = entityTypesAPI.list.useQuery();
184
- const allEntityTypes = (entityTypesData as { entityTypes: string[] } | undefined)?.entityTypes || [];
185
-
186
- // Flow state hooks (NO CONTAINERS)
187
- const { hoveredAnnotationId } = useBeckonFlow();
188
- const { assistingMotivation, progress, pendingAnnotation } = useMarkFlow(rUri);
189
- const { activePanel, scrollToAnnotationId, panelInitialTab, onScrollCompleted } = usePanelBrowse();
190
- useBindFlow(rUri);
191
- const {
192
- generationProgress,
193
- onGenerateDocument,
194
- } = useYieldFlow(locale, rUri, clearNewAnnotationId);
195
- const { gatherContext, gatherLoading, gatherError } = useContextGatherFlow({ resourceId: rUri });
196
-
197
- // Wizard state driven by bind:initiate from ReferenceEntry
198
- const [wizardOpen, setWizardOpen] = useState(false);
199
- const [wizardAnnotationId, setWizardAnnotationId] = useState<string | null>(null);
200
- const [wizardResourceId, setWizardResourceId] = useState<string | null>(null);
201
- const [wizardDefaultTitle, setWizardDefaultTitle] = useState('');
202
- const [wizardEntityTypes, setWizardEntityTypes] = useState<string[]>([]);
203
-
204
- useEffect(() => {
205
- const subscription = eventBus.get('bind:initiate').subscribe((event) => {
206
- setWizardAnnotationId(event.annotationId);
207
- setWizardResourceId(event.resourceId);
208
- setWizardDefaultTitle(event.defaultTitle);
209
- setWizardEntityTypes(event.entityTypes);
210
- setWizardOpen(true);
211
-
212
- // Trigger context gathering — gather:requested is consumed by useContextGatherFlow
213
- eventBus.get('gather:requested').next({ correlationId: crypto.randomUUID(), annotationId: event.annotationId, resourceId: event.resourceId, options: { contextWindow: 2000 } });
214
- });
215
- return () => subscription.unsubscribe();
216
- }, [eventBus]);
159
+ // Composite VM — owns all flow VMs, wizard state, annotations, entity types
160
+ const browseVM = useShellVM();
161
+ const vm = useViewModel(() => createResourceViewerPageVM(semiont!, rUri, locale, browseVM));
162
+
163
+ const annotations = useObservable(vm.annotations$) ?? [];
164
+ const groups = useObservable(vm.annotationGroups$);
165
+ const allEntityTypes = useObservable(vm.entityTypes$) ?? [];
166
+ const referencedByRaw = useObservable(vm.referencedBy$);
167
+ const referencedBy = referencedByRaw ?? [];
168
+ const referencedByLoading = referencedByRaw === undefined;
169
+ const hoveredAnnotationId = useObservable(vm.beckon.hoveredAnnotationId$) ?? null;
170
+ const pendingAnnotation = useObservable(vm.mark.pendingAnnotation$) ?? null;
171
+ const assistingMotivation = useObservable(vm.mark.assistingMotivation$) ?? null;
172
+ const progress = useObservable(vm.mark.progress$) ?? null;
173
+ const activePanel = useObservable(vm.browse.activePanel$) ?? null;
174
+ const scrollToAnnotationId = useObservable(vm.browse.scrollToAnnotationId$) ?? null;
175
+ const panelInitialTab = useObservable(vm.browse.panelInitialTab$) ?? null;
176
+ const onScrollCompleted = vm.browse.onScrollCompleted;
177
+ const generationProgress = useObservable(vm.yield.progress$) ?? null;
178
+ const gatherContext = useObservable(vm.gather.context$) ?? null;
179
+ const gatherLoading = useObservable(vm.gather.loading$) ?? false;
180
+ const gatherError = useObservable(vm.gather.error$) ?? null;
181
+ const wizardState = useObservable(vm.wizard$);
182
+ const wizardOpen = wizardState?.open ?? false;
183
+ const wizardAnnotationId = wizardState?.annotationId ?? null;
184
+ const wizardResourceId = wizardState?.resourceId ?? null;
185
+ const wizardDefaultTitle = wizardState?.defaultTitle ?? '';
186
+ const wizardEntityTypes = wizardState?.entityTypes ?? [];
217
187
 
218
188
  const handleWizardClose = useCallback(() => {
219
- setWizardOpen(false);
220
- }, []);
189
+ vm.closeWizard();
190
+ }, [vm]);
221
191
 
222
192
  const handleWizardGenerateSubmit = useCallback((referenceId: string, config: GenerationConfig) => {
223
- onGenerateDocument(referenceId, {
193
+ clearNewAnnotationId(annotationId(referenceId));
194
+ vm.yield.generate(referenceId, {
224
195
  title: config.title,
225
196
  storageUri: config.storagePath,
226
197
  prompt: config.prompt,
@@ -229,9 +200,10 @@ export function ResourceViewerPage({
229
200
  maxTokens: config.maxTokens,
230
201
  context: config.context,
231
202
  });
232
- }, [onGenerateDocument]);
203
+ }, [vm, clearNewAnnotationId]);
233
204
 
234
205
  const handleWizardLinkResource = useCallback(async (referenceId: string, targetResourceId: string) => {
206
+ if (!semiont) return;
235
207
  try {
236
208
  await semiont.bind.body(
237
209
  rUri,
@@ -259,103 +231,71 @@ export function ResourceViewerPage({
259
231
  name: title,
260
232
  entityTypes: entTypes.join(','),
261
233
  });
262
- eventBus.get('browse:router-push').next({
234
+ browser.emit('nav:push', {
263
235
  path: `/know/compose?${params.toString()}`,
264
236
  reason: 'compose-from-wizard',
265
237
  });
266
- }, []); // eventBus is stable singleton
238
+ }, [session]);
267
239
 
268
240
  // Add resource to open tabs when it loads
269
241
  useEffect(() => {
270
242
  if (resource && rUri) {
271
243
  const mediaType = getPrimaryMediaType(resource);
272
- addResource(rUri, resource.name, mediaType || undefined, resource.storageUri);
244
+ browser.addOpenResource(rUri, resource.name, mediaType || undefined, resource.storageUri);
273
245
  if (typeof localStorage !== 'undefined') {
274
246
  localStorage.setItem('lastViewedDocumentId', rUri);
275
247
  }
276
248
  }
277
- }, [resource, rUri, addResource]);
278
-
279
- // Real-time document events (SSE)
280
- // Annotation updates are handled by AnnotationStore reacting to EventBus events.
281
- // Callbacks here only handle non-annotation side effects.
282
- useResourceEvents({
283
- rUri,
284
- autoConnect: true,
285
-
286
- onAnnotationAdded: useCallback((_event: any) => {
287
- // Store handles annotation refresh; events log needs explicit invalidation
288
- queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.events(rUri) });
289
- }, [queryClient, rUri]),
290
-
291
- onAnnotationRemoved: useCallback((_event: any) => {
292
- queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.events(rUri) });
293
- }, [queryClient, rUri]),
294
-
295
- onAnnotationBodyUpdated: useCallback((_event: any) => {
296
- queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.events(rUri) });
297
- }, [queryClient, rUri]),
298
-
299
- // Document status events
300
- onDocumentArchived: useCallback((_event: any) => {
301
- refetchDocument();
302
- showSuccess('This document has been archived');
303
- }, [refetchDocument, showSuccess]),
304
-
305
- onDocumentUnarchived: useCallback((_event: any) => {
306
- refetchDocument();
307
- showSuccess('This document has been unarchived');
308
- }, [refetchDocument, showSuccess]),
309
-
310
- // Entity tag events
311
- onEntityTagAdded: useCallback((_event: any) => {
312
- refetchDocument();
313
- }, [refetchDocument]),
314
-
315
- onEntityTagRemoved: useCallback((_event: any) => {
316
- refetchDocument();
317
- }, [refetchDocument]),
318
-
319
- onError: useCallback((error: any) => {
320
- console.error('[RealTime] Event stream error:', error);
321
- }, []),
322
- });
249
+ }, [resource, rUri, browser]);
323
250
 
324
- // Mutations hoisted to top level hooks must not be called inside callbacks
325
- const updateMutation = resources.update.useMutation();
326
- const generateCloneTokenMutation = resources.generateCloneToken.useMutation();
251
+ // Bridge: when the mark VM produces a pending annotation, open the
252
+ // annotations panel. The mark VM (session-scoped) can't emit `panel:open`
253
+ // (app-scoped) directly — the React tree is the natural seam between
254
+ // the two buses.
255
+ useEffect(() => {
256
+ if (pendingAnnotation) {
257
+ browser.emit('panel:open', { panel: 'annotations' });
258
+ }
259
+ }, [pendingAnnotation, browser]);
260
+
261
+ // Domain events flow through the bus gateway (ActorVM → local EventBus).
262
+ // BrowseNamespace cache invalidation handles annotation/resource updates.
263
+ // The resource-viewer-page-vm calls client.subscribeToResource(resourceId)
264
+ // which bridges scoped domain events into the local EventBus.
327
265
 
328
- // Event handlers extracted to useCallback (tenet: no inline handlers in useEventSubscriptions)
329
266
  const handleResourceArchive = useCallback(async () => {
267
+ if (!semiont) return;
330
268
  try {
331
- await updateMutation.mutateAsync({ id: rUri, data: { archived: true } });
269
+ await semiont.updateResource(rUri, { archived: true });
332
270
  await refetchDocument();
333
271
  } catch (err) {
334
272
  console.error('Failed to archive document:', err);
335
273
  showError('Failed to archive document');
336
274
  }
337
- }, [updateMutation, rUri, refetchDocument, showError]);
275
+ }, [semiont, rUri, refetchDocument, showError]);
338
276
 
339
277
  const handleResourceUnarchive = useCallback(async () => {
278
+ if (!semiont) return;
340
279
  try {
341
- await updateMutation.mutateAsync({ id: rUri, data: { archived: false } });
280
+ await semiont.updateResource(rUri, { archived: false });
342
281
  await refetchDocument();
343
282
  } catch (err) {
344
283
  console.error('Failed to unarchive document:', err);
345
284
  showError('Failed to unarchive document');
346
285
  }
347
- }, [updateMutation, rUri, refetchDocument, showError]);
286
+ }, [semiont, rUri, refetchDocument, showError]);
348
287
 
349
288
  const handleResourceClone = useCallback(async () => {
289
+ if (!semiont) return;
350
290
  try {
351
- const result = await generateCloneTokenMutation.mutateAsync(rUri);
291
+ const result = await semiont.generateCloneToken(rUri);
352
292
  const token = result.token;
353
- eventBus.get('browse:router-push').next({ path: `/know/compose?mode=clone&token=${token}`, reason: 'clone' });
293
+ browser.emit('nav:push', { path: `/know/compose?mode=clone&token=${token}`, reason: 'clone' });
354
294
  } catch (err) {
355
295
  console.error('Failed to generate clone token:', err);
356
296
  showError('Failed to generate clone link');
357
297
  }
358
- }, [generateCloneTokenMutation, rUri, showError]);
298
+ }, [semiont, rUri, showError, session]);
359
299
 
360
300
  const handleAnnotationSparkle = useCallback(({ annotationId }: { annotationId: string }) => {
361
301
  triggerSparkleAnimation(annotationId);
@@ -365,43 +305,58 @@ export function ResourceViewerPage({
365
305
  triggerSparkleAnimation(stored.payload.annotation.id);
366
306
  }, [triggerSparkleAnimation]);
367
307
 
368
- const handleAnnotationCreateFailed = useCallback(() => showError('Failed to create annotation'), [showError]);
369
- const handleAnnotationDeleteFailed = useCallback(() => showError('Failed to delete annotation'), [showError]);
308
+ const handleAnnotationCreateFailed = useCallback(({ message }: { message?: string }) =>
309
+ showError(`Failed to create annotation: ${message || 'unknown error'}`), [showError]);
310
+ const handleAnnotationDeleteFailed = useCallback(({ message }: { message?: string }) =>
311
+ showError(`Failed to delete annotation: ${message || 'unknown error'}`), [showError]);
370
312
  const handleAnnotateBodyUpdated = useCallback(() => {
371
- // Success - optimistic update already applied via useResourceEvents
313
+ // Success - optimistic update already applied via EventBus
372
314
  }, []);
373
- const handleAnnotateBodyUpdateFailed = useCallback(() => showError('Failed to update annotation'), [showError]);
315
+ const handleAnnotateBodyUpdateFailed = useCallback(({ message }: { message: string }) =>
316
+ showError(`Failed to update reference: ${message}`), [showError]);
374
317
 
375
318
  const handleSettingsThemeChanged = useCallback(({ theme }: { theme: any }) => setTheme(theme), [setTheme]);
376
319
 
377
- const handleDetectionComplete = useCallback(() => {
378
- // Toast notification is handled by useMarkFlow; store handles annotation refresh
379
- queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.events(rUri) });
380
- }, [queryClient, rUri]);
381
- const handleDetectionFailed = useCallback(() => {
382
- // Error notification is handled by useMarkFlow; store handles annotation refresh
383
- queryClient.invalidateQueries({ queryKey: QUERY_KEYS.resources.events(rUri) });
384
- }, [queryClient, rUri]);
385
- const handleGenerationComplete = useCallback(() => {
386
- // Toast notification is handled by useYieldFlow
387
- }, []);
388
- const handleGenerationFailed = useCallback(() => {
389
- // Error notification is handled by useYieldFlow
390
- }, []);
320
+ // Unified job lifecycle handlers. `job:complete` / `job:fail` fire
321
+ // for every job type (annotation + generation); we dispatch on
322
+ // jobType and filter to this resource. `annotationId` is present on
323
+ // jobs attached to a specific annotation (today: generation from a
324
+ // reference); it's what UI consumers lower down in the tree use to
325
+ // attach per-annotation visual feedback.
326
+ const handleJobComplete = useCallback((event: components['schemas']['JobCompleteCommand']) => {
327
+ if (event.resourceId !== (resource.id as string)) return;
328
+ if (event.jobType === 'generation') {
329
+ const result = event.result as components['schemas']['JobGenerationResult'] | undefined;
330
+ const name = result?.resourceName;
331
+ showSuccess(name
332
+ ? `Resource "${name}" created successfully!`
333
+ : 'Resource created successfully!');
334
+ } else {
335
+ showSuccess('Annotation complete');
336
+ }
337
+ }, [resource.id, showSuccess]);
338
+ const handleJobFailed = useCallback((event: components['schemas']['JobFailCommand']) => {
339
+ if (event.resourceId !== (resource.id as string)) return;
340
+ if (event.jobType === 'generation') {
341
+ showError(`Resource generation failed: ${event.error}`);
342
+ } else {
343
+ showError(event.error || 'Annotation failed');
344
+ }
345
+ }, [resource.id, showError]);
391
346
 
392
347
  const handleReferenceNavigate = useCallback(({ resourceId }: { resourceId: string }) => {
393
348
  if (routes.resourceDetail) {
394
349
  const path = routes.resourceDetail(resourceId);
395
- eventBus.get('browse:router-push').next({ path, reason: 'reference-link' });
350
+ browser.emit('nav:push', { path, reason: 'reference-link' });
396
351
  }
397
- }, [routes.resourceDetail]); // eventBus is stable singleton - never in deps
352
+ }, [routes.resourceDetail, session]);
398
353
 
399
354
  const handleEntityTypeClicked = useCallback(({ entityType }: { entityType: string }) => {
400
355
  if (routes.know) {
401
356
  const path = `${routes.know}?entityType=${encodeURIComponent(entityType)}`;
402
- eventBus.get('browse:router-push').next({ path, reason: 'entity-type-filter' });
357
+ browser.emit('nav:push', { path, reason: 'entity-type-filter' });
403
358
  }
404
- }, [routes.know]); // eventBus is stable singleton - never in deps
359
+ }, [routes.know, session]);
405
360
 
406
361
  const handleModeToggled = useCallback(() => {
407
362
  setAnnotateMode(prev => !prev);
@@ -421,10 +376,9 @@ export function ResourceViewerPage({
421
376
  'bind:body-update-failed': handleAnnotateBodyUpdateFailed,
422
377
  'settings:theme-changed': handleSettingsThemeChanged,
423
378
  'settings:line-numbers-toggled': toggleLineNumbers,
424
- 'mark:assist-finished': handleDetectionComplete,
425
- 'mark:assist-failed': handleDetectionFailed,
426
- 'yield:finished': handleGenerationComplete,
427
- 'yield:failed': handleGenerationFailed,
379
+ 'job:complete': handleJobComplete,
380
+ 'job:fail': handleJobFailed,
381
+ 'mark:assist-cancelled': () => showInfo('Annotation cancelled'),
428
382
  'browse:reference-navigate': handleReferenceNavigate,
429
383
  'browse:entity-type-clicked': handleEntityTypeClicked,
430
384
  });
@@ -460,28 +414,6 @@ export function ResourceViewerPage({
460
414
  return false;
461
415
  });
462
416
 
463
- // Group annotations by type using static ANNOTATORS (memoized to avoid re-grouping on unrelated re-renders)
464
- const groups = useMemo(() => {
465
- const result = {
466
- highlights: [] as Annotation[],
467
- references: [] as Annotation[],
468
- assessments: [] as Annotation[],
469
- comments: [] as Annotation[],
470
- tags: [] as Annotation[]
471
- };
472
-
473
- for (const ann of annotations) {
474
- const annotator = Object.values(ANNOTATORS).find(a => a.matchesAnnotation(ann));
475
- if (annotator) {
476
- const key = annotator.internalType + 's'; // highlight -> highlights
477
- if (result[key as keyof typeof result]) {
478
- result[key as keyof typeof result].push(ann);
479
- }
480
- }
481
- }
482
-
483
- return result;
484
- }, [annotations]);
485
417
 
486
418
  // Combine resource with content
487
419
  const resourceWithContent = { ...resource, content };
@@ -489,9 +421,9 @@ export function ResourceViewerPage({
489
421
  // Handlers for AnnotationHistory (legacy event-based interaction)
490
422
  const handleEventHover = useCallback((annotationId: string | null) => {
491
423
  if (annotationId) {
492
- eventBus.get('beckon:sparkle').next({ annotationId });
424
+ session?.client.emit('beckon:sparkle', { annotationId });
493
425
  }
494
- }, []); // eventBus is stable singleton - never in deps
426
+ }, [session]);
495
427
 
496
428
  const handleEventClick = useCallback((_annotationId: string | null) => {
497
429
  // ResourceViewer now manages scroll state internally
@@ -539,8 +471,8 @@ export function ResourceViewerPage({
539
471
  ) : (
540
472
  <ResourceViewer
541
473
  resource={resourceWithContent}
542
- annotations={groups}
543
- generatingReferenceId={generationProgress?.referenceId ?? null}
474
+ annotations={groups ?? { highlights: [], comments: [], assessments: [], references: [], tags: [] }}
475
+ generatingReferenceId={generationProgress?.annotationId ?? null}
544
476
  showLineNumbers={showLineNumbers}
545
477
  hoverDelayMs={hoverDelayMs}
546
478
  hoveredAnnotationId={hoveredAnnotationId}
@@ -583,7 +515,7 @@ export function ResourceViewerPage({
583
515
  progress={progress}
584
516
  pendingAnnotation={pendingAnnotation}
585
517
  allEntityTypes={allEntityTypes}
586
- generatingReferenceId={generationProgress?.referenceId ?? null}
518
+ generatingReferenceId={generationProgress?.annotationId ?? null}
587
519
  referencedBy={referencedBy}
588
520
  referencedByLoading={referencedByLoading}
589
521
  resourceId={rUri}
@@ -632,7 +564,7 @@ export function ResourceViewerPage({
632
564
  {/* Collaboration Panel */}
633
565
  {activePanel === 'collaboration' && (
634
566
  <CollaborationPanel
635
- isConnected={streamStatus === 'connected'}
567
+ state={streamStatus}
636
568
  eventCount={0}
637
569
  knowledgeBaseName={knowledgeBaseName}
638
570
  />
@@ -665,7 +597,6 @@ export function ResourceViewerPage({
665
597
  context={gatherContext}
666
598
  contextLoading={gatherLoading}
667
599
  contextError={gatherError}
668
- eventBus={eventBus}
669
600
  onGenerateSubmit={handleWizardGenerateSubmit}
670
601
  onLinkResource={handleWizardLinkResource}
671
602
  onComposeNavigate={handleWizardComposeNavigate}