@yourgpt/copilot-sdk 2.5.1-beta.1 → 2.5.2-alpha.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.
@@ -1561,6 +1561,10 @@ var AbstractChat = class {
1561
1561
  get isStreaming() {
1562
1562
  return this.transport.isStreaming();
1563
1563
  }
1564
+ /** The thread id currently associated with this chat, if any. */
1565
+ get threadId() {
1566
+ return this.config.threadId;
1567
+ }
1564
1568
  // ============================================
1565
1569
  // Public Actions
1566
1570
  // ============================================
@@ -2206,6 +2210,7 @@ ${this.dynamicContext}`.trim() : this.config.systemPrompt;
2206
2210
  this.debug("Starting to process stream");
2207
2211
  let chunkCount = 0;
2208
2212
  let toolCallsEmitted = false;
2213
+ let postToolTextStreamed = false;
2209
2214
  let pendingClientToolCalls;
2210
2215
  for await (const chunk of stream) {
2211
2216
  chunkCount++;
@@ -2262,15 +2267,27 @@ ${this.dynamicContext}`.trim() : this.config.systemPrompt;
2262
2267
  continue;
2263
2268
  }
2264
2269
  if (!this.streamState) {
2265
- if (chunk.type === "tool_calls") {
2270
+ if (chunk.type === "message:delta") {
2271
+ this.debug(
2272
+ "message:delta with no streamState \u2014 auto-init new message"
2273
+ );
2274
+ const currentLeaf = this.state.messages;
2275
+ const currentLeafId = currentLeaf.length > 0 ? currentLeaf[currentLeaf.length - 1].id : void 0;
2276
+ const newMessage = createEmptyAssistantMessage(void 0, {
2277
+ parentId: currentLeafId
2278
+ });
2279
+ this.state.pushMessage(newMessage);
2280
+ this.streamState = createStreamState(newMessage.id);
2281
+ this.callbacks.onMessageStart?.(newMessage.id);
2282
+ postToolTextStreamed = true;
2283
+ } else if (chunk.type === "tool_calls") {
2266
2284
  pendingClientToolCalls = chunk.toolCalls;
2267
2285
  this.debug("tool_calls (post-message:end, stored as pending)", {
2268
2286
  count: pendingClientToolCalls?.length,
2269
2287
  ids: pendingClientToolCalls?.map((tc) => tc.id)
2270
2288
  });
2271
2289
  continue;
2272
- }
2273
- if (chunk.type === "done") {
2290
+ } else if (chunk.type === "done") {
2274
2291
  this.debug("done (post-message:end)", {
2275
2292
  hasPendingToolCalls: !!pendingClientToolCalls?.length,
2276
2293
  pendingCount: pendingClientToolCalls?.length ?? 0,
@@ -2362,9 +2379,10 @@ ${this.dynamicContext}`.trim() : this.config.systemPrompt;
2362
2379
  });
2363
2380
  }
2364
2381
  continue;
2382
+ } else {
2383
+ this.debug("warning", "streamState is null, skipping chunk");
2384
+ continue;
2365
2385
  }
2366
- this.debug("warning", "streamState is null, skipping chunk");
2367
- continue;
2368
2386
  }
2369
2387
  this.streamState = processStreamChunk(chunk, this.streamState);
2370
2388
  if (chunk.type === "action:start") {
@@ -2460,9 +2478,12 @@ ${this.dynamicContext}`.trim() : this.config.systemPrompt;
2460
2478
  }
2461
2479
  }
2462
2480
  }
2481
+ let seenToolResult = false;
2463
2482
  for (const msg of chunk.messages) {
2483
+ if (msg.role === "tool") seenToolResult = true;
2464
2484
  if (msg.role === "assistant" && !msg.tool_calls?.length) {
2465
- continue;
2485
+ if (!seenToolResult) continue;
2486
+ if (postToolTextStreamed) continue;
2466
2487
  }
2467
2488
  if (msg.role === "assistant" && msg.tool_calls?.length && msg.tool_calls.every(
2468
2489
  (toolCall) => currentStreamToolCallIds.has(toolCall.id)
@@ -2990,6 +3011,7 @@ var AbstractAgentLoop = class {
2990
3011
  }
2991
3012
  const result = await tool.handler(toolCall.args, {
2992
3013
  signal: this.abortController?.signal,
3014
+ threadId: this.config.getThreadId?.(),
2993
3015
  data: { toolCallId: toolCall.id },
2994
3016
  approvalData
2995
3017
  });
@@ -3517,7 +3539,13 @@ var ChatWithTools = class {
3517
3539
  this.agentLoop = new AbstractAgentLoop(
3518
3540
  {
3519
3541
  maxIterations: config.maxIterations ?? 20,
3520
- tools: config.tools
3542
+ tools: config.tools,
3543
+ // Expose this chat's current threadId to tool handlers via
3544
+ // ToolContext.threadId. Read lazily per invocation so it reflects
3545
+ // the id assigned by the server mid-stream (thread:created).
3546
+ // If the caller provided a getThreadId override (e.g. the React
3547
+ // provider exposing its stable registry key), prefer that.
3548
+ getThreadId: config.getThreadId ? () => config.getThreadId() : () => this.chat?.threadId
3521
3549
  },
3522
3550
  {
3523
3551
  onExecutionsChange: (executions) => {
@@ -5427,7 +5455,8 @@ function CopilotProvider(props) {
5427
5455
  mcpServers,
5428
5456
  optimization,
5429
5457
  messageHistory,
5430
- skills
5458
+ skills,
5459
+ concurrentThreads = false
5431
5460
  } = props;
5432
5461
  const isThreadIdControlled = Object.prototype.hasOwnProperty.call(
5433
5462
  props,
@@ -5465,87 +5494,350 @@ function CopilotProvider(props) {
5465
5494
  controlled: isThreadIdControlled,
5466
5495
  value: threadId
5467
5496
  });
5497
+ const SINGLE_INSTANCE_KEY = "__single__";
5468
5498
  const chatRef = React2.useRef(null);
5469
- if (chatRef.current !== null && chatRef.current.disposed) {
5470
- chatRef.current.revive();
5471
- debugLog("Revived disposed instance (React StrictMode)");
5472
- }
5473
- if (chatRef.current === null) {
5474
- const uiInitialMessages = initialMessages;
5475
- chatRef.current = new ReactChatWithTools(
5476
- {
5477
- runtimeUrl,
5478
- systemPrompt,
5479
- threadId,
5480
- onCreateSession,
5481
- yourgptConfig,
5482
- initialMessages: uiInitialMessages,
5483
- streaming,
5484
- headers,
5485
- body,
5486
- parseError,
5487
- debug,
5488
- maxIterations,
5489
- maxIterationsMessage,
5490
- optimization
5491
- },
5492
- {
5493
- onToolExecutionsChange: (executions) => {
5494
- debugLog("Tool executions changed:", executions.length);
5495
- setToolExecutions(executions);
5496
- setAgentIteration(chatRef.current?.iteration ?? 0);
5497
- },
5498
- onApprovalRequired: (execution) => {
5499
- debugLog("Tool approval required:", execution.name);
5500
- },
5501
- onContextUsageChange: (usage) => {
5502
- setContextUsage(usage);
5503
- },
5504
- onError: (error2) => {
5505
- if (error2) onError?.(error2);
5506
- },
5507
- onThreadChange: (id) => {
5508
- debugLog("Thread/session ID assigned:", id);
5509
- setActualThreadId(id);
5510
- onThreadChange?.(id);
5511
- },
5512
- onSessionStatusChange: (status2) => {
5513
- debugLog("Session status:", status2);
5514
- setSessionStatus(status2);
5499
+ const instancesRef = React2.useRef(/* @__PURE__ */ new Map());
5500
+ const activeInstanceKeyRef = React2.useRef(SINGLE_INSTANCE_KEY);
5501
+ const pendingCounterRef = React2.useRef(0);
5502
+ const sharedToolsRef = React2.useRef(/* @__PURE__ */ new Map());
5503
+ const sharedSkillsRef = React2.useRef([]);
5504
+ const sharedSystemContextRef = React2.useRef("");
5505
+ const subscribersRef = React2.useRef(/* @__PURE__ */ new Set());
5506
+ const stableSubscribe = React2.useMemo(
5507
+ () => (cb) => {
5508
+ subscribersRef.current.add(cb);
5509
+ return () => {
5510
+ subscribersRef.current.delete(cb);
5511
+ };
5512
+ },
5513
+ []
5514
+ );
5515
+ const [busyThreadIds, setBusyThreadIds] = React2.useState(
5516
+ () => /* @__PURE__ */ new Set()
5517
+ );
5518
+ const recomputeBusyThreadIds = React2.useCallback(() => {
5519
+ if (!concurrentThreads) return;
5520
+ const next = /* @__PURE__ */ new Set();
5521
+ for (const [key, inst] of instancesRef.current) {
5522
+ if (key === SINGLE_INSTANCE_KEY) continue;
5523
+ if (key.startsWith("__pending_")) continue;
5524
+ const s = inst.status;
5525
+ if (s === "streaming" || s === "submitted") next.add(key);
5526
+ }
5527
+ setBusyThreadIds((prev) => {
5528
+ if (prev.size === next.size) {
5529
+ let same = true;
5530
+ for (const id of next) {
5531
+ if (!prev.has(id)) {
5532
+ same = false;
5533
+ break;
5534
+ }
5535
+ }
5536
+ if (same) return prev;
5537
+ }
5538
+ return next;
5539
+ });
5540
+ }, [concurrentThreads]);
5541
+ const [pendingApprovalsByThread, setPendingApprovalsByThread] = React2.useState(() => /* @__PURE__ */ new Map());
5542
+ const recomputePendingApprovalsByThread = React2.useCallback(() => {
5543
+ if (!concurrentThreads) return;
5544
+ const next = /* @__PURE__ */ new Map();
5545
+ for (const [key, inst] of instancesRef.current) {
5546
+ if (key === SINGLE_INSTANCE_KEY) continue;
5547
+ if (key.startsWith("__pending_")) continue;
5548
+ const pending = inst.toolExecutions.filter(
5549
+ (e) => e.approvalStatus === "required"
5550
+ );
5551
+ if (pending.length > 0) next.set(key, pending);
5552
+ }
5553
+ setPendingApprovalsByThread((prev) => {
5554
+ if (prev.size !== next.size) return next;
5555
+ for (const [id, execs] of next) {
5556
+ const prevExecs = prev.get(id);
5557
+ if (!prevExecs || prevExecs.length !== execs.length) return next;
5558
+ for (let i = 0; i < execs.length; i++) {
5559
+ if (prevExecs[i] !== execs[i]) return next;
5560
+ }
5561
+ }
5562
+ return prev;
5563
+ });
5564
+ }, [concurrentThreads]);
5565
+ const notifyStateChange = React2.useCallback(() => {
5566
+ for (const cb of subscribersRef.current) cb();
5567
+ recomputeBusyThreadIds();
5568
+ recomputePendingApprovalsByThread();
5569
+ }, [recomputeBusyThreadIds, recomputePendingApprovalsByThread]);
5570
+ const configRef = React2.useRef({
5571
+ runtimeUrl,
5572
+ systemPrompt,
5573
+ threadId,
5574
+ onCreateSession,
5575
+ yourgptConfig,
5576
+ initialMessages,
5577
+ streaming,
5578
+ headers,
5579
+ body,
5580
+ parseError,
5581
+ debug,
5582
+ maxIterations,
5583
+ maxIterationsMessage,
5584
+ optimization
5585
+ });
5586
+ configRef.current = {
5587
+ runtimeUrl,
5588
+ systemPrompt,
5589
+ threadId,
5590
+ onCreateSession,
5591
+ yourgptConfig,
5592
+ initialMessages,
5593
+ streaming,
5594
+ headers,
5595
+ body,
5596
+ parseError,
5597
+ debug,
5598
+ maxIterations,
5599
+ maxIterationsMessage,
5600
+ optimization
5601
+ };
5602
+ const callbacksRef = React2.useRef({ onError, onThreadChange });
5603
+ callbacksRef.current = { onError, onThreadChange };
5604
+ const handleInstanceThreadAssigned = React2.useCallback(
5605
+ (inst, newId) => {
5606
+ let oldKey;
5607
+ for (const [k, v] of instancesRef.current) {
5608
+ if (v === inst) {
5609
+ oldKey = k;
5610
+ break;
5611
+ }
5612
+ }
5613
+ if (oldKey === void 0) return;
5614
+ const isInternalKey = oldKey === SINGLE_INSTANCE_KEY || oldKey.startsWith("__pending_");
5615
+ const shouldRekey = !concurrentThreads || isInternalKey;
5616
+ if (shouldRekey && oldKey !== newId && !instancesRef.current.has(newId)) {
5617
+ instancesRef.current.delete(oldKey);
5618
+ instancesRef.current.set(newId, inst);
5619
+ if (activeInstanceKeyRef.current === oldKey) {
5620
+ activeInstanceKeyRef.current = newId;
5621
+ }
5622
+ if (inst === chatRef.current) {
5623
+ debugLog("Thread/session ID assigned:", newId);
5624
+ setActualThreadId(newId);
5625
+ callbacksRef.current.onThreadChange?.(newId);
5626
+ }
5627
+ }
5628
+ recomputeBusyThreadIds();
5629
+ recomputePendingApprovalsByThread();
5630
+ },
5631
+ [
5632
+ concurrentThreads,
5633
+ debugLog,
5634
+ recomputeBusyThreadIds,
5635
+ recomputePendingApprovalsByThread
5636
+ ]
5637
+ );
5638
+ const createInstance = React2.useCallback(
5639
+ (key, opts) => {
5640
+ const cfg = configRef.current;
5641
+ const initialThreadId = key === SINGLE_INSTANCE_KEY || key.startsWith("__pending_") ? cfg.threadId : key;
5642
+ const inst = new ReactChatWithTools(
5643
+ {
5644
+ runtimeUrl: cfg.runtimeUrl,
5645
+ systemPrompt: cfg.systemPrompt,
5646
+ threadId: initialThreadId,
5647
+ onCreateSession: cfg.onCreateSession,
5648
+ yourgptConfig: cfg.yourgptConfig,
5649
+ initialMessages: opts?.initialMessages ?? cfg.initialMessages,
5650
+ streaming: cfg.streaming,
5651
+ headers: cfg.headers,
5652
+ body: cfg.body,
5653
+ parseError: cfg.parseError,
5654
+ debug: cfg.debug,
5655
+ maxIterations: cfg.maxIterations,
5656
+ maxIterationsMessage: cfg.maxIterationsMessage,
5657
+ optimization: cfg.optimization,
5658
+ // Expose the registry key (usually the UI thread id after
5659
+ // assignLocalThreadId runs) to tool handlers, NOT chat.config.threadId.
5660
+ // For extensions that key per-thread state on context.threadId (e.g.
5661
+ // browser-tab pinning), this guarantees a stable id — even while the
5662
+ // backend session id is still being resolved. Internal keys
5663
+ // (__single__, __pending_*) return undefined so handlers can treat
5664
+ // them as "not yet committed".
5665
+ getThreadId: () => {
5666
+ for (const [k, v] of instancesRef.current) {
5667
+ if (v === inst) {
5668
+ if (k === SINGLE_INSTANCE_KEY || k.startsWith("__pending_")) {
5669
+ return void 0;
5670
+ }
5671
+ return k;
5672
+ }
5673
+ }
5674
+ return void 0;
5675
+ }
5515
5676
  },
5516
- onStreamChunk: (chunk) => {
5517
- if (streamListenersRef.current.size > 0) {
5518
- for (const handler of streamListenersRef.current) {
5519
- handler(chunk);
5677
+ {
5678
+ onToolExecutionsChange: (executions) => {
5679
+ debugLog("Tool executions changed:", executions.length);
5680
+ if (inst === chatRef.current) {
5681
+ setToolExecutions(executions);
5682
+ setAgentIteration(inst.iteration ?? 0);
5683
+ }
5684
+ recomputePendingApprovalsByThread();
5685
+ },
5686
+ onApprovalRequired: (execution) => {
5687
+ debugLog("Tool approval required:", execution.name);
5688
+ recomputePendingApprovalsByThread();
5689
+ },
5690
+ onContextUsageChange: (usage) => {
5691
+ if (inst === chatRef.current) {
5692
+ setContextUsage(usage);
5693
+ }
5694
+ },
5695
+ onError: (error2) => {
5696
+ if (error2 && inst === chatRef.current) {
5697
+ callbacksRef.current.onError?.(error2);
5698
+ }
5699
+ },
5700
+ onThreadChange: (id) => {
5701
+ handleInstanceThreadAssigned(inst, id);
5702
+ },
5703
+ onSessionStatusChange: (status2) => {
5704
+ debugLog("Session status:", status2);
5705
+ if (inst === chatRef.current) {
5706
+ setSessionStatus(status2);
5707
+ }
5708
+ },
5709
+ onStreamChunk: (chunk) => {
5710
+ if (streamListenersRef.current.size > 0) {
5711
+ for (const handler of streamListenersRef.current) {
5712
+ handler(chunk);
5713
+ }
5520
5714
  }
5521
5715
  }
5522
5716
  }
5717
+ );
5718
+ for (const { tool } of sharedToolsRef.current.values()) {
5719
+ inst.registerTool(tool);
5523
5720
  }
5524
- );
5721
+ if (sharedSkillsRef.current.length > 0) {
5722
+ inst.setInlineSkills(sharedSkillsRef.current);
5723
+ }
5724
+ if (sharedSystemContextRef.current) {
5725
+ inst.setContext(sharedSystemContextRef.current);
5726
+ }
5727
+ inst.subscribe(notifyStateChange);
5728
+ instancesRef.current.set(key, inst);
5729
+ return inst;
5730
+ },
5731
+ [
5732
+ debugLog,
5733
+ handleInstanceThreadAssigned,
5734
+ notifyStateChange,
5735
+ recomputePendingApprovalsByThread
5736
+ ]
5737
+ );
5738
+ if (chatRef.current !== null && chatRef.current.disposed) {
5739
+ for (const inst of instancesRef.current.values()) {
5740
+ inst.revive();
5741
+ inst.subscribe(notifyStateChange);
5742
+ }
5743
+ debugLog("Revived disposed instance(s) (React StrictMode)");
5525
5744
  }
5745
+ if (chatRef.current === null) {
5746
+ const initialKey = concurrentThreads && threadId ? threadId : SINGLE_INSTANCE_KEY;
5747
+ activeInstanceKeyRef.current = initialKey;
5748
+ chatRef.current = createInstance(initialKey);
5749
+ }
5750
+ const switchActiveInstance = React2.useCallback(
5751
+ (key, opts) => {
5752
+ if (!concurrentThreads) {
5753
+ chatRef.current?.setActiveThread(key);
5754
+ return;
5755
+ }
5756
+ let targetKey;
5757
+ if (key == null) {
5758
+ const currentKey = activeInstanceKeyRef.current;
5759
+ const currentInst = instancesRef.current.get(currentKey);
5760
+ const isCurrentFreshEmpty = currentInst !== void 0 && currentInst.messages.length === 0 && (currentKey === SINGLE_INSTANCE_KEY || currentKey.startsWith("__pending_"));
5761
+ if (isCurrentFreshEmpty) {
5762
+ return;
5763
+ }
5764
+ targetKey = `__pending_${++pendingCounterRef.current}__`;
5765
+ } else {
5766
+ targetKey = key;
5767
+ }
5768
+ let inst = instancesRef.current.get(targetKey);
5769
+ const wasFresh = !inst;
5770
+ if (!inst) {
5771
+ const currentKey = activeInstanceKeyRef.current;
5772
+ const currentInst = instancesRef.current.get(currentKey);
5773
+ const currentIsPromotableSlot = currentInst !== void 0 && currentInst.messages.length === 0 && (currentKey === SINGLE_INSTANCE_KEY || currentKey.startsWith("__pending_"));
5774
+ if (currentIsPromotableSlot) {
5775
+ instancesRef.current.delete(currentKey);
5776
+ instancesRef.current.set(targetKey, currentInst);
5777
+ currentInst.setActiveThread(targetKey);
5778
+ inst = currentInst;
5779
+ if (opts?.hydrateMessages) {
5780
+ inst.setMessages(opts.hydrateMessages);
5781
+ }
5782
+ if (opts?.hydrateActiveLeafId) {
5783
+ inst.switchBranch(opts.hydrateActiveLeafId);
5784
+ }
5785
+ } else {
5786
+ inst = createInstance(targetKey, {
5787
+ initialMessages: opts?.hydrateMessages
5788
+ });
5789
+ if (opts?.hydrateActiveLeafId) {
5790
+ inst.switchBranch(opts.hydrateActiveLeafId);
5791
+ }
5792
+ }
5793
+ } else if (wasFresh && opts?.hydrateMessages && inst.messages.length === 0) {
5794
+ inst.setMessages(opts.hydrateMessages);
5795
+ if (opts.hydrateActiveLeafId)
5796
+ inst.switchBranch(opts.hydrateActiveLeafId);
5797
+ }
5798
+ if (activeInstanceKeyRef.current === targetKey) return;
5799
+ activeInstanceKeyRef.current = targetKey;
5800
+ chatRef.current = inst;
5801
+ if (targetKey === SINGLE_INSTANCE_KEY || targetKey.startsWith("__pending_")) {
5802
+ setActualThreadId(void 0);
5803
+ } else {
5804
+ setActualThreadId(targetKey);
5805
+ }
5806
+ setSessionStatus(inst.getSessionStatus());
5807
+ setToolExecutions(inst.toolExecutions);
5808
+ setAgentIteration(inst.iteration);
5809
+ notifyStateChange();
5810
+ debugLog("Active instance switched", { key: targetKey });
5811
+ },
5812
+ [concurrentThreads, createInstance, debugLog, notifyStateChange]
5813
+ );
5526
5814
  React2.useEffect(() => {
5527
- if (chatRef.current && systemPrompt !== void 0) {
5528
- chatRef.current.setSystemPrompt(systemPrompt);
5529
- debugLog("System prompt updated from prop");
5815
+ if (systemPrompt === void 0) return;
5816
+ for (const inst of instancesRef.current.values()) {
5817
+ inst.setSystemPrompt(systemPrompt);
5530
5818
  }
5819
+ debugLog("System prompt updated from prop");
5531
5820
  }, [systemPrompt, debugLog]);
5532
5821
  React2.useEffect(() => {
5533
- if (chatRef.current && headers !== void 0) {
5534
- chatRef.current.setHeaders(headers);
5535
- debugLog("Headers config updated from prop");
5822
+ if (headers === void 0) return;
5823
+ for (const inst of instancesRef.current.values()) {
5824
+ inst.setHeaders(headers);
5536
5825
  }
5826
+ debugLog("Headers config updated from prop");
5537
5827
  }, [headers, debugLog]);
5538
5828
  React2.useEffect(() => {
5539
- if (chatRef.current && body !== void 0) {
5540
- chatRef.current.setBody(body);
5541
- debugLog("Body config updated from prop");
5829
+ if (body === void 0) return;
5830
+ for (const inst of instancesRef.current.values()) {
5831
+ inst.setBody(body);
5542
5832
  }
5833
+ debugLog("Body config updated from prop");
5543
5834
  }, [body, debugLog]);
5544
5835
  React2.useEffect(() => {
5545
- if (chatRef.current && runtimeUrl !== void 0) {
5546
- chatRef.current.setUrl(runtimeUrl);
5547
- debugLog("URL config updated from prop");
5836
+ if (runtimeUrl === void 0) return;
5837
+ for (const inst of instancesRef.current.values()) {
5838
+ inst.setUrl(runtimeUrl);
5548
5839
  }
5840
+ debugLog("URL config updated from prop");
5549
5841
  }, [runtimeUrl, debugLog]);
5550
5842
  React2.useEffect(() => {
5551
5843
  const prev = lastControlledThreadIdRef.current;
@@ -5561,11 +5853,21 @@ function CopilotProvider(props) {
5561
5853
  if (!isThreadIdControlled) {
5562
5854
  return;
5563
5855
  }
5564
- chatRef.current?.setActiveThread(threadId ?? null);
5565
- setActualThreadId(threadId);
5566
- setSessionStatus(threadId ? "ready" : "idle");
5856
+ if (concurrentThreads) {
5857
+ switchActiveInstance(threadId ?? null);
5858
+ } else {
5859
+ chatRef.current?.setActiveThread(threadId ?? null);
5860
+ setActualThreadId(threadId);
5861
+ setSessionStatus(threadId ? "ready" : "idle");
5862
+ }
5567
5863
  debugLog("Thread/session synced from prop", { threadId });
5568
- }, [debugLog, isThreadIdControlled, threadId]);
5864
+ }, [
5865
+ debugLog,
5866
+ isThreadIdControlled,
5867
+ threadId,
5868
+ concurrentThreads,
5869
+ switchActiveInstance
5870
+ ]);
5569
5871
  const EMPTY_MESSAGES = React2.useRef([]);
5570
5872
  const getMessagesSnapshot = React2.useCallback(() => chatRef.current.messages, []);
5571
5873
  const getServerMessagesSnapshot = React2.useCallback(
@@ -5575,36 +5877,110 @@ function CopilotProvider(props) {
5575
5877
  const getStatusSnapshot = React2.useCallback(() => chatRef.current.status, []);
5576
5878
  const getErrorSnapshot = React2.useCallback(() => chatRef.current.error, []);
5577
5879
  const messages = React2.useSyncExternalStore(
5578
- chatRef.current.subscribe,
5880
+ stableSubscribe,
5579
5881
  getMessagesSnapshot,
5580
5882
  getServerMessagesSnapshot
5581
5883
  );
5582
5884
  const status = React2.useSyncExternalStore(
5583
- chatRef.current.subscribe,
5885
+ stableSubscribe,
5584
5886
  getStatusSnapshot,
5585
5887
  () => "ready"
5586
5888
  );
5587
5889
  const errorFromChat = React2.useSyncExternalStore(
5588
- chatRef.current.subscribe,
5890
+ stableSubscribe,
5589
5891
  getErrorSnapshot,
5590
5892
  () => void 0
5591
5893
  );
5592
5894
  const error = errorFromChat ?? null;
5593
5895
  const isLoading = status === "streaming" || status === "submitted";
5594
- const setActiveThread = React2.useCallback((id) => {
5595
- chatRef.current?.setActiveThread(id);
5596
- setActualThreadId(id ?? void 0);
5597
- }, []);
5896
+ const setActiveThread = React2.useCallback(
5897
+ (id, opts) => {
5898
+ if (concurrentThreads) {
5899
+ switchActiveInstance(id, opts);
5900
+ } else {
5901
+ chatRef.current?.setActiveThread(id);
5902
+ setActualThreadId(id ?? void 0);
5903
+ }
5904
+ },
5905
+ [concurrentThreads, switchActiveInstance]
5906
+ );
5907
+ const disposeThreadInstance = React2.useCallback(
5908
+ (id) => {
5909
+ if (!concurrentThreads) return;
5910
+ const inst = instancesRef.current.get(id);
5911
+ if (!inst) return;
5912
+ inst.dispose();
5913
+ instancesRef.current.delete(id);
5914
+ if (activeInstanceKeyRef.current === id) {
5915
+ switchActiveInstance(null);
5916
+ } else {
5917
+ recomputeBusyThreadIds();
5918
+ recomputePendingApprovalsByThread();
5919
+ }
5920
+ },
5921
+ [
5922
+ concurrentThreads,
5923
+ switchActiveInstance,
5924
+ recomputeBusyThreadIds,
5925
+ recomputePendingApprovalsByThread
5926
+ ]
5927
+ );
5928
+ const assignLocalThreadId = React2.useCallback(
5929
+ (localId) => {
5930
+ if (!concurrentThreads) return;
5931
+ if (!localId) return;
5932
+ const currentKey = activeInstanceKeyRef.current;
5933
+ if (currentKey === localId) return;
5934
+ const currentInst = instancesRef.current.get(currentKey);
5935
+ if (!currentInst) return;
5936
+ const isInternalSlot = currentKey === SINGLE_INSTANCE_KEY || currentKey.startsWith("__pending_");
5937
+ if (!isInternalSlot) return;
5938
+ if (instancesRef.current.has(localId)) return;
5939
+ instancesRef.current.delete(currentKey);
5940
+ instancesRef.current.set(localId, currentInst);
5941
+ activeInstanceKeyRef.current = localId;
5942
+ if (currentInst === chatRef.current) {
5943
+ setActualThreadId(localId);
5944
+ callbacksRef.current.onThreadChange?.(localId);
5945
+ }
5946
+ recomputeBusyThreadIds();
5947
+ recomputePendingApprovalsByThread();
5948
+ debugLog("Assigned local thread id", { localId });
5949
+ },
5950
+ [
5951
+ concurrentThreads,
5952
+ debugLog,
5953
+ recomputeBusyThreadIds,
5954
+ recomputePendingApprovalsByThread
5955
+ ]
5956
+ );
5598
5957
  const renewSession = React2.useCallback(() => {
5599
5958
  chatRef.current?.renewSession();
5600
5959
  setActualThreadId(void 0);
5601
5960
  setSessionStatus("idle");
5602
5961
  }, []);
5603
5962
  const registerTool = React2.useCallback((tool) => {
5604
- chatRef.current?.registerTool(tool);
5963
+ const existing = sharedToolsRef.current.get(tool.name);
5964
+ if (existing) {
5965
+ existing.tool = tool;
5966
+ existing.refCount++;
5967
+ } else {
5968
+ sharedToolsRef.current.set(tool.name, { tool, refCount: 1 });
5969
+ }
5970
+ for (const inst of instancesRef.current.values()) {
5971
+ inst.registerTool(tool);
5972
+ }
5605
5973
  }, []);
5606
5974
  const unregisterTool = React2.useCallback((name) => {
5607
- chatRef.current?.unregisterTool(name);
5975
+ const entry = sharedToolsRef.current.get(name);
5976
+ if (!entry) return;
5977
+ entry.refCount = Math.max(0, entry.refCount - 1);
5978
+ for (const inst of instancesRef.current.values()) {
5979
+ inst.unregisterTool(name);
5980
+ }
5981
+ if (entry.refCount === 0) {
5982
+ sharedToolsRef.current.delete(name);
5983
+ }
5608
5984
  }, []);
5609
5985
  const approveToolExecution = React2.useCallback(
5610
5986
  (id, extraData, permissionLevel) => {
@@ -5655,6 +6031,7 @@ function CopilotProvider(props) {
5655
6031
  parentId
5656
6032
  );
5657
6033
  const contextString = printTree(contextTreeRef.current);
6034
+ sharedSystemContextRef.current = contextString;
5658
6035
  chatRef.current?.setContext(contextString);
5659
6036
  setContextChars(contextString.length);
5660
6037
  debugLog("Context added:", id);
@@ -5666,6 +6043,7 @@ function CopilotProvider(props) {
5666
6043
  (id) => {
5667
6044
  contextTreeRef.current = removeNode(contextTreeRef.current, id);
5668
6045
  const contextString = printTree(contextTreeRef.current);
6046
+ sharedSystemContextRef.current = contextString;
5669
6047
  chatRef.current?.setContext(contextString);
5670
6048
  setContextChars(contextString.length);
5671
6049
  debugLog("Context removed:", id);
@@ -5674,14 +6052,19 @@ function CopilotProvider(props) {
5674
6052
  );
5675
6053
  const setSystemPrompt = React2.useCallback(
5676
6054
  (prompt) => {
5677
- chatRef.current?.setSystemPrompt(prompt);
6055
+ for (const inst of instancesRef.current.values()) {
6056
+ inst.setSystemPrompt(prompt);
6057
+ }
5678
6058
  debugLog("System prompt updated via function");
5679
6059
  },
5680
6060
  [debugLog]
5681
6061
  );
5682
6062
  const setInlineSkills = React2.useCallback(
5683
6063
  (skills2) => {
5684
- chatRef.current?.setInlineSkills(skills2);
6064
+ sharedSkillsRef.current = skills2;
6065
+ for (const inst of instancesRef.current.values()) {
6066
+ inst.setInlineSkills(skills2);
6067
+ }
5685
6068
  debugLog("Inline skills updated", { count: skills2.length });
5686
6069
  },
5687
6070
  [debugLog]
@@ -5729,7 +6112,7 @@ function CopilotProvider(props) {
5729
6112
  []
5730
6113
  );
5731
6114
  const hasBranches = React2.useSyncExternalStore(
5732
- chatRef.current.subscribe,
6115
+ stableSubscribe,
5733
6116
  getHasBranchesSnapshot,
5734
6117
  () => false
5735
6118
  );
@@ -5764,7 +6147,9 @@ function CopilotProvider(props) {
5764
6147
  }, [error, onError]);
5765
6148
  React2.useEffect(() => {
5766
6149
  return () => {
5767
- chatRef.current?.dispose();
6150
+ for (const inst of instancesRef.current.values()) {
6151
+ inst.dispose();
6152
+ }
5768
6153
  };
5769
6154
  }, []);
5770
6155
  const contextValue = React2.useMemo(
@@ -5818,7 +6203,13 @@ function CopilotProvider(props) {
5818
6203
  toolsConfig,
5819
6204
  // Headless primitives
5820
6205
  subscribeToStreamEvents,
5821
- messageMeta: messageMetaStoreRef.current
6206
+ messageMeta: messageMetaStoreRef.current,
6207
+ // Multi-thread streaming
6208
+ concurrentThreads,
6209
+ busyThreadIds,
6210
+ pendingApprovalsByThread,
6211
+ disposeThreadInstance,
6212
+ assignLocalThreadId
5822
6213
  }),
5823
6214
  [
5824
6215
  messages,
@@ -5858,7 +6249,12 @@ function CopilotProvider(props) {
5858
6249
  renewSession,
5859
6250
  sessionStatus,
5860
6251
  runtimeUrl,
5861
- toolsConfig
6252
+ toolsConfig,
6253
+ concurrentThreads,
6254
+ busyThreadIds,
6255
+ pendingApprovalsByThread,
6256
+ disposeThreadInstance,
6257
+ assignLocalThreadId
5862
6258
  ]
5863
6259
  );
5864
6260
  const messageHistoryContextValue = React2__default.default.useMemo(
@@ -5988,5 +6384,5 @@ exports.useMessageHistoryContext = useMessageHistoryContext;
5988
6384
  exports.useSkillContext = useSkillContext;
5989
6385
  exports.useTool = useTool;
5990
6386
  exports.useTools = useTools;
5991
- //# sourceMappingURL=chunk-ZYLHGNIG.cjs.map
5992
- //# sourceMappingURL=chunk-ZYLHGNIG.cjs.map
6387
+ //# sourceMappingURL=chunk-GW5ZZHJ7.cjs.map
6388
+ //# sourceMappingURL=chunk-GW5ZZHJ7.cjs.map