@novoagents/react 0.3.0-alpha.5 → 0.3.0-alpha.51

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.
package/CHANGELOG.md CHANGED
@@ -2,108 +2,202 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
- ## 0.3.0-alpha.5 - 2026-07-31
5
+ ## 0.3.0-alpha.50 - 2026-09-20
6
6
 
7
- - Companion release for `novoagents` 0.5.0-alpha.5. No source change: the
8
- companion carries a type-only EXACT peer on the core release it was published
9
- against, so it must move in lockstep or a consumer that installs both ends up
10
- with an unsatisfiable peer and, worse, two copies of the run-controller type.
11
-
12
- ## 0.3.0-alpha.4 - 2026-07-29
13
-
14
- - Companion release for `novoagents` 0.5.0-alpha.4 (the browser SSE source
15
- now applies the same cursor-established rule as the server reader for
16
- id-less data frames, and `isFollowupQueueFullDetails` requires the caps the
17
- server always sends). The hook source is unchanged; the exact SDK peer
18
- moves.
19
-
20
- ## 0.3.0-alpha.3 - 2026-07-29
21
-
22
- - Companion release for `novoagents` 0.5.0-alpha.3 (stream schema v5:
23
- heartbeat covers long tool executions with `phase: 'executing_tool'` and
24
- `waitId`, `followup_queue_full` becomes a first-class error code,
25
- `tool-input-delta` is forwarded, and an id-less data frame after an
26
- established cursor is a fatal transport error). The hook source is
27
- unchanged; the exact SDK peer moves to the v5 contract.
28
-
29
- ## 0.3.0-alpha.2 - 2026-07-28
30
-
31
- - Companion release for `novoagents` 0.5.0-alpha.2 (stream schema v4:
32
- `data-novo-step-terminal` splits overflow reporting into `truncated` for
33
- clipped text and `messagesOmitted` for dropped message arrays, and
34
- `artifactRef` is removed). The hook source is unchanged; the exact SDK peer
35
- moves to the v4 contract, and the packed tarball check now verifies the
36
- installed wire contract against the workspace artifact instead of a pinned
37
- revision literal.
38
-
39
- ## 0.3.0-alpha.1 - 2026-07-28
40
-
41
- - Companion release for `novoagents` 0.5.0-alpha.1 (stream schema v3:
42
- `platform_error` replaces the three engine-named terminal reasons, terminal
43
- errors carry `retryable`, and the SDK ships the generated
44
- `novoagents/wire-contract.json` compatibility manifest). The hook source is
45
- unchanged; the exact SDK peer moves to the v3 contract, and the packed
46
- tarball check verifies the installed wire contract's protocol revision.
47
-
48
- ## 0.2.0-alpha.7 - 2026-07-16
49
-
50
- - Companion release for `novoagents` 0.4.0-alpha.7. The hook source remains
51
- unchanged; its exact SDK peer moves with the App-only GitHub contract.
52
- - The workspace declares `novoagents` as a development dependency so a clean
53
- CI install can compile the peer-backed `novoagents/browser` imports;
54
- consumers still receive only the exact peer dependency, never a nested SDK.
55
-
56
- ## 0.2.0-alpha.6 - 2026-07-16
57
-
58
- - Companion release for `novoagents` 0.4.0-alpha.6. The hook source is
59
- unchanged; publishing the React package alongside the SDK keeps its exact
60
- `novoagents` peer on the current canonical browser-controller contract.
61
-
62
- ## 0.2.0-alpha.5 - 2026-07-16
63
-
64
- - Moves the type-only `novoagents` relationship from a runtime dependency to
65
- an exact peer dependency on 0.4.0-alpha.5. The hooks still require the
66
- matching canonical browser-controller contract, while package managers now
67
- install one app-owned SDK copy instead of retaining an older nested copy or
68
- requiring a consumer override.
69
-
70
- ## 0.2.0-alpha.4 - 2026-07-16
71
-
72
- - Superseded by 0.2.0-alpha.5. The alpha.4 registry artifact was published from
73
- the workspace source with npm, which retained the internal `workspace:*`
74
- peer specifier instead of pnpm's exact-version rewrite. It is not assigned a
75
- dist-tag and should not be installed.
76
-
77
- ## 0.2.0-alpha.3 - 2026-07-15
78
-
79
- - `useNovoRun` exposes the latest provider-neutral thinking/activity snapshot
80
- on `state.progress`, or `null` after answer, pause, restart, or terminal
81
- boundaries. When safe readable reasoning is available, the snapshot may be a
82
- short reasoning-derived summary; it never contains raw reasoning. The hook
83
- inherits the canonical browser reducer behavior from `novoagents`.
84
-
85
- ## 0.2.0-alpha.2 - 2026-07-15
86
-
87
- - Updates the packed `novoagents` dependency to 0.4.0-alpha.2, so React run
88
- hooks consume targeted `data-novo-model-step-restart` retractions through the
89
- canonical browser controller.
90
-
91
- ## 0.2.0-alpha.1 - 2026-07-14
92
-
93
- - `useNovoRun` normalizes `enabled` and `runKey` before its dependency arrays,
94
- so identity toggles (`enabled: undefined <-> true`, `runKey: undefined <->
95
- null`) no longer dispose and recreate a live controller.
96
- - Picks up `novoagents` 0.4.0-alpha.1 controller fixes: fatal (non-retried)
97
- initial-connect errors, per-drop `transportError` publication, a final
98
- `disposed` snapshot published before listeners detach, and interaction-race
99
- hardening.
100
-
101
- ## 0.2.0-alpha.0 - 2026-07-14
102
-
103
- - Replaced the alpha store/transport hooks with effect-owned bindings over the
104
- canonical `NovoRunController`.
105
- - Added `useNovoRunSelector` and controller-bound `useNovoInteractions`.
106
- - Fixed run-key/unmount leaks with stop + disposal and generation-fenced core
107
- consumption.
108
- - Added client entry directives, `sideEffects: false`, React 18/19 peer range,
109
- SSR snapshots, and Strict Mode lifecycle coverage.
7
+ - Retain nonblocking questions across run terminals and later runs. Questions
8
+ can be answered while the agent works; approval gates still pause execution.
9
+
10
+ ## 0.3.0-alpha.49 - 2026-09-16
11
+
12
+ Companion release for `novoagents` 0.5.0-alpha.45. The exact peer carries
13
+ named skill resources, the generated context contract, and optional screenshot
14
+ export status. React runtime and stream revision 21 are unchanged.
15
+
16
+ ## 0.3.0-alpha.48 - 2026-09-16
17
+
18
+ Companion release for `novoagents` 0.5.0-alpha.44. The exact peer includes
19
+ thread-scoped browser inventory, stable live-view identity, and screenshot
20
+ evidence status. The React runtime and stream revision 21 are unchanged.
21
+
22
+ ## 0.3.0-alpha.47 - 2026-09-15
23
+
24
+ Companion release for `novoagents` 0.5.0-alpha.43 and stream revision 21. The
25
+ exact peer preserves optional text purpose in browser state while keeping run
26
+ completion authoritative. The React hook runtime is unchanged.
27
+
28
+ ## 0.3.0-alpha.46 - 2026-09-15
29
+
30
+ Companion release for `novoagents` 0.5.0-alpha.42. The exact peer includes
31
+ thread admission epochs, explicit Stop/resume controls and attempt-only
32
+ maintenance cancellation. The React runtime is unchanged.
33
+
34
+ ## 0.3.0-alpha.45 - 2026-09-15
35
+
36
+ Companion release for `novoagents` 0.5.0-alpha.41 and stream revision 20. The
37
+ exact peer includes worker cancellation state and updated orchestration
38
+ contracts. The React runtime is unchanged.
39
+
40
+ ## 0.3.0-alpha.44 - 2026-09-14
41
+
42
+ Companion release for `novoagents` 0.5.0-alpha.40 and the worker/advisor tool
43
+ catalog. The React runtime is unchanged.
44
+
45
+ ## 0.3.0-alpha.43 - 2026-09-09
46
+
47
+ Companion release for `novoagents` 0.5.0-alpha.39 and stream revision 19. The React runtime is unchanged.
48
+
49
+ ## 0.3.0-alpha.42 - 2026-09-09
50
+
51
+ Companion release for `novoagents` 0.5.0-alpha.38. The exact peer carries context
52
+ audiences, thread credential attachments, execution authorization types, the
53
+ current error catalog, and updated managed-media documentation. The React runtime
54
+ is unchanged. Packed-consumer tests cover the SDK types and HTTP attachment
55
+ contract alongside React 18 compatibility.
56
+
57
+ ## 0.3.0-alpha.41 - 2026-09-07
58
+
59
+ Companion release for `novoagents` 0.5.0-alpha.37. The exact peer carries the
60
+ managed operation blocking contract and blockable-tool catalog. The React
61
+ runtime is unchanged.
62
+
63
+ ## 0.3.0-alpha.40 - 2026-09-06
64
+
65
+ Companion release for `novoagents` 0.5.0-alpha.36. The React runtime is
66
+ unchanged; the exact peer carries the corrected public type and error exports.
67
+ Packed-consumer verification now runs from a staged copy and exercises the
68
+ workspace peer resolver independently.
69
+
70
+ ## 0.3.0-alpha.39 - 2026-09-06
71
+
72
+ Companion release for `novoagents` 0.5.0-alpha.35. The React runtime is
73
+ unchanged; the exact peer carries the combined private-input and managed-web
74
+ usage contracts.
75
+
76
+ ## 0.3.0-alpha.38 - 2026-09-06
77
+
78
+ Companion release for `novoagents` 0.5.0-alpha.34. The React runtime is
79
+ unchanged; the exact peer carries protected browser input and context revision
80
+ contracts.
81
+
82
+ ## 0.3.0-alpha.37 - 2026-09-03
83
+
84
+ Companion release for `novoagents` 0.5.0-alpha.33. The React runtime is
85
+ unchanged; the exact peer carries the new agent configuration warnings type.
86
+
87
+ ## 0.3.0-alpha.36 - 2026-09-01
88
+
89
+ Companion release for `novoagents` 0.5.0-alpha.32.
90
+
91
+ - Controller ownership now rides the external-store snapshot (run key and
92
+ controller travel with the subscribed state), so an abandoned concurrent
93
+ render can no longer strand ownership in a render-written ref. Selector
94
+ readers isolate speculative renders while preserving equal object identity
95
+ across inline selector changes: the committed selection lives in a
96
+ `useState`-held cell that is still read during render, by design, mirroring
97
+ React's own `useSyncExternalStoreWithSelector` shim. Behavior is unchanged
98
+ across React 18 and 19, and the package now passes the canonical hooks lint
99
+ rules.
100
+
101
+ ## 0.3.0-alpha.35 - 2026-08-26
102
+
103
+ - Companion release for `novoagents` 0.5.0-alpha.31. GitHub connection and
104
+ agent repository grants now carry exact immutable repository identities
105
+ end to end; an explicit empty agent selection grants no repositories.
106
+
107
+ ## 0.3.0-alpha.34 - 2026-08-26
108
+
109
+ - Companion release for `novoagents` 0.5.0-alpha.30. The React runtime is
110
+ unchanged; the exact peer carries the immutable repository id on grantable
111
+ GitHub repositories.
112
+
113
+ ## 0.3.0-alpha.33 - 2026-08-26
114
+
115
+ - Companion release for `novoagents` 0.5.0-alpha.29. The React runtime is
116
+ unchanged; the exact peer carries the discriminated usage-lineage and
117
+ debit-group identity contract.
118
+
119
+ ## 0.3.0-alpha.32 - 2026-08-25
120
+
121
+ - Companion release for `novoagents` 0.5.0-alpha.28. It carries the exact peer
122
+ for scheduled wake-ups, stream protocol 18, and customer wake-up
123
+ cancellation. The React runtime is unchanged.
124
+
125
+ ## 0.3.0-alpha.31 - 2026-08-21
126
+
127
+ - Companion release for `novoagents` 0.5.0-alpha.27. The controller can follow
128
+ ordered thread segments with an injected cursor codec; React state exposes
129
+ the segment ledger while preserving the transcript across engine-owned runs.
130
+
131
+ ## 0.3.0-alpha.27 - 2026-08-20
132
+
133
+ - Companion release for `novoagents` 0.5.0-alpha.25. The React runtime is
134
+ unchanged; the exact peer carries autonomous run starts and all-attempt Stop
135
+ semantics for background subagents.
136
+
137
+ ## 0.3.0-alpha.26 - 2026-08-20
138
+
139
+ - Companion release for `novoagents` 0.5.0-alpha.24. It carries the exact peer
140
+ for wire protocol 16, the shared GitHub installation contract, and the
141
+ `ask_question` lifecycle whose first public terminal is the user's real
142
+ resolution.
143
+
144
+ ## 0.3.0-alpha.25 - 2026-08-20
145
+
146
+ - Companion release for `novoagents` 0.5.0-alpha.23. It carries the exact peer
147
+ required for stream schema 14, including background worker task metadata and
148
+ `data-novo-worker-report` message parts.
149
+
150
+ ## 0.3.0-alpha.24 - 2026-08-18
151
+
152
+ - Companion release for `novoagents` 0.5.0-alpha.22. The hook runtime is
153
+ unchanged; the package carries an exact peer on the SDK version whose browser
154
+ run controller it binds. Consumers rendering `run.messages` now see one
155
+ `data-novo-heartbeat` part per tool wait (see the core changelog).
156
+
157
+ ## 0.3.0-alpha.23 - 2026-08-17
158
+
159
+ - Companion release for `novoagents` 0.5.0-alpha.21. The hook runtime is
160
+ unchanged; the package carries an exact peer on the SDK version whose browser
161
+ run controller it binds.
162
+
163
+ ## 0.3.0-alpha.22 - 2026-08-17
164
+
165
+ - Companion release for `novoagents` 0.5.0-alpha.20. The hook runtime is
166
+ unchanged; the package carries an exact peer on the SDK version whose browser
167
+ run controller it binds.
168
+
169
+ ## 0.3.0-alpha.21 - 2026-08-17
170
+
171
+ - Companion release for `novoagents` 0.5.0-alpha.19. The hook runtime is
172
+ unchanged; the package carries an exact peer on the SDK version whose browser
173
+ run controller it binds.
174
+
175
+ ## 0.3.0-alpha.20 - 2026-08-16
176
+
177
+ - Companion release for `novoagents` 0.5.0-alpha.18. The hook runtime is
178
+ unchanged; the package carries an exact peer on the SDK version whose browser
179
+ run controller it binds.
180
+ - `0.3.0-alpha.19` was published through npm instead of pnpm and therefore
181
+ preserved the workspace protocol in its peer metadata. It was never tagged
182
+ `latest`; use this build instead.
183
+
184
+ ## 0.3.0-alpha.18 - 2026-08-12
185
+
186
+ - Companion release for `novoagents` 0.5.0-alpha.17. The hook runtime is
187
+ unchanged; the package carries an exact peer on the SDK version whose browser
188
+ run controller it binds.
189
+
190
+ ## 0.3.0-alpha.17 - 2026-08-12
191
+
192
+ - Companion release for `novoagents` 0.5.0-alpha.16. The hook runtime is
193
+ unchanged; the package carries an exact peer on the SDK version whose browser
194
+ run controller it binds.
195
+
196
+ ## 0.3.0-alpha.16 - 2026-08-12
197
+
198
+ - Companion release for `novoagents` 0.5.0-alpha.15. The hook runtime is
199
+ unchanged; the package carries an exact peer on the SDK version whose browser
200
+ run controller it binds.
201
+
202
+ Earlier prerelease builds are unsupported. Their immutable package artifacts
203
+ and Git history retain their release records.
package/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2026 Robert Pinckney. All rights reserved.
2
+
3
+ This software and associated documentation files (the "Software") are
4
+ proprietary and confidential. No license, express or implied, is granted
5
+ to any party to use, copy, modify, merge, publish, distribute, sublicense,
6
+ or sell copies of the Software without the prior written permission of
7
+ the copyright holder.
8
+
9
+ Unauthorized use, reproduction, or distribution of the Software, or any
10
+ portion of it, is strictly prohibited and may result in civil and criminal
11
+ penalties.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
17
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
18
+ OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @novoagents/react
2
2
 
