@townco/ui 0.1.109 → 0.1.111

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.
@@ -25,6 +25,7 @@ export class HttpTransport {
25
25
  options;
26
26
  isReceivingMessages = false;
27
27
  isInReplayMode = false; // True during session replay, ignores non-replay streaming
28
+ pendingReplayUpdates = []; // Queue session updates during replay for late-subscribing callbacks
28
29
  agentInfo;
29
30
  constructor(options) {
30
31
  // Validate options at the boundary using Zod
@@ -565,6 +566,61 @@ export class HttpTransport {
565
566
  }
566
567
  onSessionUpdate(callback) {
567
568
  this.sessionUpdateCallbacks.add(callback);
569
+ // Replay any queued session updates from replay that arrived before callbacks were registered
570
+ // IMPORTANT: Use notifySessionUpdate to send to ALL registered callbacks, not just this one.
571
+ // This ensures that when multiple hooks register (e.g., message handler, useToolCalls),
572
+ // they ALL receive the queued updates, not just the first one to register.
573
+ if (this.pendingReplayUpdates.length > 0) {
574
+ logger.info("Replaying queued session updates to ALL callbacks", {
575
+ count: this.pendingReplayUpdates.length,
576
+ callbackCount: this.sessionUpdateCallbacks.size,
577
+ updateTypes: this.pendingReplayUpdates.map((u) => u.type),
578
+ updateDetails: this.pendingReplayUpdates.map((u) => {
579
+ if (u.type === "tool_call") {
580
+ return { type: "tool_call", toolCallId: u.toolCall?.id };
581
+ }
582
+ if (u.type === "sources") {
583
+ return {
584
+ type: "sources",
585
+ count: u.sources?.length,
586
+ ids: u.sources?.map((s) => s.id),
587
+ };
588
+ }
589
+ if (u.type === "generic" && u.message) {
590
+ const firstContent = u.message.content?.[0];
591
+ return {
592
+ type: "message",
593
+ role: u.message.role,
594
+ contentLength: firstContent?.type === "text" ? firstContent.text?.length : 0,
595
+ };
596
+ }
597
+ return { type: u.type };
598
+ }),
599
+ });
600
+ // Copy and clear the queue immediately to prevent re-triggering when other callbacks register
601
+ const updatesToReplay = [...this.pendingReplayUpdates];
602
+ this.pendingReplayUpdates = [];
603
+ // Use setTimeout to ensure all callbacks have a chance to register before replaying
604
+ setTimeout(() => {
605
+ logger.info("Starting replay of queued updates to all callbacks", {
606
+ updateCount: updatesToReplay.length,
607
+ callbackCount: this.sessionUpdateCallbacks.size,
608
+ });
609
+ for (const update of updatesToReplay) {
610
+ try {
611
+ logger.info("Replaying update to all callbacks", {
612
+ type: update.type,
613
+ });
614
+ // Notify ALL registered callbacks, not just the one that triggered this replay
615
+ this.notifySessionUpdate(update);
616
+ }
617
+ catch (error) {
618
+ logger.error("Error replaying session update", { error });
619
+ }
620
+ }
621
+ logger.info("Finished replay of queued updates");
622
+ }, 0);
623
+ }
568
624
  return () => {
569
625
  this.sessionUpdateCallbacks.delete(callback);
570
626
  };
@@ -810,6 +866,28 @@ export class HttpTransport {
810
866
  try {
811
867
  const message = JSON.parse(data);
812
868
  logger.debug("Received SSE message", { message });
869
+ // Check if this is a sources message (custom extension to ACP)
870
+ const isSourcesMessage = message &&
871
+ typeof message === "object" &&
872
+ message.method === "session/update" &&
873
+ message.params?.update?.sessionUpdate === "sources";
874
+ if (isSourcesMessage) {
875
+ // Use console.warn directly for gui-console capture
876
+ console.warn("🟢 RECEIVED SOURCES SSE MESSAGE", {
877
+ sourcesCount: message.params?.update?.sources?.length,
878
+ isInReplayMode: this.isInReplayMode,
879
+ callbackCount: this.sessionUpdateCallbacks.size,
880
+ });
881
+ // Handle sources directly without ACP schema validation
882
+ try {
883
+ this.handleSessionNotification(message.params);
884
+ console.warn("🟢 AFTER handleSessionNotification for sources");
885
+ }
886
+ catch (error) {
887
+ console.error("🔴 ERROR in handleSessionNotification for sources", error);
888
+ }
889
+ return;
890
+ }
813
891
  // Validate the message is an ACP agent outgoing message
814
892
  const parseResult = acp.agentOutgoingMessageSchema.safeParse(message);
815
893
  if (!parseResult.success) {
@@ -843,18 +921,29 @@ export class HttpTransport {
843
921
  * Handle a session notification from the agent
844
922
  */
845
923
  handleSessionNotification(params) {
924
+ // Extract update type early to check if this is a sources notification
925
+ const paramsAny = params;
926
+ const updateAny = paramsAny.update;
927
+ const isSourcesNotification = updateAny?.sessionUpdate === "sources";
846
928
  // Skip processing if stream has been cancelled/completed
847
- if (this.streamComplete) {
929
+ // BUT always allow sources through - they arrive after stream completion during replay
930
+ if (this.streamComplete && !isSourcesNotification) {
848
931
  logger.debug("Skipping session notification - stream complete/cancelled");
849
932
  return;
850
933
  }
934
+ if (this.streamComplete && isSourcesNotification) {
935
+ console.warn("🟢 Processing sources notification after stream complete");
936
+ }
851
937
  logger.debug("handleSessionNotification called", { params });
852
938
  // Extract content from the update
853
939
  const paramsExtended = params;
854
940
  const update = paramsExtended.update;
855
941
  const sessionId = this.currentSessionId || params.sessionId;
856
- logger.debug("Update session type", {
942
+ logger.warn("📥 SSE UPDATE RECEIVED", {
857
943
  sessionUpdate: update?.sessionUpdate,
944
+ hasUpdate: !!update,
945
+ isInReplayMode: this.isInReplayMode,
946
+ callbackCount: this.sessionUpdateCallbacks.size,
858
947
  });
859
948
  // Handle sandbox file changes
860
949
  // Type assertion needed because TypeScript doesn't recognize this as a valid session update type
@@ -1064,9 +1153,18 @@ export class HttpTransport {
1064
1153
  isReplay,
1065
1154
  isInReplayMode: this.isInReplayMode,
1066
1155
  });
1067
- // During replay, notify directly since there's no active receive() consumer
1156
+ // During replay, handle specially
1068
1157
  if (isReplay || this.isInReplayMode) {
1069
- this.notifySessionUpdate(sessionUpdate);
1158
+ // If no callbacks registered yet, queue for late-subscribing hooks
1159
+ if (this.sessionUpdateCallbacks.size === 0) {
1160
+ logger.debug("Queueing tool_call for late-subscribing callbacks", {
1161
+ toolCallId: toolCall.id,
1162
+ });
1163
+ this.pendingReplayUpdates.push(sessionUpdate);
1164
+ }
1165
+ else {
1166
+ this.notifySessionUpdate(sessionUpdate);
1167
+ }
1070
1168
  }
1071
1169
  else {
1072
1170
  // Queue tool call as a chunk for ordered processing during live streaming
@@ -1273,8 +1371,16 @@ export class HttpTransport {
1273
1371
  // Check if this is replay (tool_call_update doesn't have isReplay in _meta,
1274
1372
  // but we can check if we're in replay mode)
1275
1373
  if (this.isInReplayMode) {
1276
- // During replay, notify directly since there's no active receive() consumer
1277
- this.notifySessionUpdate(sessionUpdate);
1374
+ // If no callbacks registered yet, queue for late-subscribing hooks
1375
+ if (this.sessionUpdateCallbacks.size === 0) {
1376
+ logger.debug("Queueing tool_call_update for late-subscribing callbacks", {
1377
+ toolCallId: toolCallUpdate.id,
1378
+ });
1379
+ this.pendingReplayUpdates.push(sessionUpdate);
1380
+ }
1381
+ else {
1382
+ this.notifySessionUpdate(sessionUpdate);
1383
+ }
1278
1384
  }
1279
1385
  else {
1280
1386
  // Queue tool call update as a chunk for ordered processing
@@ -1406,7 +1512,16 @@ export class HttpTransport {
1406
1512
  };
1407
1513
  // During replay, notify directly; otherwise queue for ordered processing
1408
1514
  if (this.isInReplayMode) {
1409
- this.notifySessionUpdate(sessionUpdate);
1515
+ // If no callbacks registered yet, queue for late-subscribing hooks
1516
+ if (this.sessionUpdateCallbacks.size === 0) {
1517
+ logger.debug("Queueing tool_output for late-subscribing callbacks", {
1518
+ toolCallId: toolOutput.id,
1519
+ });
1520
+ this.pendingReplayUpdates.push(sessionUpdate);
1521
+ }
1522
+ else {
1523
+ this.notifySessionUpdate(sessionUpdate);
1524
+ }
1410
1525
  }
1411
1526
  else {
1412
1527
  // Queue tool output as a chunk for ordered processing
@@ -1433,21 +1548,56 @@ export class HttpTransport {
1433
1548
  update.sessionUpdate === "sources") {
1434
1549
  // Sources notification - citation sources from tool calls
1435
1550
  const sourcesUpdate = update;
1436
- logger.debug("Received sources notification", {
1551
+ console.warn("🔵 SOURCES in handleSessionNotification", {
1437
1552
  sourcesCount: sourcesUpdate.sources.length,
1553
+ isInReplayMode: this.isInReplayMode,
1554
+ callbackCount: this.sessionUpdateCallbacks.size,
1555
+ firstSourceId: sourcesUpdate.sources[0]?.id,
1556
+ firstSourceToolCallId: sourcesUpdate.sources[0]?.toolCallId,
1438
1557
  });
1439
- // Create a sources chunk for the message queue
1440
- const sourcesChunk = {
1558
+ // Create a sources session update
1559
+ const sessionUpdate = {
1441
1560
  type: "sources",
1561
+ sessionId,
1562
+ status: "active",
1442
1563
  sources: sourcesUpdate.sources,
1443
1564
  };
1444
- // Queue for ordered processing
1445
- const resolver = this.chunkResolvers.shift();
1446
- if (resolver) {
1447
- resolver(sourcesChunk);
1565
+ // During replay, handle sources specially
1566
+ if (this.isInReplayMode) {
1567
+ // If no callbacks are registered yet (React hooks haven't subscribed),
1568
+ // queue the sources to be replayed when they do subscribe
1569
+ if (this.sessionUpdateCallbacks.size === 0) {
1570
+ console.warn("🔵 QUEUEING sources for late-subscribing callbacks", {
1571
+ sourcesCount: sourcesUpdate.sources.length,
1572
+ queueLengthBefore: this.pendingReplayUpdates.length,
1573
+ });
1574
+ this.pendingReplayUpdates.push(sessionUpdate);
1575
+ console.warn("🔵 Queue length after:", {
1576
+ queueLengthAfter: this.pendingReplayUpdates.length,
1577
+ });
1578
+ }
1579
+ else {
1580
+ console.warn("🔵 NOTIFYING sources immediately (callbacks registered)", {
1581
+ sourcesCount: sourcesUpdate.sources.length,
1582
+ callbackCount: this.sessionUpdateCallbacks.size,
1583
+ });
1584
+ this.notifySessionUpdate(sessionUpdate);
1585
+ }
1448
1586
  }
1449
1587
  else {
1450
- this.messageQueue.push(sourcesChunk);
1588
+ // Create a sources chunk for the message queue
1589
+ const sourcesChunk = {
1590
+ type: "sources",
1591
+ sources: sourcesUpdate.sources,
1592
+ };
1593
+ // Queue for ordered processing
1594
+ const resolver = this.chunkResolvers.shift();
1595
+ if (resolver) {
1596
+ resolver(sourcesChunk);
1597
+ }
1598
+ else {
1599
+ this.messageQueue.push(sourcesChunk);
1600
+ }
1451
1601
  }
1452
1602
  }
1453
1603
  else if (update?.sessionUpdate === "agent_message_chunk") {
@@ -1539,8 +1689,18 @@ export class HttpTransport {
1539
1689
  timestamp: new Date().toISOString(),
1540
1690
  },
1541
1691
  };
1542
- // Notify as a complete message (for session replay or initial message)
1543
- this.notifySessionUpdate(messageSessionUpdate);
1692
+ // During replay, queue messages along with tool_calls and sources
1693
+ // so everything is processed in the correct order
1694
+ if (isReplay && this.sessionUpdateCallbacks.size === 0) {
1695
+ logger.debug("Queueing assistant message for late-subscribing callbacks", {
1696
+ textLength: contentObj.text.length,
1697
+ });
1698
+ this.pendingReplayUpdates.push(messageSessionUpdate);
1699
+ }
1700
+ else {
1701
+ // Notify as a complete message (for session replay or initial message)
1702
+ this.notifySessionUpdate(messageSessionUpdate);
1703
+ }
1544
1704
  }
1545
1705
  }
1546
1706
  // Send session update for:
@@ -1554,6 +1714,11 @@ export class HttpTransport {
1554
1714
  else if (update?.sessionUpdate === "user_message_chunk") {
1555
1715
  // Handle user message chunks (could be from replay or new messages)
1556
1716
  logger.debug("Received user_message_chunk", { update });
1717
+ // Check if this is a replay
1718
+ const isReplay = update._meta &&
1719
+ typeof update._meta === "object" &&
1720
+ "isReplay" in update._meta &&
1721
+ update._meta.isReplay === true;
1557
1722
  const content = update.content;
1558
1723
  if (content && typeof content === "object") {
1559
1724
  const contentObj = content;
@@ -1575,13 +1740,28 @@ export class HttpTransport {
1575
1740
  timestamp: new Date().toISOString(),
1576
1741
  },
1577
1742
  };
1578
- logger.debug("Notifying session update for user message");
1579
- this.notifySessionUpdate(sessionUpdate);
1743
+ // During replay, queue messages along with tool_calls and sources
1744
+ // so everything is processed in the correct order
1745
+ if (isReplay && this.sessionUpdateCallbacks.size === 0) {
1746
+ logger.debug("Queueing user message for late-subscribing callbacks", {
1747
+ textLength: contentObj.text.length,
1748
+ });
1749
+ this.pendingReplayUpdates.push(sessionUpdate);
1750
+ }
1751
+ else {
1752
+ logger.debug("Notifying session update for user message");
1753
+ this.notifySessionUpdate(sessionUpdate);
1754
+ }
1580
1755
  }
1581
1756
  }
1582
1757
  }
1583
1758
  else {
1584
1759
  // Handle other session updates
1760
+ logger.warn("⚠️ UNHANDLED SESSION UPDATE - falling through to generic", {
1761
+ sessionUpdate: update?.sessionUpdate,
1762
+ updateKeys: update ? Object.keys(update) : [],
1763
+ hasSources: update && "sources" in update,
1764
+ });
1585
1765
  const sessionUpdate = {
1586
1766
  type: "generic",
1587
1767
  sessionId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@townco/ui",
3
- "version": "0.1.109",
3
+ "version": "0.1.111",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -49,7 +49,7 @@
49
49
  "@radix-ui/react-slot": "^1.2.4",
50
50
  "@radix-ui/react-tabs": "^1.1.13",
51
51
  "@radix-ui/react-tooltip": "^1.2.8",
52
- "@townco/core": "0.0.87",
52
+ "@townco/core": "0.0.89",
53
53
  "@types/mdast": "^4.0.4",
54
54
  "@uiw/react-json-view": "^2.0.0-alpha.39",
55
55
  "class-variance-authority": "^0.7.1",
@@ -67,7 +67,7 @@
67
67
  "zustand": "^5.0.8"
68
68
  },
69
69
  "devDependencies": {
70
- "@townco/tsconfig": "0.1.106",
70
+ "@townco/tsconfig": "0.1.108",
71
71
  "@types/node": "^24.10.0",
72
72
  "@types/react": "^19.2.2",
73
73
  "@types/unist": "^3.0.3",