@semiont/react-ui 0.4.20 → 0.4.22

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