@futurity/chat-react 0.0.2 → 0.1.0

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/dist/index.js CHANGED
@@ -13,6 +13,325 @@ function parseServerMessage(data) {
13
13
  return result.data;
14
14
  }
15
15
 
16
+ // src/stream-accumulator.ts
17
+ import {
18
+ Z_DataEndSplitSchema,
19
+ Z_DataSplitSchema,
20
+ Z_DataSubagentPartSchema
21
+ } from "@futurity/chat-protocol";
22
+ function isTypedChunk(x) {
23
+ return typeof x === "object" && x !== null && "type" in x && typeof x.type === "string";
24
+ }
25
+ var StreamAccumulator = class {
26
+ parts = [];
27
+ textById = /* @__PURE__ */ new Map();
28
+ reasoningById = /* @__PURE__ */ new Map();
29
+ toolById = /* @__PURE__ */ new Map();
30
+ partialToolText = /* @__PURE__ */ new Map();
31
+ processedCount = 0;
32
+ push(part) {
33
+ this.parts.push(part);
34
+ }
35
+ /** Process any new raw chunks and return the full accumulated parts array. */
36
+ accumulate(rawParts) {
37
+ for (let i = this.processedCount; i < rawParts.length; i++) {
38
+ const chunk = rawParts[i];
39
+ if (!isTypedChunk(chunk)) {
40
+ this.parts.push(chunk);
41
+ continue;
42
+ }
43
+ switch (chunk.type) {
44
+ // --- Text accumulation ---
45
+ case "text-start": {
46
+ const part = { type: "text", text: "", state: "streaming" };
47
+ this.textById.set(chunk.id, part);
48
+ this.push(part);
49
+ break;
50
+ }
51
+ case "text-delta": {
52
+ const part = this.textById.get(chunk.id);
53
+ if (part) part.text += chunk.delta;
54
+ break;
55
+ }
56
+ case "text-end": {
57
+ const part = this.textById.get(chunk.id);
58
+ if (part) part.state = "done";
59
+ break;
60
+ }
61
+ // --- Reasoning accumulation ---
62
+ case "reasoning-start": {
63
+ const part = { type: "reasoning", text: "", state: "streaming" };
64
+ this.reasoningById.set(chunk.id, part);
65
+ this.push(part);
66
+ break;
67
+ }
68
+ case "reasoning-delta": {
69
+ const part = this.reasoningById.get(chunk.id);
70
+ if (part) part.text += chunk.delta;
71
+ break;
72
+ }
73
+ case "reasoning-end": {
74
+ const part = this.reasoningById.get(chunk.id);
75
+ if (part) part.state = "done";
76
+ break;
77
+ }
78
+ // --- Tool invocation accumulation ---
79
+ case "tool-input-start": {
80
+ const typeName = chunk.dynamic ? "dynamic-tool" : `tool-${chunk.toolName}`;
81
+ const part = {
82
+ type: typeName,
83
+ toolCallId: chunk.toolCallId,
84
+ toolName: chunk.toolName,
85
+ state: "input-streaming",
86
+ input: void 0,
87
+ providerExecuted: chunk.providerExecuted,
88
+ title: chunk.title
89
+ };
90
+ this.toolById.set(chunk.toolCallId, part);
91
+ this.partialToolText.set(chunk.toolCallId, "");
92
+ this.push(part);
93
+ break;
94
+ }
95
+ case "tool-input-delta": {
96
+ const text = (this.partialToolText.get(chunk.toolCallId) ?? "") + (chunk.inputTextDelta ?? "");
97
+ this.partialToolText.set(chunk.toolCallId, text);
98
+ const part = this.toolById.get(chunk.toolCallId);
99
+ if (part) {
100
+ try {
101
+ part.input = JSON.parse(text);
102
+ } catch {
103
+ }
104
+ }
105
+ break;
106
+ }
107
+ case "tool-input-available": {
108
+ const typeName = chunk.dynamic ? "dynamic-tool" : `tool-${chunk.toolName}`;
109
+ let part = this.toolById.get(chunk.toolCallId);
110
+ if (part) {
111
+ part.state = "input-available";
112
+ part.input = chunk.input;
113
+ part.providerExecuted = chunk.providerExecuted;
114
+ } else {
115
+ part = {
116
+ type: typeName,
117
+ toolCallId: chunk.toolCallId,
118
+ toolName: chunk.toolName,
119
+ state: "input-available",
120
+ input: chunk.input,
121
+ providerExecuted: chunk.providerExecuted,
122
+ title: chunk.title
123
+ };
124
+ this.toolById.set(chunk.toolCallId, part);
125
+ this.push(part);
126
+ }
127
+ break;
128
+ }
129
+ case "tool-input-error": {
130
+ const part = this.toolById.get(chunk.toolCallId);
131
+ if (part) {
132
+ part.state = "output-error";
133
+ part.errorText = chunk.errorText;
134
+ part.input = chunk.input;
135
+ }
136
+ break;
137
+ }
138
+ case "tool-output-available": {
139
+ const part = this.toolById.get(chunk.toolCallId);
140
+ if (part) {
141
+ part.state = "output-available";
142
+ part.output = chunk.output;
143
+ part.preliminary = chunk.preliminary;
144
+ }
145
+ break;
146
+ }
147
+ case "tool-output-error": {
148
+ const part = this.toolById.get(chunk.toolCallId);
149
+ if (part) {
150
+ part.state = "output-error";
151
+ part.errorText = chunk.errorText;
152
+ }
153
+ break;
154
+ }
155
+ // --- Pass-through parts ---
156
+ case "file":
157
+ this.push({
158
+ type: "file",
159
+ mediaType: chunk.mediaType,
160
+ url: chunk.url,
161
+ filename: chunk.filename
162
+ });
163
+ break;
164
+ case "source-url":
165
+ this.push({
166
+ type: "source-url",
167
+ sourceId: chunk.sourceId,
168
+ url: chunk.url,
169
+ title: chunk.title
170
+ });
171
+ break;
172
+ case "source-document":
173
+ this.push({
174
+ type: "source-document",
175
+ sourceId: chunk.sourceId,
176
+ mediaType: chunk.mediaType,
177
+ title: chunk.title,
178
+ filename: chunk.filename
179
+ });
180
+ break;
181
+ case "start-step":
182
+ this.push({ type: "step-start" });
183
+ break;
184
+ // --- Ignored protocol chunks ---
185
+ case "finish-step":
186
+ case "start":
187
+ case "finish":
188
+ case "error":
189
+ break;
190
+ // --- Unknown / custom data parts → pass through ---
191
+ default:
192
+ if (chunk.type.startsWith("data-")) {
193
+ this.push(chunk);
194
+ }
195
+ break;
196
+ }
197
+ }
198
+ this.processedCount = rawParts.length;
199
+ return this.parts;
200
+ }
201
+ };
202
+ function createPreprocessorState(messageId) {
203
+ return {
204
+ messageId: messageId ?? null,
205
+ scannedLength: 0,
206
+ claimedIndices: /* @__PURE__ */ new Set(),
207
+ openGroups: /* @__PURE__ */ new Map(),
208
+ groupsByStartIndex: /* @__PURE__ */ new Map(),
209
+ groupBySubAgentId: /* @__PURE__ */ new Map(),
210
+ accumulators: /* @__PURE__ */ new Map()
211
+ };
212
+ }
213
+ function incrementalPreprocess(state, parts) {
214
+ for (let i2 = state.scannedLength; i2 < parts.length; i2++) {
215
+ const part = parts[i2];
216
+ if (!isTypedChunk(part)) continue;
217
+ if (part.type === "data-split") {
218
+ const splitParse = Z_DataSplitSchema.safeParse(part);
219
+ if (splitParse.success && splitParse.data.data.subAgentId) {
220
+ const sid = splitParse.data.data.subAgentId;
221
+ const group = {
222
+ startIndex: i2,
223
+ title: splitParse.data.data.title,
224
+ subtitle: splitParse.data.data.subtitle,
225
+ desktopSessionId: splitParse.data.data.desktopSessionId,
226
+ subAgentId: sid,
227
+ innerParts: [],
228
+ endIndex: null
229
+ };
230
+ state.openGroups.set(sid, group);
231
+ state.groupsByStartIndex.set(i2, group);
232
+ state.groupBySubAgentId.set(sid, group);
233
+ state.claimedIndices.add(i2);
234
+ continue;
235
+ }
236
+ }
237
+ if (part.type === "data-endsplit") {
238
+ const endSplitParse = Z_DataEndSplitSchema.safeParse(part);
239
+ if (endSplitParse.success && endSplitParse.data?.data?.subAgentId) {
240
+ const sid = endSplitParse.data.data.subAgentId;
241
+ const group = state.openGroups.get(sid);
242
+ if (group) {
243
+ group.endIndex = i2;
244
+ group.desktopSessionId = group.desktopSessionId ?? endSplitParse.data.data.desktopSessionId;
245
+ state.openGroups.delete(sid);
246
+ }
247
+ state.claimedIndices.add(i2);
248
+ continue;
249
+ }
250
+ }
251
+ if (part.type === "data-subagent-part") {
252
+ const subagentParse = Z_DataSubagentPartSchema.safeParse(part);
253
+ if (subagentParse.success) {
254
+ const sid = subagentParse.data.data.subAgentId;
255
+ const group = state.openGroups.get(sid) ?? state.groupBySubAgentId.get(sid);
256
+ if (group) {
257
+ group.innerParts.push(subagentParse.data.data.part);
258
+ }
259
+ state.claimedIndices.add(i2);
260
+ }
261
+ }
262
+ }
263
+ state.scannedLength = parts.length;
264
+ const processed = [];
265
+ let i = 0;
266
+ while (i < parts.length) {
267
+ const groupAtIndex = state.groupsByStartIndex.get(i);
268
+ if (groupAtIndex) {
269
+ let acc = state.accumulators.get(groupAtIndex.startIndex);
270
+ if (!acc) {
271
+ acc = new StreamAccumulator();
272
+ state.accumulators.set(groupAtIndex.startIndex, acc);
273
+ }
274
+ processed.push({
275
+ type: "split-group",
276
+ title: groupAtIndex.title,
277
+ subtitle: groupAtIndex.subtitle,
278
+ // Incrementally accumulate — only processes newly added innerParts
279
+ parts: acc.accumulate(groupAtIndex.innerParts),
280
+ startIndex: groupAtIndex.startIndex,
281
+ endIndex: groupAtIndex.endIndex ?? parts.length,
282
+ desktopSessionId: groupAtIndex.desktopSessionId
283
+ });
284
+ i++;
285
+ continue;
286
+ }
287
+ if (state.claimedIndices.has(i)) {
288
+ i++;
289
+ continue;
290
+ }
291
+ const part = parts[i];
292
+ const type = isTypedChunk(part) ? part.type : void 0;
293
+ if (type === "data-split") {
294
+ const splitParse = Z_DataSplitSchema.safeParse(part);
295
+ if (splitParse.success && !splitParse.data.data.subAgentId) {
296
+ let endIndex = parts.length;
297
+ let desktopSessionId = splitParse.data.data.desktopSessionId;
298
+ for (let searchIndex = i + 1; searchIndex < parts.length; searchIndex++) {
299
+ if (state.claimedIndices.has(searchIndex)) continue;
300
+ const checkPart = parts[searchIndex];
301
+ const endParse = Z_DataEndSplitSchema.safeParse(checkPart);
302
+ if (endParse.success && !endParse.data?.data?.subAgentId) {
303
+ endIndex = searchIndex;
304
+ desktopSessionId = desktopSessionId ?? endParse.data?.data?.desktopSessionId;
305
+ break;
306
+ }
307
+ }
308
+ const groupedParts = parts.slice(i + 1, endIndex).filter((_, idx) => {
309
+ const absIdx = i + 1 + idx;
310
+ return !state.claimedIndices.has(absIdx);
311
+ });
312
+ processed.push({
313
+ type: "split-group",
314
+ title: splitParse.data.data.title,
315
+ subtitle: splitParse.data.data.subtitle,
316
+ parts: groupedParts,
317
+ startIndex: i,
318
+ endIndex,
319
+ desktopSessionId
320
+ });
321
+ i = endIndex < parts.length ? endIndex + 1 : endIndex;
322
+ continue;
323
+ }
324
+ }
325
+ if (type === "data-endsplit") {
326
+ i++;
327
+ continue;
328
+ }
329
+ processed.push({ type: "regular", part, originalIndex: i });
330
+ i++;
331
+ }
332
+ return processed;
333
+ }
334
+
16
335
  // src/tree-builder.ts