3
+ > Developer access is currently closed. Novo Agents powers the agents in Novo North. This package is maintained for approved integrations; installing it does not grant service access.
4
+
3
5
  Headless React bindings for the canonical browser run controller in
4
6
  `novoagents/browser`. The package renders nothing, ships no CSS, and never
5
7
  accepts a Novo API key. Browser requests go only to your authenticated proxy
@@ -49,25 +51,9 @@ configure `interactionDiscovery: { list, intervalMs? }` on the controller.
49
51
  Discovery is generation-fenced, runs while the SSE connection is idle, and
50
52
  reconciles DB-only requests into the same canonical pending queue.
51
53
 
52
- ## Migrating from 0.1.0-alpha.0
53
-
54
- - `createNovoRunStore` → `createNovoRunController` from `novoagents/browser`.
55
- - `BrowserRunTransport` → `NovoRunEventSource`; use
56
- `createNovoHttpRunSource` for fetch/SSE routes.
57
- - `useNovoRun({ transport })` →
58
- `useNovoRun({ createController, runKey })`.
59
- - `useNovoInteractions({ endpoint, run })` → configure the controller's
60
- interaction transport, then call `useNovoInteractions(run.controller)`.
61
- - `resolveNovoInteractions` → `createHttpInteractionResolveTransport`.
62
-
63
- There are no compatibility aliases for the alpha API.
64
-
65
- ## Publishing
66
-
67
- After publishing `novoagents`, publish with
68
- `pnpm publish --access public --tag alpha`: pnpm rewrites the `workspace:*`
69
- peer dependency on `novoagents` to the exact packed version. Publish the two
70
- alpha packages in lockstep so both `alpha` tags always form one compatible
71
- browser-controller installation. Do not publish the workspace source with
72
- `npm publish`; npm does not perform pnpm's workspace-protocol rewrite. Verify
73
- the registry peer metadata before moving `latest`.
54
+ Questions with `blocking: false` stay in the pending queue across run terminals
55
+ and new runs. An explicit Stop cancels them. They do not change the run status or require a paused controller
56
+ to resolve. Approval gates remain blocking. Use a thread-follow source and
57
+ controller for this ongoing conversation, and scope discovery to the thread so
58
+ questions from older runs remain available. The example above follows one finite
59
+ run only.
@@ -1,11 +1,12 @@
1
1
  'use client';
