@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.
Files changed (61) hide show
  1. package/THIRD_PARTY_NOTICES.md +208 -0
  2. package/dist/chat-react-controller.d.ts +4 -1
  3. package/dist/chat-react-controller.js +4 -0
  4. package/dist/chat-react-remote.d.ts +16 -1
  5. package/dist/chat-react-remote.js +69 -10
  6. package/dist/chat-react-session.d.ts +2 -3
  7. package/dist/chat-react-session.js +38 -4
  8. package/dist/drawio-viewer.d.ts +4 -0
  9. package/dist/drawio-viewer.js +6 -0
  10. package/dist/native-chat.d.ts +8 -1
  11. package/dist/native-chat.js +45 -8
  12. package/dist/remote-chat.js +51 -5
  13. package/dist/vendor/drawio-viewer-static.d.ts +3 -0
  14. package/dist/vendor/drawio-viewer-static.js +5 -0
  15. package/package.json +6 -2
  16. package/r/agent-target.json +4 -4
  17. package/r/app-shell.json +9 -9
  18. package/r/board-viewer.json +6 -6
  19. package/r/button.json +1 -1
  20. package/r/dock.json +1 -1
  21. package/r/documents.json +12 -12
  22. package/r/drawing-viewer.json +57 -0
  23. package/r/feedback.json +6 -6
  24. package/r/file-tree.json +4 -4
  25. package/r/highlights.json +3 -3
  26. package/r/html-viewer.json +3 -3
  27. package/r/looks-viewer.json +2 -2
  28. package/r/maquette-viewer.json +1 -1
  29. package/r/markdown-editor.json +3 -3
  30. package/r/mockup-viewer.json +1 -1
  31. package/r/pi-ambient.json +8 -8
  32. package/r/pi-app-kinds.json +10 -10
  33. package/r/pi-app.json +11 -11
  34. package/r/pi-artifacts.json +2 -2
  35. package/r/pi-chat-approval.json +2 -2
  36. package/r/pi-chat-attachments.json +1 -1
  37. package/r/pi-chat-markdown.json +2 -2
  38. package/r/pi-chat-mentions.json +1 -1
  39. package/r/pi-chat-pickers.json +1 -1
  40. package/r/pi-chat-plugins.json +11 -11
  41. package/r/pi-chat-question.json +1 -1
  42. package/r/pi-chat-slash.json +2 -2
  43. package/r/pi-chat-subagent.json +1 -1
  44. package/r/pi-chat-voice.json +3 -3
  45. package/r/pi-chat.json +8 -8
  46. package/r/pi-workspace.json +3 -3
  47. package/r/placed-agent.json +4 -4
  48. package/r/provider-setup.json +1 -1
  49. package/r/registry.json +210 -161
  50. package/r/shared-chat.json +6 -6
  51. package/r/spaces.json +1 -1
  52. package/r/task-list-viewer.json +2 -2
  53. package/r/version-history.json +2 -2
  54. package/r/viewer-code.json +2 -2
  55. package/r/viewer-frame.json +4 -4
  56. package/r/viewer-html.json +4 -4
  57. package/r/viewer-image.json +2 -2
  58. package/r/viewer-markdown.json +5 -5
  59. package/r/viewer-pdf.json +3 -3
  60. package/r/viewers.json +6 -6
  61. package/r/windows.json +1 -1
@@ -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: Partial<Pick<RemoteChat, 'answer' | 'withdraw'>>;
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
- create: async () => {
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 = await createRemoteChat({ endpoint: url, fetch: request => latest.current.fetch(request),
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
- controller = createNativeChatController({ identity: identity, ...remote });
45
- remotes.set(controller, remote);
46
- return controller;
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
- await remote.close();
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 remotes.get(controller)?.close();
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
- return selected.status === 'offline' ? { status: 'offline', conversationId, error: String(selected.error?.message ?? selected.error) } : { status: 'connecting', conversationId };
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: import("./native-chat.js").ChatSnapshot;
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
- const sendBlocked = uploading > 0 || state.disposed || !connected || unconfirmed;
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
- added = await onFiles(files, { identity: state.identity, conversationId: state.conversationId, signal: abort.signal });
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,4 @@
1
+ export { DRAWIO_VIEWER_SCRIPT } from './vendor/drawio-viewer-static.js';
2
+ /** The jgraph/drawio release the vendored viewer comes from. */
3
+ export declare const DRAWIO_VIEWER_VERSION = "32.4.1";
4
+ //# sourceMappingURL=drawio-viewer.d.ts.map
@@ -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';
@@ -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. Resolves with the native submission, or
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>;
@@ -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
- let maxEntryId;
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 (maxEntryId === undefined || entry.id > maxEntryId)
373
- maxEntryId = entry.id;
374
- if (maxEntryId === undefined) {
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
- publish({ connection: live(), view: acquired.value, ...(ready ? { timing: Object.freeze({ inputReady: mark('input-ready'), sends: Object.freeze([...timings]) }) } : {}) });
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. Resolves with the native submission, or
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;