17
336
  var MessageNode = class {
18
337
  id;
@@ -379,12 +698,35 @@ function useReconnectingWebSocket({
379
698
 
380
699
  // src/useStreamChat.ts
381
700
  import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
382
- function transformChatHistory(rawMessages) {
701
+ function simpleProcessedParts(parts) {
702
+ return parts.map((part, i) => ({
703
+ type: "regular",
704
+ part,
705
+ originalIndex: i
706
+ }));
707
+ }
708
+ function preprocessMessage(msg, statesMap) {
709
+ if (msg.role !== "assistant") {
710
+ return { ...msg, processedParts: simpleProcessedParts(msg.parts) };
711
+ }
712
+ let state = statesMap.get(msg.id);
713
+ if (!state || state.scannedLength > (msg.parts?.length ?? 0)) {
714
+ state = createPreprocessorState(msg.id);
715
+ statesMap.set(msg.id, state);
716
+ }
717
+ return {
718
+ ...msg,
719
+ processedParts: incrementalPreprocess(state, msg.parts ?? [])
720
+ };
721
+ }
722
+ function transformChatHistory(rawMessages, statesMap) {
383
723
  const parsedMessages = Z_ChatMessage.array().safeParse(rawMessages);
384
724
  if (!parsedMessages.success) {
385
725
  throw new Error("Invalid chat history");
386
726
  }
387
- const messages = parsedMessages.data;
727
+ const messages = parsedMessages.data.map(
728
+ (msg) => preprocessMessage(msg, statesMap)
729
+ );
388
730
  const byId = /* @__PURE__ */ new Map();
389
731
  const tree = buildTree(messages, byId);
390
732
  const initialPath = findLatestPath(tree, byId);
@@ -399,8 +741,6 @@ function useStreamChat({
399
741
  chatId,
400
742
  wsUrl,
401
743
  onStart,
402
- onDelta,
403
- onResume,
404
744
  onFinish,
405
745
  onError,
406
746
  onHistory
@@ -416,9 +756,8 @@ function useStreamChat({
416
756
  const [job, setJob] = useState2("");
417
757
  const hasRequestedChat = useRef2(false);
418
758
  const [pendingClarify, setPendingClarify] = useState2(null);
759
+ const preprocessorStatesRef = useRef2(/* @__PURE__ */ new Map());
419
760
  const onStartRef = useRef2(onStart);
420
- const onDeltaRef = useRef2(onDelta);
421
- const onResumeRef = useRef2(onResume);
422
761
  const onFinishRef = useRef2(onFinish);
423
762
  const onErrorRef = useRef2(onError);
424
763
  const onHistoryRef = useRef2(onHistory);
@@ -426,8 +765,6 @@ function useStreamChat({
426
765
  });
427
766
  useEffect2(() => {
428
767
  onStartRef.current = onStart;
429
- onDeltaRef.current = onDelta;
430
- onResumeRef.current = onResume;
431
768
  onFinishRef.current = onFinish;
432
769
  onErrorRef.current = onError;
433
770
  onHistoryRef.current = onHistory;
@@ -459,14 +796,81 @@ function useStreamChat({
459
796
  }
460
797
  case "stream": {
461
798
  const messageId = message.messageId;
799
+ const delta = message.delta;
800
+ const consolidated = message.consolidated;
462
801
  setStatus("streaming");
463
- onDeltaRef.current?.(messageId, message.delta, message.consolidated);
802
+ setMessages((prev) => {
803
+ const states = preprocessorStatesRef.current;
804
+ const exists = prev.some((m) => m.id === messageId);
805
+ let updated;
806
+ if (!exists) {
807
+ const newMsg = {
808
+ id: messageId,
809
+ role: "assistant",
810
+ parts: [delta],
811
+ processedParts: []
812
+ };
813
+ updated = [...prev, newMsg];
814
+ } else {
815
+ updated = prev.map((m) => {
816
+ if (m.id !== messageId) return m;
817
+ const parts = m.parts ?? [];
818
+ const nextParts = consolidated ? [...parts.slice(0, -1), delta] : [...parts, delta];
819
+ return { ...m, parts: nextParts };
820
+ });
821
+ }
822
+ return updated.map(
823
+ (m) => m.id === messageId ? preprocessMessage(m, states) : m
824
+ );
825
+ });
464
826
  break;
465
827
  }
466
828
  case "stream_resume": {
467
829
  const messageId = message.messageId;
468
830
  setStatus("streaming");
469
- onResumeRef.current?.(messageId, message.parts);
831
+ setMessages((prev) => {
832
+ const states = preprocessorStatesRef.current;
833
+ states.delete(messageId);
834
+ return prev.map((m) => {
835
+ if (m.id !== messageId) return m;
836
+ const updated = { ...m, parts: message.parts };
837
+ return preprocessMessage(updated, states);
838
+ });
839
+ });
840
+ break;
841
+ }
842
+ case "inject_ack": {
843
+ const { new_assistant_id } = message.data;
844
+ setMessages((prev) => {
845
+ const exists = prev.some((m) => m.id === new_assistant_id);
846
+ if (exists) return prev;
847
+ return [
848
+ ...prev,
849
+ {
850
+ id: new_assistant_id,
851
+ role: "assistant",
852
+ parts: [],
853
+ processedParts: []
854
+ }
855
+ ];
856
+ });
857
+ break;
858
+ }
859
+ case "inject_split": {
860
+ const { new_message_id } = message.data;
861
+ setMessages((prev) => {
862
+ const exists = prev.some((m) => m.id === new_message_id);
863
+ if (exists) return prev;
864
+ return [
865
+ ...prev,
866
+ {
867
+ id: new_message_id,
868
+ role: "assistant",
869
+ parts: [],
870
+ processedParts: []
871
+ }
872
+ ];
873
+ });
470
874
  break;
471
875
  }
472
876
  case "done": {
@@ -481,7 +885,10 @@ function useStreamChat({
481
885
  break;
482
886
  }
483
887
  case "chat_history": {
484
- const result = transformChatHistory(message.data.messages);
888
+ const result = transformChatHistory(
889
+ message.data.messages,
890
+ preprocessorStatesRef.current
891
+ );
485
892
  result.activeMessageId = message.data.activeMessageId;
486
893
  onHistoryRef.current?.(result);
487
894
  break;
@@ -535,6 +942,7 @@ function useStreamChat({
535
942
  id: crypto.randomUUID(),
536
943
  role: "user",
537
944
  parts: payload.parts,
945
+ processedParts: simpleProcessedParts(payload.parts),
538
946
  metadata: {
539
947
  parent_id
540
948
  }
@@ -570,12 +978,14 @@ function useStreamChat({
570
978
  const connected = await ensureConnected();
571
979
  if (!connected) return;
572
980
  const message_id = crypto.randomUUID();
981
+ const parts = [{ type: "text", text }];
573
982
  setMessages((prev) => [
574
983
  ...prev,
575
984
  {
576
985
  id: message_id,
577
986
  role: "user",
578
- parts: [{ type: "text", text }]
987
+ parts,
988
+ processedParts: simpleProcessedParts(parts)
579
989
  }
580
990
  ]);
581
991
  send({
@@ -610,6 +1020,7 @@ function useStreamChat({
610
1020
  setJob("");
611
1021
  setPendingClarify(null);
612
1022
  hasRequestedChat.current = false;
1023
+ preprocessorStatesRef.current.clear();
613
1024
  }, []);
614
1025
  return {
615
1026
  messages,
@@ -626,10 +1037,13 @@ function useStreamChat({
626
1037
  }
627
1038
  export {
628
1039
  MessageNode,
1040
+ StreamAccumulator,
629
1041
  WebSocketConnection,
630
1042
  Z_ChatMessage,
631
1043
  buildTree,
1044
+ createPreprocessorState,
632
1045
  findLatestPath,
1046
+ incrementalPreprocess,
633
1047
  messageMetadataSchema,
634
1048
  parseServerMessage,
635
1049
  useReconnectingWebSocket,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@futurity/chat-react",
3
- "version": "0.0.2",
3
+ "version": "0.1.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "sideEffects": false,
@@ -13,11 +13,15 @@
13
13
  },
14
14
  "main": "./dist/index.js",
15
15
  "types": "./dist/index.d.ts",
16
- "files": ["dist", "src", "README.md"],
16
+ "files": [
17
+ "dist",
18
+ "src",
19
+ "README.md"
20
+ ],
17
21
  "scripts": {
18
22
  "build": "tsup",
19
23
  "check": "tsgo --noEmit",
20
- "prepublishOnly": "tsup"
24
+ "prepublishOnly": "bunx tsup"
21
25
  },
22
26
  "dependencies": {
23
27
  "@futurity/chat-protocol": "0.1.0"
package/src/index.ts CHANGED
@@ -4,6 +4,13 @@
4
4
  export type { ClientCommand } from "./chat-protocol";
5
5
  // Protocol helpers
6
6
  export { parseServerMessage } from "./chat-protocol";
7
+ // Stream accumulator
8
+ export type { PreprocessorState, ProcessedPart } from "./stream-accumulator";
9
+ export {
10
+ createPreprocessorState,
11
+ incrementalPreprocess,
12
+ StreamAccumulator,
13
+ } from "./stream-accumulator";
7
14
  // Tree utilities
8
15
  export { buildTree, findLatestPath, MessageNode } from "./tree-builder";
9
16
  // Types