@hachej/boring-ui-kit 0.4.49 → 0.4.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/THIRD_PARTY_NOTICES.md +208 -0
- package/dist/chat-react-controller.d.ts +4 -1
- package/dist/chat-react-controller.js +4 -0
- package/dist/chat-react-remote.d.ts +16 -1
- package/dist/chat-react-remote.js +69 -10
- package/dist/chat-react-session.d.ts +2 -3
- package/dist/chat-react-session.js +38 -4
- package/dist/drawio-viewer.d.ts +4 -0
- package/dist/drawio-viewer.js +6 -0
- package/dist/native-chat.d.ts +8 -1
- package/dist/native-chat.js +45 -8
- package/dist/remote-chat.js +51 -5
- package/dist/vendor/drawio-viewer-static.d.ts +3 -0
- package/dist/vendor/drawio-viewer-static.js +5 -0
- package/package.json +6 -2
- package/r/agent-target.json +4 -4
- package/r/app-shell.json +9 -9
- package/r/board-viewer.json +6 -6
- package/r/button.json +1 -1
- package/r/dock.json +1 -1
- package/r/documents.json +12 -12
- package/r/drawing-viewer.json +57 -0
- package/r/feedback.json +6 -6
- package/r/file-tree.json +4 -4
- package/r/highlights.json +3 -3
- package/r/html-viewer.json +3 -3
- package/r/looks-viewer.json +2 -2
- package/r/maquette-viewer.json +1 -1
- package/r/markdown-editor.json +3 -3
- package/r/mockup-viewer.json +1 -1
- package/r/pi-ambient.json +8 -8
- package/r/pi-app-kinds.json +10 -10
- package/r/pi-app.json +11 -11
- package/r/pi-artifacts.json +2 -2
- package/r/pi-chat-approval.json +2 -2
- package/r/pi-chat-attachments.json +1 -1
- package/r/pi-chat-markdown.json +2 -2
- package/r/pi-chat-mentions.json +1 -1
- package/r/pi-chat-pickers.json +1 -1
- package/r/pi-chat-plugins.json +11 -11
- package/r/pi-chat-question.json +1 -1
- package/r/pi-chat-slash.json +2 -2
- package/r/pi-chat-subagent.json +1 -1
- package/r/pi-chat-voice.json +3 -3
- package/r/pi-chat.json +8 -8
- package/r/pi-workspace.json +3 -3
- package/r/placed-agent.json +4 -4
- package/r/provider-setup.json +1 -1
- package/r/registry.json +210 -161
- package/r/shared-chat.json +6 -6
- package/r/spaces.json +1 -1
- package/r/task-list-viewer.json +2 -2
- package/r/version-history.json +2 -2
- package/r/viewer-code.json +2 -2
- package/r/viewer-frame.json +4 -4
- package/r/viewer-html.json +4 -4
- package/r/viewer-image.json +2 -2
- package/r/viewer-markdown.json +5 -5
- package/r/viewer-pdf.json +3 -3
- package/r/viewers.json +6 -6
- package/r/windows.json +1 -1
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -118,3 +118,211 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
|
118
118
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
119
119
|
SOFTWARE.
|
|
120
120
|
```
|
|
121
|
+
|
|
122
|
+
## draw.io viewer (`@hachej/boring-ui-kit/drawio-viewer`)
|
|
123
|
+
|
|
124
|
+
The kit carries, unmodified, `viewer-static.min.js` of jgraph/drawio v32.4.1 (https://github.com/jgraph/drawio, sha256 bc7586cb62eec98018c13b78619078d3a7179cc7bfc711c566dda19527520a30), Copyright JGraph Ltd, as the text of `src/vendor/drawio-viewer-static.ts`. The project has no NOTICE file. The script bundles DOMPurify (Apache-2.0 or MPL-2.0), pako (MIT and Zlib) and other libraries whose notices it keeps inline. Its licence:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
Apache License
|
|
128
|
+
Version 2.0, January 2004
|
|
129
|
+
http://www.apache.org/licenses/
|
|
130
|
+
|
|
131
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
132
|
+
|
|
133
|
+
1. Definitions.
|
|
134
|
+
|
|
135
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
136
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
137
|
+
|
|
138
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
139
|
+
the copyright owner that is granting the License.
|
|
140
|
+
|
|
141
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
142
|
+
other entities that control, are controlled by, or are under common
|
|
143
|
+
control with that entity. For the purposes of this definition,
|
|
144
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
145
|
+
direction or management of such entity, whether by contract or
|
|
146
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
147
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
148
|
+
|
|
149
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
150
|
+
exercising permissions granted by this License.
|
|
151
|
+
|
|
152
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
153
|
+
including but not limited to software source code, documentation
|
|
154
|
+
source, and configuration files.
|
|
155
|
+
|
|
156
|
+
"Object" form shall mean any form resulting from mechanical
|
|
157
|
+
transformation or translation of a Source form, including but
|
|
158
|
+
not limited to compiled object code, generated documentation,
|
|
159
|
+
and conversions to other media types.
|
|
160
|
+
|
|
161
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
162
|
+
Object form, made available under the License, as indicated by a
|
|
163
|
+
copyright notice that is included in or attached to the work
|
|
164
|
+
(an example is provided in the Appendix below).
|
|
165
|
+
|
|
166
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
167
|
+
form, that is based on (or derived from) the Work and for which the
|
|
168
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
169
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
170
|
+
of this License, Derivative Works shall not include works that remain
|
|
171
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
172
|
+
the Work and Derivative Works thereof.
|
|
173
|
+
|
|
174
|
+
"Contribution" shall mean any work of authorship, including
|
|
175
|
+
the original version of the Work and any modifications or additions
|
|
176
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
177
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
178
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
179
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
180
|
+
means any form of electronic, verbal, or written communication sent
|
|
181
|
+
to the Licensor or its representatives, including but not limited to
|
|
182
|
+
communication on electronic mailing lists, source code control systems,
|
|
183
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
184
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
185
|
+
excluding communication that is conspicuously marked or otherwise
|
|
186
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
187
|
+
|
|
188
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
189
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
190
|
+
subsequently incorporated within the Work.
|
|
191
|
+
|
|
192
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
193
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
194
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
195
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
196
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
197
|
+
Work and such Derivative Works in Source or Object form.
|
|
198
|
+
|
|
199
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
200
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
201
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
202
|
+
(except as stated in this section) patent license to make, have made,
|
|
203
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
204
|
+
where such license applies only to those patent claims licensable
|
|
205
|
+
by such Contributor that are necessarily infringed by their
|
|
206
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
207
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
208
|
+
institute patent litigation against any entity (including a
|
|
209
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
210
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
211
|
+
or contributory patent infringement, then any patent licenses
|
|
212
|
+
granted to You under this License for that Work shall terminate
|
|
213
|
+
as of the date such litigation is filed.
|
|
214
|
+
|
|
215
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
216
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
217
|
+
modifications, and in Source or Object form, provided that You
|
|
218
|
+
meet the following conditions:
|
|
219
|
+
|
|
220
|
+
(a) You must give any other recipients of the Work or
|
|
221
|
+
Derivative Works a copy of this License; and
|
|
222
|
+
|
|
223
|
+
(b) You must cause any modified files to carry prominent notices
|
|
224
|
+
stating that You changed the files; and
|
|
225
|
+
|
|
226
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
227
|
+
that You distribute, all copyright, patent, trademark, and
|
|
228
|
+
attribution notices from the Source form of the Work,
|
|
229
|
+
excluding those notices that do not pertain to any part of
|
|
230
|
+
the Derivative Works; and
|
|
231
|
+
|
|
232
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
233
|
+
distribution, then any Derivative Works that You distribute must
|
|
234
|
+
include a readable copy of the attribution notices contained
|
|
235
|
+
within such NOTICE file, excluding those notices that do not
|
|
236
|
+
pertain to any part of the Derivative Works, in at least one
|
|
237
|
+
of the following places: within a NOTICE text file distributed
|
|
238
|
+
as part of the Derivative Works; within the Source form or
|
|
239
|
+
documentation, if provided along with the Derivative Works; or,
|
|
240
|
+
within a display generated by the Derivative Works, if and
|
|
241
|
+
wherever such third-party notices normally appear. The contents
|
|
242
|
+
of the NOTICE file are for informational purposes only and
|
|
243
|
+
do not modify the License. You may add Your own attribution
|
|
244
|
+
notices within Derivative Works that You distribute, alongside
|
|
245
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
246
|
+
that such additional attribution notices cannot be construed
|
|
247
|
+
as modifying the License.
|
|
248
|
+
|
|
249
|
+
You may add Your own copyright statement to Your modifications and
|
|
250
|
+
may provide additional or different license terms and conditions
|
|
251
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
252
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
253
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
254
|
+
the conditions stated in this License.
|
|
255
|
+
|
|
256
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
257
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
258
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
259
|
+
this License, without any additional terms or conditions.
|
|
260
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
261
|
+
the terms of any separate license agreement you may have executed
|
|
262
|
+
with Licensor regarding such Contributions.
|
|
263
|
+
|
|
264
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
265
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
266
|
+
except as required for reasonable and customary use in describing the
|
|
267
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
268
|
+
|
|
269
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
270
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
271
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
272
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
273
|
+
implied, including, without limitation, any warranties or conditions
|
|
274
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
275
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
276
|
+
appropriateness of using or redistributing the Work and assume any
|
|
277
|
+
risks associated with Your exercise of permissions under this License.
|
|
278
|
+
|
|
279
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
280
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
281
|
+
unless required by applicable law (such as deliberate and grossly
|
|
282
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
283
|
+
liable to You for damages, including any direct, indirect, special,
|
|
284
|
+
incidental, or consequential damages of any character arising as a
|
|
285
|
+
result of this License or out of the use or inability to use the
|
|
286
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
287
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
288
|
+
other commercial damages or losses), even if such Contributor
|
|
289
|
+
has been advised of the possibility of such damages.
|
|
290
|
+
|
|
291
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
292
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
293
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
294
|
+
or other liability obligations and/or rights consistent with this
|
|
295
|
+
License. However, in accepting such obligations, You may act only
|
|
296
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
297
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
298
|
+
defend, and hold each Contributor harmless for any liability
|
|
299
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
300
|
+
of your accepting any such warranty or additional liability.
|
|
301
|
+
|
|
302
|
+
END OF TERMS AND CONDITIONS
|
|
303
|
+
|
|
304
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
305
|
+
|
|
306
|
+
To apply the Apache License to your work, attach the following
|
|
307
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
308
|
+
replaced with your own identifying information. (Don't include
|
|
309
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
310
|
+
comment syntax for the file format. We also recommend that a
|
|
311
|
+
file or class name and description of purpose be included on the
|
|
312
|
+
same "printed page" as the copyright notice for easier
|
|
313
|
+
identification within third-party archives.
|
|
314
|
+
|
|
315
|
+
Copyright [yyyy] [name of copyright owner]
|
|
316
|
+
|
|
317
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
318
|
+
you may not use this file except in compliance with the License.
|
|
319
|
+
You may obtain a copy of the License at
|
|
320
|
+
|
|
321
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
322
|
+
|
|
323
|
+
Unless required by applicable law or agreed to in writing, software
|
|
324
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
325
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
326
|
+
See the License for the specific language governing permissions and
|
|
327
|
+
limitations under the License.
|
|
328
|
+
```
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
export type ConversationControllerState<C> =
|
|
1
|
+
export type ConversationControllerState<C> =
|
|
2
|
+
/** `controller`: created, its `connect` still running (absent before creation). Hosts that wait for `ready` can ignore it. */
|
|
3
|
+
{
|
|
2
4
|
readonly status: 'connecting';
|
|
5
|
+
readonly controller?: C;
|
|
3
6
|
} | {
|
|
4
7
|
readonly status: 'offline';
|
|
5
8
|
readonly error: unknown;
|
|
@@ -34,6 +34,7 @@ export function useControllerPerConversation(options) {
|
|
|
34
34
|
await retire();
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
|
+
publish({ status: 'connecting', controller: owned });
|
|
37
38
|
await connect?.(owned);
|
|
38
39
|
if (off) {
|
|
39
40
|
await retire();
|
|
@@ -43,6 +44,9 @@ export function useControllerPerConversation(options) {
|
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
catch (error) {
|
|
47
|
+
// A controller handed out while connecting is withdrawn before it is disposed, so no host holds a disposed one.
|
|
48
|
+
if (owned)
|
|
49
|
+
publish({ status: 'connecting' });
|
|
46
50
|
try {
|
|
47
51
|
await retire();
|
|
48
52
|
}
|
|
@@ -4,18 +4,32 @@ export type ChatPlacement = 'docked' | 'floating';
|
|
|
4
4
|
/** Reports the placement of the chat showing `controller` while the caller is mounted. */
|
|
5
5
|
export declare function useChatPlacement(controller: ChatController, placement: ChatPlacement): void;
|
|
6
6
|
export declare function readChatPlacement(controller: ChatController | undefined): ChatPlacement | undefined;
|
|
7
|
+
type RemoteActions = Partial<Pick<RemoteChat, 'answer' | 'withdraw'>>;
|
|
8
|
+
/**
|
|
9
|
+
* `connecting` carries the controller as soon as it exists (D-1434, before the host's first view lands): its composer takes messages at
|
|
10
|
+
* once, shows them as the person's own and sends them, in order, once connected; the transcript appears with the first view. `actions` and
|
|
11
|
+
* `configure` wait for the connection. `ready` still means the first view landed, with the same controller; a host that waits for `ready`
|
|
12
|
+
* (or ignores `controller` while connecting) behaves as before. If connecting fails the state turns `offline` without a controller, and
|
|
13
|
+
* the messages not sent come back in the next attempt's composer.
|
|
14
|
+
*/
|
|
7
15
|
export type RemoteChatState = {
|
|
8
16
|
readonly status: 'connecting';
|
|
9
17
|
readonly conversationId: string | undefined;
|
|
18
|
+
readonly controller?: NativeChatController;
|
|
19
|
+
readonly actions?: RemoteActions;
|
|
20
|
+
readonly configure?: RemoteChat['configure'];
|
|
10
21
|
} | {
|
|
11
22
|
readonly status: 'offline';
|
|
12
23
|
readonly conversationId: string | undefined;
|
|
13
24
|
readonly error: string;
|
|
25
|
+
readonly controller?: undefined;
|
|
26
|
+
readonly actions?: undefined;
|
|
27
|
+
readonly configure?: undefined;
|
|
14
28
|
} | {
|
|
15
29
|
readonly status: 'ready';
|
|
16
30
|
readonly conversationId: string;
|
|
17
31
|
readonly controller: NativeChatController;
|
|
18
|
-
readonly actions:
|
|
32
|
+
readonly actions: RemoteActions;
|
|
19
33
|
readonly configure: RemoteChat['configure'];
|
|
20
34
|
};
|
|
21
35
|
export interface RemoteChatHookOptions {
|
|
@@ -30,4 +44,5 @@ export interface RemoteChatHookOptions {
|
|
|
30
44
|
}
|
|
31
45
|
/** One owned native controller per selection; the remote transport itself reopens dropped streams. Failed acquisition retries each second. */
|
|
32
46
|
export declare function useRemoteChat({ conversationId, endpoint, fetch, identity, requestContext, transport, webSocket }: RemoteChatHookOptions): RemoteChatState;
|
|
47
|
+
export {};
|
|
33
48
|
//# sourceMappingURL=chat-react-remote.d.ts.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useLayoutEffect, useMemo, useRef } from 'react';
|
|
3
|
+
import { BACKGROUND_CONTEXT } from '@earendil-works/chord/context';
|
|
3
4
|
import { createNativeChatController } from './native-chat.js';
|
|
4
5
|
import { createRemoteChat } from './remote-chat.js';
|
|
5
6
|
import { useControllerPerConversation } from './chat-react-controller.js';
|
|
@@ -24,6 +25,34 @@ export function useChatPlacement(controller, placement) {
|
|
|
24
25
|
export function readChatPlacement(controller) {
|
|
25
26
|
return controller ? placements.get(controller)?.at(-1)?.placement : undefined;
|
|
26
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The conversation and link the controller borrows while the remote chat opens. Each member waits for the remote and then forwards
|
|
30
|
+
* to it unchanged; the identity is the selection until the server's view names it (the controller reads it again on connect).
|
|
31
|
+
*/
|
|
32
|
+
function pendingConversation(pending, selection) {
|
|
33
|
+
const forward = (name) => (async (...args) => (await pending.remote).conversation[name](...args));
|
|
34
|
+
const conversation = {
|
|
35
|
+
get id() { return (pending.current?.conversation.id ?? selection); },
|
|
36
|
+
watch: forward('watch'), submit: forward('submit'), abort: forward('abort'), entries: forward('entries'),
|
|
37
|
+
commit: async (change, context) => (await pending.remote).conversation.commit(change, context),
|
|
38
|
+
};
|
|
39
|
+
const link = {
|
|
40
|
+
reconnecting: () => pending.current?.link.reconnecting() ?? false,
|
|
41
|
+
subscribe: listener => {
|
|
42
|
+
let off, gone = false;
|
|
43
|
+
void pending.remote.then(remote => { if (!gone)
|
|
44
|
+
off = remote.link.subscribe(listener); }, () => { });
|
|
45
|
+
return () => { gone = true; off?.(); };
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
return { conversation, link };
|
|
49
|
+
}
|
|
50
|
+
/** What a controller that never connected still holds (messages taken from the composer, then the draft), to give to the next attempt. */
|
|
51
|
+
function unsent(controller) {
|
|
52
|
+
const snapshot = controller.getSnapshot(), drafts = [...snapshot.echo.map(item => item.draft), snapshot.draft];
|
|
53
|
+
const attachments = drafts.flatMap(item => item.attachments).filter((item, index, all) => all.findIndex(other => other.id === item.id) === index);
|
|
54
|
+
return { text: drafts.map(item => item.text).filter(text => text.trim()).join('\n'), attachments };
|
|
55
|
+
}
|
|
27
56
|
/** One owned native controller per selection; the remote transport itself reopens dropped streams. Failed acquisition retries each second. */
|
|
28
57
|
export function useRemoteChat({ conversationId, endpoint, fetch, identity, requestContext, transport, webSocket }) {
|
|
29
58
|
const url = conversationId === undefined ? undefined : String(endpoint(conversationId));
|
|
@@ -32,30 +61,57 @@ export function useRemoteChat({ conversationId, endpoint, fetch, identity, reque
|
|
|
32
61
|
useLayoutEffect(() => { latest.current = { fetch, requestContext, webSocket }; });
|
|
33
62
|
// Transport ownership stays with the controller that acquired it, including when connect or selection changes race.
|
|
34
63
|
const remotes = useMemo(() => new WeakMap(), []);
|
|
64
|
+
// Messages a failed attempt never sent, by selection, for the next attempt's composer.
|
|
65
|
+
const carried = useMemo(() => new Map(), []);
|
|
35
66
|
const selected = useControllerPerConversation({ key,
|
|
36
|
-
|
|
67
|
+
// The controller exists at once; the remote chat opens behind it (D-1434). Its watch, submits and actions wait for the first view.
|
|
68
|
+
create: () => {
|
|
37
69
|
let controller;
|
|
38
|
-
const remote =
|
|
70
|
+
const remote = createRemoteChat({ endpoint: url, fetch: request => latest.current.fetch(request),
|
|
39
71
|
requestContext: () => {
|
|
40
72
|
const placement = readChatPlacement(controller), screen = latest.current.requestContext?.();
|
|
41
73
|
return placement || screen ? { ...screen, ...(placement && { placement }) } : undefined;
|
|
42
74
|
}, ...(transport ? { transport } : {}), ...(latest.current.webSocket ? { webSocket: latest.current.webSocket } : {}) });
|
|
75
|
+
const pending = { remote,
|
|
76
|
+
actions: { answer: async (...args) => (await remote).answer(...args), withdraw: async (...args) => (await remote).withdraw(...args) },
|
|
77
|
+
configure: async (...args) => (await remote).configure(...args) };
|
|
78
|
+
void remote.then(value => { pending.current = value; }, () => { });
|
|
43
79
|
try {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
80
|
+
const { conversation, link } = pendingConversation(pending, conversationId);
|
|
81
|
+
controller = createNativeChatController({ identity: identity, conversation, context: BACKGROUND_CONTEXT, link,
|
|
82
|
+
definitelyNotAdmitted: error => pending.current?.definitelyNotAdmitted(error) ?? false });
|
|
47
83
|
}
|
|
48
84
|
catch (error) {
|
|
49
|
-
|
|
85
|
+
void remote.then(value => value.close(), () => { });
|
|
50
86
|
throw error;
|
|
51
87
|
}
|
|
88
|
+
remotes.set(controller, pending);
|
|
89
|
+
const back = carried.get(String(key));
|
|
90
|
+
if (back) {
|
|
91
|
+
carried.delete(String(key));
|
|
92
|
+
if (back.text)
|
|
93
|
+
controller.setText(back.text);
|
|
94
|
+
if (back.attachments.length)
|
|
95
|
+
controller.setAttachments(back.attachments);
|
|
96
|
+
}
|
|
97
|
+
// `connecting` at once, so the chat shows it and takes messages; the native watch opening waits for the remote's first view.
|
|
98
|
+
void controller.connect();
|
|
99
|
+
return controller;
|
|
52
100
|
},
|
|
53
|
-
connect: controller => controller.connect(),
|
|
101
|
+
connect: async (controller) => { await remotes.get(controller).remote; await controller.connect(); },
|
|
54
102
|
dispose: async (controller) => {
|
|
103
|
+
const remote = remotes.get(controller);
|
|
104
|
+
// Disposed before its first view (a failed connect, or another selection): what it took was held, never submitted (see `send`). Its
|
|
105
|
+
// words come back in the composer of this selection's next controller, never sent by themselves.
|
|
106
|
+
if (!controller.getSnapshot().view) {
|
|
107
|
+
const back = unsent(controller);
|
|
108
|
+
if (back.text || back.attachments.length)
|
|
109
|
+
carried.set(String(key), back);
|
|
110
|
+
}
|
|
55
111
|
// Start both: closing a transport may release a pending native watch opening.
|
|
56
112
|
const pending = controller.dispose();
|
|
57
113
|
try {
|
|
58
|
-
await
|
|
114
|
+
await (remote?.current ?? await remote?.remote.catch(() => undefined))?.close();
|
|
59
115
|
}
|
|
60
116
|
finally {
|
|
61
117
|
await pending;
|
|
@@ -63,10 +119,13 @@ export function useRemoteChat({ conversationId, endpoint, fetch, identity, reque
|
|
|
63
119
|
}
|
|
64
120
|
}, });
|
|
65
121
|
if (selected.status === 'ready' && conversationId !== undefined) {
|
|
66
|
-
const remote = remotes.get(selected.controller);
|
|
122
|
+
const remote = remotes.get(selected.controller).current;
|
|
67
123
|
const actions = { ...(typeof remote.answer === 'function' ? { answer: remote.answer } : {}),
|
|
68
124
|
...(typeof remote.withdraw === 'function' ? { withdraw: remote.withdraw } : {}) };
|
|
69
125
|
return { status: 'ready', conversationId, controller: selected.controller, actions, configure: remote.configure };
|
|
70
126
|
}
|
|
71
|
-
|
|
127
|
+
if (selected.status === 'offline')
|
|
128
|
+
return { status: 'offline', conversationId, error: String(selected.error?.message ?? selected.error) };
|
|
129
|
+
const early = selected.controller && remotes.get(selected.controller);
|
|
130
|
+
return early && selected.controller ? { status: 'connecting', conversationId, controller: selected.controller, actions: early.actions, configure: early.configure } : { status: 'connecting', conversationId };
|
|
72
131
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ToolCall, ToolResultMessage } from '@earendil-works/pi-ai';
|
|
2
|
-
import type { ChatAttachment, ChatController } from './native-chat.js';
|
|
2
|
+
import type { ChatAttachment, ChatController, ChatSnapshot } from './native-chat.js';
|
|
3
3
|
import type { ChatCard, DeriveOptions, Mode, QueuedMessage, Row } from './chat-view.js';
|
|
4
4
|
/** A conversation's observed file; opening remains a host operation, not workspace ownership. */
|
|
5
5
|
export interface ConversationFile {
|
|
@@ -116,9 +116,8 @@ export interface ChatSessionOptions<Content = unknown> extends Partial<DeriveOpt
|
|
|
116
116
|
/** Focus is presentation; called only by actions, never during render. */
|
|
117
117
|
readonly onFocus?: (() => void) | undefined;
|
|
118
118
|
}
|
|
119
|
-
/** Headless session over the public native controller snapshot/commands. Borrows it; cleanup only aborts this hook's uploads. */
|
|
120
119
|
export declare function useChatSession<Content = unknown>(options: ChatSessionOptions<Content>): {
|
|
121
|
-
state:
|
|
120
|
+
state: ChatSnapshot;
|
|
122
121
|
derived: import("./chat-view.js").Derived<Content>;
|
|
123
122
|
rows: Row<Content>[];
|
|
124
123
|
queued: {
|
|
@@ -3,6 +3,30 @@ import { useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalS
|
|
|
3
3
|
import { derive, isFileBlock, object, queuedMessages, waitingForAnswer as waitsForAnswer } from './chat-view.js';
|
|
4
4
|
import { appendToDraft, displayName, kindMentionDraft, looksLikeMarkdown, mentionKind, mentionToken, pieces, prependToDraft, withoutKindMentions, writeKindMentionDraft } from './chat-text.js';
|
|
5
5
|
/** Headless session over the public native controller snapshot/commands. Borrows it; cleanup only aborts this hook's uploads. */
|
|
6
|
+
/**
|
|
7
|
+
* The snapshot once the controller has its first view (its conversation id is then the server's); undefined if it is disposed, fails or
|
|
8
|
+
* `signal` aborts first.
|
|
9
|
+
*/
|
|
10
|
+
function connectedSnapshot(controller, signal) {
|
|
11
|
+
return new Promise(resolve => {
|
|
12
|
+
let off = () => { }, done = false;
|
|
13
|
+
const finish = (value) => { if (done)
|
|
14
|
+
return; done = true; off(); signal.removeEventListener('abort', aborted); resolve(value); };
|
|
15
|
+
const aborted = () => finish(undefined);
|
|
16
|
+
const check = () => {
|
|
17
|
+
const snapshot = controller.getSnapshot(), kind = snapshot.connection.kind;
|
|
18
|
+
if (signal.aborted)
|
|
19
|
+
finish(undefined);
|
|
20
|
+
else if (snapshot.view || snapshot.disposed || kind === 'error' || kind === 'closed')
|
|
21
|
+
finish(snapshot.view && !snapshot.disposed ? snapshot : undefined);
|
|
22
|
+
};
|
|
23
|
+
check();
|
|
24
|
+
if (done)
|
|
25
|
+
return;
|
|
26
|
+
off = controller.subscribe(check);
|
|
27
|
+
signal.addEventListener('abort', aborted, { once: true });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
6
30
|
export function useChatSession(options) {
|
|
7
31
|
const { controller, activeController: active, mode, presentation, actions, renderEntry, hideLine, renderTool: catchAll, toolRenderers, groupTool, subagent, onOpenImage, onFiles, mentions, slash, attachments, model, effort, artifacts, afterSend, feedback, onOpenFile, onPickFiles, commandMentions, onFocus, labels } = options;
|
|
8
32
|
const state = useSyncExternalStore(controller.subscribe, controller.getSnapshot, controller.getSnapshot);
|
|
@@ -118,7 +142,10 @@ export function useChatSession(options) {
|
|
|
118
142
|
// Only a submission whose outcome is unknown blocks, until it is checked or retried.
|
|
119
143
|
const unconfirmed = state.send.kind === 'unknown';
|
|
120
144
|
const connected = state.connection.kind === 'connected';
|
|
121
|
-
|
|
145
|
+
// Before the first view the composer already takes messages: the controller holds them, shown as the person's own, and sends them once
|
|
146
|
+
// connected (or puts them back if the connection fails).
|
|
147
|
+
const firstConnect = state.connection.kind === 'connecting' && !state.view;
|
|
148
|
+
const sendBlocked = uploading > 0 || state.disposed || !(connected || firstConnect) || unconfirmed;
|
|
122
149
|
/** Puts a message back into the composer ahead of whatever is there now (nothing is dropped). */
|
|
123
150
|
const putBack = (text, images) => {
|
|
124
151
|
if (controller.getSnapshot().disposed)
|
|
@@ -271,8 +298,15 @@ export function useChatSession(options) {
|
|
|
271
298
|
paths = [...paths, result.path];
|
|
272
299
|
}
|
|
273
300
|
}
|
|
274
|
-
else
|
|
275
|
-
|
|
301
|
+
else {
|
|
302
|
+
// While the chat connects its conversation id is still the host's selection: the host's upload waits for the first view's.
|
|
303
|
+
const target = await connectedSnapshot(controller, abort.signal);
|
|
304
|
+
if (abort.signal.aborted || !current())
|
|
305
|
+
return;
|
|
306
|
+
if (!target)
|
|
307
|
+
throw new Error(labels.uploadFailedShort);
|
|
308
|
+
added = await onFiles(files, { identity: target.identity, conversationId: target.conversationId, signal: abort.signal });
|
|
309
|
+
}
|
|
276
310
|
if (current() && !abort.signal.aborted) {
|
|
277
311
|
if (added.length)
|
|
278
312
|
controller.setAttachments([...controller.getSnapshot().draft.attachments, ...added]);
|
|
@@ -363,7 +397,7 @@ export function useChatSession(options) {
|
|
|
363
397
|
act(async () => catalog.onActivate({ name: selected.name, behavior: selected.behavior }));
|
|
364
398
|
};
|
|
365
399
|
const empty = connected && !state.view?.entries.length && !rows.length;
|
|
366
|
-
const loading = (state.connection.kind === 'idle' || state.connection.kind === 'connecting') && !state.view;
|
|
400
|
+
const loading = (state.connection.kind === 'idle' || state.connection.kind === 'connecting') && !state.view && !echo?.length;
|
|
367
401
|
return { state, derived, rows, queued, queueActions, working, waitingForAnswer, connected, sendBlocked, error, setError, report, act, current,
|
|
368
402
|
files, empty, loading, developer, natural, sending, send, putBack, upload, pasteText, picked, pending, uploading, activateMention,
|
|
369
403
|
pickMention: (path) => setPicked(value => value.includes(path) ? value : [...value, path]),
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// `@hachej/boring-ui-kit/drawio-viewer` (D-1398): the official diagrams.net viewer, vendored and pinned (scripts/vendor-drawio-viewer.mjs),
|
|
2
|
+
// as text. The drawing-viewer registry block imports this subpath lazily, only when a draw.io file opens, and runs the text inside its
|
|
3
|
+
// sandboxed frame: nothing is fetched from diagrams.net. The kit never evaluates it.
|
|
4
|
+
export { DRAWIO_VIEWER_SCRIPT } from './vendor/drawio-viewer-static.js';
|
|
5
|
+
/** The jgraph/drawio release the vendored viewer comes from. */
|
|
6
|
+
export const DRAWIO_VIEWER_VERSION = '32.4.1';
|
package/dist/native-chat.d.ts
CHANGED
|
@@ -86,6 +86,12 @@ export interface NativeChatHistorySource {
|
|
|
86
86
|
readonly id: Conversation['id'];
|
|
87
87
|
readonly entries: Conversation['entries'];
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Whether entries older than the view's first one exist, when the host says so. A host that sends a slice of the conversation sets
|
|
91
|
+
* `hasEarlier` on the view it sends (a boolean beside the native fields); without it, a native `head` on a kept entry means earlier entries
|
|
92
|
+
* exist (a reset or compaction starts the view there). `undefined`: the view does not say.
|
|
93
|
+
*/
|
|
94
|
+
export declare function viewHasEarlier(view: ConversationView | undefined): boolean | undefined;
|
|
89
95
|
export type ChatHistoryState = {
|
|
90
96
|
readonly kind: 'disabled' | 'idle';
|
|
91
97
|
} | {
|
|
@@ -195,7 +201,8 @@ export declare function createNativeChatController(options: NativeChatOptions):
|
|
|
195
201
|
/**
|
|
196
202
|
* Sends the composer draft (or `input`) and empties the composer at once. While another send is still being confirmed the message
|
|
197
203
|
* waits in `outbox` and is submitted, in order, after it. A refused message, and any waiting behind it, go back into the composer
|
|
198
|
-
* ahead of what was typed since; nothing is dropped or merged into another message.
|
|
204
|
+
* ahead of what was typed since; nothing is dropped or merged into another message. Sent before any view (while connecting), it is
|
|
205
|
+
* held until the first view arrives and then submitted, in order; if the connection fails first it goes back into the composer. Resolves with the native submission, or
|
|
199
206
|
* undefined when the message was not admitted (refused, unknown, or the controller was disposed).
|
|
200
207
|
*/
|
|
201
208
|
send: (whenBusy?: ChatAttempt["whenBusy"], input?: ChatSendInput) => Promise<Submission | undefined>;
|
package/dist/native-chat.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { awaitWithContext, withCancel } from '@earendil-works/chord/context';
|
|
2
2
|
import { randomUUID } from '@hachej/boring-files/platform';
|
|
3
|
+
/**
|
|
4
|
+
* Whether entries older than the view's first one exist, when the host says so. A host that sends a slice of the conversation sets
|
|
5
|
+
* `hasEarlier` on the view it sends (a boolean beside the native fields); without it, a native `head` on a kept entry means earlier entries
|
|
6
|
+
* exist (a reset or compaction starts the view there). `undefined`: the view does not say.
|
|
7
|
+
*/
|
|
8
|
+
export function viewHasEarlier(view) {
|
|
9
|
+
const said = view?.hasEarlier;
|
|
10
|
+
return typeof said === 'boolean' ? said : undefined;
|
|
11
|
+
}
|
|
3
12
|
function draft(text, attachments, version) {
|
|
4
13
|
if (typeof text !== 'string' || attachments.some(item => typeof item.id !== 'string' || !item.id || typeof item.name !== 'string'
|
|
5
14
|
|| item.content.type !== 'image' || typeof item.content.data !== 'string' || typeof item.content.mimeType !== 'string'))
|
|
@@ -191,6 +200,17 @@ export function createNativeChatController(options) {
|
|
|
191
200
|
publish({ send: { kind: 'admitted', attempt, submissionId, ...(record ? { record } : {}) }, ...(found ? { echo: echoView() } : {}) });
|
|
192
201
|
return true;
|
|
193
202
|
};
|
|
203
|
+
/** Resolves true once a view is there, false if the controller is disposed or its connection fails first. */
|
|
204
|
+
const viewed = () => snapshot.view ? Promise.resolve(true) : new Promise(resolve => {
|
|
205
|
+
const check = () => {
|
|
206
|
+
const failed = snapshot.disposed || snapshot.connection.kind === 'error' || snapshot.connection.kind === 'closed';
|
|
207
|
+
if (snapshot.view || failed) {
|
|
208
|
+
listeners.delete(check);
|
|
209
|
+
resolve(Boolean(snapshot.view) && !snapshot.disposed);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
listeners.add(check);
|
|
213
|
+
});
|
|
194
214
|
/** Messages taken from the composer that wait for the current send (`outbox` in the snapshot). */
|
|
195
215
|
const waiting = [];
|
|
196
216
|
let sending = false;
|
|
@@ -367,15 +387,16 @@ export function createNativeChatController(options) {
|
|
|
367
387
|
if (previous.kind === 'ready' && !previous.hasMore)
|
|
368
388
|
return Promise.resolve();
|
|
369
389
|
if (!historyScan) {
|
|
370
|
-
|
|
390
|
+
// Earlier means below the view's first entry: the view's own entries are on screen already, so the first page never repeats them.
|
|
391
|
+
let firstEntryId;
|
|
371
392
|
for (const entry of snapshot.view?.entries ?? [])
|
|
372
|
-
if (
|
|
373
|
-
|
|
374
|
-
if (
|
|
393
|
+
if (firstEntryId === undefined || entry.id < firstEntryId)
|
|
394
|
+
firstEntryId = entry.id;
|
|
395
|
+
if (firstEntryId === undefined || firstEntryId <= 0 || viewHasEarlier(snapshot.view) === false) {
|
|
375
396
|
publish({ history: { kind: 'ready', entries: Object.freeze([]), hasMore: false } });
|
|
376
397
|
return Promise.resolve();
|
|
377
398
|
}
|
|
378
|
-
historyScan = { maxEntryId };
|
|
399
|
+
historyScan = { maxEntryId: firstEntryId - 1 };
|
|
379
400
|
}
|
|
380
401
|
const scan = historyScan, generation = historyGeneration;
|
|
381
402
|
const entries = 'entries' in previous ? previous.entries : Object.freeze([]);
|
|
@@ -445,7 +466,14 @@ export function createNativeChatController(options) {
|
|
|
445
466
|
}
|
|
446
467
|
watch = acquired;
|
|
447
468
|
const ready = snapshot.timing.inputReady === undefined;
|
|
448
|
-
|
|
469
|
+
// Messages sent before any view were counted against none; they are held until now (see `send`), so the first view is their baseline.
|
|
470
|
+
for (const [index, item] of echoes.entries())
|
|
471
|
+
if (item.blind) {
|
|
472
|
+
item.blind = false;
|
|
473
|
+
firstEntries.set(item.key, acquired.value.entries.length);
|
|
474
|
+
item.base = countOf(acquired.value, item.match) + echoes.slice(0, index).filter(other => other.match === item.match).length;
|
|
475
|
+
}
|
|
476
|
+
publish({ connection: live(), view: acquired.value, ...(snapshot.conversationId !== conversation.id ? { conversationId: conversation.id } : {}), ...(ready ? { timing: Object.freeze({ inputReady: mark('input-ready'), sends: Object.freeze([...timings]) }) } : {}) });
|
|
449
477
|
acquired.start(async (view) => {
|
|
450
478
|
if (snapshot.disposed || watch !== acquired)
|
|
451
479
|
return;
|
|
@@ -474,7 +502,8 @@ export function createNativeChatController(options) {
|
|
|
474
502
|
/**
|
|
475
503
|
* Sends the composer draft (or `input`) and empties the composer at once. While another send is still being confirmed the message
|
|
476
504
|
* waits in `outbox` and is submitted, in order, after it. A refused message, and any waiting behind it, go back into the composer
|
|
477
|
-
* ahead of what was typed since; nothing is dropped or merged into another message.
|
|
505
|
+
* ahead of what was typed since; nothing is dropped or merged into another message. Sent before any view (while connecting), it is
|
|
506
|
+
* held until the first view arrives and then submitted, in order; if the connection fails first it goes back into the composer. Resolves with the native submission, or
|
|
478
507
|
* undefined when the message was not admitted (refused, unknown, or the controller was disposed).
|
|
479
508
|
*/
|
|
480
509
|
send: async (whenBusy = 'followUp', input) => {
|
|
@@ -490,13 +519,15 @@ export function createNativeChatController(options) {
|
|
|
490
519
|
}
|
|
491
520
|
if (input?.restore)
|
|
492
521
|
restoreOf.set(selected, draft(input.restore.text, input.restore.attachments ?? [], 0));
|
|
522
|
+
// Connecting with no view yet: the message is held until the first view (below), which is then its echo's baseline.
|
|
523
|
+
const held = !snapshot.view && snapshot.connection.kind === 'connecting';
|
|
493
524
|
// The message is the person's from this moment: it is drawn at once (the echo) and the composer is emptied, before any
|
|
494
525
|
// validation or network call. The server's copy replaces it when it arrives; a refusal puts the words back in the composer.
|
|
495
526
|
// (A steer takes a queued message back and sends it again, so it is not echoed twice.)
|
|
496
527
|
if (whenBusy !== 'steer') {
|
|
497
528
|
const enter = mark('enter');
|
|
498
529
|
const match = selected.text.trim(), key = `m${++echoSeq}`;
|
|
499
|
-
echoes.push({ key, draft: selected, match, base: countOf(snapshot.view, match) + echoes.filter(item => item.match === match).length, at: enter, sentAt: Date.now(), failed: false, admitted: false });
|
|
530
|
+
echoes.push({ key, draft: selected, match, base: countOf(snapshot.view, match) + echoes.filter(item => item.match === match).length, blind: held, at: enter, sentAt: Date.now(), failed: false, admitted: false });
|
|
500
531
|
firstEntries.set(key, snapshot.view?.entries.length ?? 0);
|
|
501
532
|
timings.push(Object.freeze({ key, enter }));
|
|
502
533
|
if (timings.length > 20)
|
|
@@ -512,6 +543,12 @@ export function createNativeChatController(options) {
|
|
|
512
543
|
sending = true;
|
|
513
544
|
publish(cleared);
|
|
514
545
|
try {
|
|
546
|
+
// Sent while connecting: held (shown as the person's own) until the first view, so nothing is submitted before the connection is up.
|
|
547
|
+
// If the connection fails first, it and everything behind it go back into the composer, marked not sent.
|
|
548
|
+
if (held && !await viewed()) {
|
|
549
|
+
restore([selected], { send: { kind: 'blocked', reason: 'Not connected: the message was not sent' } });
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
515
552
|
let next = { draft: selected, whenBusy, resolve: () => { } };
|
|
516
553
|
const first = await dispatch(next);
|
|
517
554
|
let result = first;
|