2
- import { useEffect, useMemo, useRef, useSyncExternalStore } from 'react';
2
+ import { useEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react';
3
3
  export const NOVO_IDLE_RUN_STATE = Object.freeze({
4
4
  messages: Object.freeze([]),
5
5
  status: 'queued',
6
6
  progress: null,
7
7
  pendingInteractions: Object.freeze([]),
8
8
  tasks: Object.freeze([]),
9
+ segments: Object.freeze([]),
9
10
  steps: Object.freeze([]),
10
11
  browserSessions: Object.freeze([]),
11
12
  warnings: Object.freeze([]),
@@ -21,7 +22,17 @@ function createControllerSlot() {
21
22
  const listeners = new Set();
22
23
  let controller;
23
24
  let unsubscribe;
25
+ let snapshot = {
26
+ controller: undefined,
27
+ runKey: UNOWNED_RUN_KEY,
28
+ state: NOVO_IDLE_RUN_STATE,
29
+ };
24
30
  const publish = () => {
31
+ snapshot = {
32
+ ...snapshot,
33
+ state: controller?.getSnapshot() ??
34
+ NOVO_IDLE_RUN_STATE,
35
+ };
25
36
  for (const listener of listeners)
26
37
  listener();
27
38
  };
@@ -30,15 +41,20 @@ function createControllerSlot() {
30
41
  listeners.add(listener);
31
42
  return () => listeners.delete(listener);
32
43
  },
33
- getSnapshot: () => controller?.getSnapshot() ??
34
- NOVO_IDLE_RUN_STATE,
44
+ getSnapshot: () => snapshot,
35
45
  getController: () => controller,
36
- setController(next) {
37
- if (controller === next)
46
+ setController(next, runKey = UNOWNED_RUN_KEY) {
47
+ if (controller === next && snapshot.runKey === runKey)
38
48
  return;
39
49
  unsubscribe?.();
40
50
  controller = next;
41
51
  unsubscribe = next?.subscribe(publish);
52
+ snapshot = {
53
+ controller: next,
54
+ runKey,
55
+ state: next?.getSnapshot() ??
56
+ NOVO_IDLE_RUN_STATE,
57
+ };
42
58
  publish();
43
59
  },
44
60
  };
@@ -50,11 +66,10 @@ function createControllerSlot() {
50
66
  */
51
67
  export function useNovoRun(options) {
52
68
  const factoryRef = useRef(options.createController);
53
- factoryRef.current = options.createController;
54
- const slotRef = useRef(undefined);
55
- slotRef.current ??= createControllerSlot();
56
- const slot = slotRef.current;
57
- const ownedRunKeyRef = useRef(UNOWNED_RUN_KEY);
69
+ useEffect(() => {
70
+ factoryRef.current = options.createController;
71
+ }, [options.createController]);
72
+ const [slot] = useState(() => createControllerSlot());
58
73
  // Normalize before any dependency array: `enabled: undefined <-> true` and
59
74
  // `runKey: undefined <-> null` toggles are identity changes and must not
60
75
  // dispose and recreate a live controller.
@@ -66,14 +81,10 @@ export function useNovoRun(options) {
66
81
  return;
67
82
  }
68
83
  const controller = factoryRef.current();
69
- ownedRunKeyRef.current = runKey;
70
- slot.setController(controller);
84
+ slot.setController(controller, runKey);
71
85
  return () => {
72
86
  controller.stop();
73
87
  controller.dispose();
74
- if (ownedRunKeyRef.current === runKey) {
75
- ownedRunKeyRef.current = UNOWNED_RUN_KEY;
76
- }
77
88
  if (slot.getController() === controller)
78
89
  slot.setController(undefined);
79
90
  };
@@ -81,16 +92,16 @@ export function useNovoRun(options) {
81
92
  useEffect(() => {
82
93
  if (enabled &&
83
94
  options.autoStart !== false &&
84
- ownedRunKeyRef.current === runKey) {
95
+ slot.getSnapshot().runKey === runKey) {
85
96
  void slot.getController()?.start();
86
97
  }
87
98
  }, [options.autoStart, enabled, runKey, slot]);
88
- const observedState = useSyncExternalStore(slot.subscribe, slot.getSnapshot, () => NOVO_IDLE_RUN_STATE);
89
- const ownsRenderedKey = enabled && ownedRunKeyRef.current === runKey;
99
+ const observed = useSyncExternalStore(slot.subscribe, slot.getSnapshot, () => IDLE_SLOT_SNAPSHOT);
100
+ const ownsRenderedKey = enabled && observed.runKey === runKey;
90
101
  const state = ownsRenderedKey
91
- ? observedState
102
+ ? observed.state
92
103
  : NOVO_IDLE_RUN_STATE;
93
- const controller = ownsRenderedKey ? slot.getController() : undefined;
104
+ const controller = ownsRenderedKey ? observed.controller : undefined;
94
105
  return useMemo(() => ({
95
106
  ...state,
96
107
  controller,
@@ -104,3 +115,8 @@ export function useNovoRun(options) {
104
115
  }), [controller, state]);
105
116
  }
106
117
  const UNOWNED_RUN_KEY = Symbol('unowned Novo run key');
118
+ const IDLE_SLOT_SNAPSHOT = Object.freeze({
119
+ controller: undefined,
120
+ runKey: UNOWNED_RUN_KEY,
121
+ state: NOVO_IDLE_RUN_STATE,
122
+ });
@@ -1,47 +1,67 @@
1
1
  'use client';
2
- import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore, } from 'react';
2
+ import { useCallback, useEffect, useMemo, useState, useSyncExternalStore, } from 'react';
3
3
  import { NOVO_IDLE_RUN_STATE } from './useNovoRun.js';
4
4
  /** Concurrent-safe selector binding with selector/equality identity support. */
5
5
  export function useNovoRunSelector(controller, selector, isEqual = Object.is) {
6
- const committedSelection = useRef({
7
- hasValue: false,
8
- });
9
- const idleState = NOVO_IDLE_RUN_STATE;
10
- const [getSnapshot, getServerSnapshot] = useMemo(() => {
11
- let hasMemo = false;
12
- let memoizedState;
13
- let memoizedSelection;
14
- const select = (state) => {
15
- if (!hasMemo) {
16
- hasMemo = true;
17
- memoizedState = state;
18
- const nextSelection = selector(state);
19
- const committed = committedSelection.current;
20
- memoizedSelection =
21
- committed.hasValue && isEqual(committed.value, nextSelection)
22
- ? committed.value
23
- : nextSelection;
24
- return memoizedSelection;
25
- }
26
- if (Object.is(memoizedState, state))
27
- return memoizedSelection;
28
- const nextSelection = selector(state);
29
- memoizedState = state;
30
- if (isEqual(memoizedSelection, nextSelection))
31
- return memoizedSelection;
32
- memoizedSelection = nextSelection;
33
- return memoizedSelection;
34
- };
35
- const serverSelection = select(idleState);
36
- return [
37
- () => select(controller?.getSnapshot() ?? idleState),
38
- () => serverSelection,
39
- ];
40
- }, [controller, isEqual, selector]);
6
+ const [committedSelection] = useState(() => createCommittedSelection());
7
+ const [getSnapshot, getServerSnapshot] = useMemo(() => createSelectionReader(controller, selector, isEqual, NOVO_IDLE_RUN_STATE, committedSelection), [committedSelection, controller, isEqual, selector]);
41
8
  const subscribe = useCallback((listener) => controller?.subscribe(listener) ?? (() => { }), [controller]);
42
9
  const selection = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
43
10
  useEffect(() => {
44
- committedSelection.current = { hasValue: true, value: selection };
45
- }, [selection]);
11
+ committedSelection.write(selection);
12
+ }, [committedSelection, selection]);
46
13
  return selection;
47
14
  }
15
+ // Deliberately a mutable cell read during render (via getSnapshot) and
16
+ // written from the commit effect, mirroring React's own
17
+ // useSyncExternalStoreWithSelector shim. The hooks lint layer cannot see a
18
+ // closure-held cell; do not "fix" this back into a ref, which it would flag.
19
+ function createCommittedSelection() {
20
+ let committed = {
21
+ hasValue: false,
22
+ };
23
+ return {
24
+ read: () => committed,
25
+ write: (value) => {
26
+ committed = { hasValue: true, value };
27
+ },
28
+ };
29
+ }
30
+ function createSelectionReader(controller, selector, isEqual, idleState, committedSelection) {
31
+ let hasValue = false;
32
+ let value;
33
+ let memoizedState;
34
+ let hasServerValue = false;
35
+ let serverValue;
36
+ const select = (state) => {
37
+ if (Object.is(memoizedState, state) && hasValue)
38
+ return value;
39
+ const next = selector(state);
40
+ memoizedState = state;
41
+ if (!hasValue) {
42
+ hasValue = true;
43
+ const committed = committedSelection.read();
44
+ value =
45
+ committed.hasValue && isEqual(committed.value, next)
46
+ ? committed.value
47
+ : next;
48
+ }
49
+ else if (!isEqual(value, next)) {
50
+ value = next;
51
+ }
52
+ return value;
53
+ };
54
+ const selectServer = () => {
55
+ if (hasServerValue)
56
+ return serverValue;
57
+ const next = selector(idleState);
58
+ const committed = committedSelection.read();
59
+ serverValue =
60
+ committed.hasValue && isEqual(committed.value, next)
61
+ ? committed.value
62
+ : next;
63
+ hasServerValue = true;
64
+ return serverValue;
65
+ };
66
+ return [() => select(controller?.getSnapshot() ?? idleState), selectServer];
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@novoagents/react",
3
- "version": "0.3.0-alpha.5",
3
+ "version": "0.3.0-alpha.51",
4
4
  "description": "Browser-safe React hooks for Novo Agents streams and interactions.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -22,27 +22,26 @@
22
22
  "publishConfig": {
23
23
  "access": "public"
24
24
  },
25
+ "peerDependencies": {
26
+ "novoagents": "0.5.0-alpha.47",
27
+ "react": "^18.0.0 || ^19.0.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/react": "19.2.18",
31
+ "@types/react-dom": "19.2.7",
32
+ "happy-dom": "20.14.0",
33
+ "novoagents": "0.5.0-alpha.47",
34
+ "react": "19.2.8",
35
+ "react-dom": "19.2.8",
36
+ "typescript": "6.0.3",
37
+ "vite": "8.2.2",
38
+ "vitest": "5.0.0"
39
+ },
25
40
  "scripts": {
26
41
  "build": "tsc -p tsconfig.json && node scripts/check-dist.mjs",
27
42
  "clean": "rm -rf dist",
28
43
  "test": "vitest run && pnpm run test:react18",
29
44
  "test:react18": "node scripts/test-react18.mjs",
30
- "typecheck": "tsc -p tsconfig.json --noEmit",
31
- "prepack": "pnpm run clean && pnpm run build"
32
- },
33
- "peerDependencies": {
34
- "novoagents": "workspace:*",
35
- "react": "^18.0.0 || ^19.0.0"
36
- },
37
- "devDependencies": {
38
- "@types/react": "19.2.17",
39
- "@types/react-dom": "19.2.3",
40
- "happy-dom": "20.11.1",
41
- "novoagents": "workspace:*",
42
- "react": "19.2.7",
43
- "react-dom": "19.2.7",
44
- "typescript": "catalog:",
45
- "vite": "catalog:",
46
- "vitest": "catalog:"
45
+ "typecheck": "tsc -p tsconfig.json --noEmit"
47
46
  }
48
- }
47
+ }