@yushaw/sanqian-chat 0.2.25 → 0.2.30

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.
@@ -74,7 +74,8 @@ interface ChatUiConfigSerializable {
74
74
  /** Font size scale: small (13px), normal (14px), large (16px), extra-large (18px) */
75
75
  fontSize?: ChatFontSize;
76
76
  accentColor?: string;
77
- locale?: Locale;
77
+ /** Supports Locale plus BCP-47 variants (e.g. zh-CN, en-US) */
78
+ locale?: Locale | string;
78
79
  strings?: Partial<ChatUiStrings>;
79
80
  alwaysOnTop?: boolean;
80
81
  }
@@ -74,7 +74,8 @@ interface ChatUiConfigSerializable {
74
74
  /** Font size scale: small (13px), normal (14px), large (16px), extra-large (18px) */
75
75
  fontSize?: ChatFontSize;
76
76
  accentColor?: string;
77
- locale?: Locale;
77
+ /** Supports Locale plus BCP-47 variants (e.g. zh-CN, en-US) */
78
+ locale?: Locale | string;
78
79
  strings?: Partial<ChatUiStrings>;
79
80
  alwaysOnTop?: boolean;
80
81
  }
@@ -507,7 +507,8 @@ interface ChatUiConfigSerializable {
507
507
  /** Font size scale: small (13px), normal (14px), large (16px), extra-large (18px) */
508
508
  fontSize?: ChatFontSize;
509
509
  accentColor?: string;
510
- locale?: Locale;
510
+ /** Supports Locale plus BCP-47 variants (e.g. zh-CN, en-US) */
511
+ locale?: Locale | string;
511
512
  strings?: Partial<ChatUiStrings>;
512
513
  alwaysOnTop?: boolean;
513
514
  }
@@ -922,7 +923,8 @@ declare class ChatPanel {
922
923
  private saveState;
923
924
  private getSdk;
924
925
  /**
925
- * Resolve HITL runId. Prefer explicit runId from renderer; fallback to latest active stream runId.
926
+ * Resolve HITL runId with deterministic binding for concurrent streams.
927
+ * Priority: explicit runId -> streamId-bound runId -> single active stream fallback.
926
928
  */
927
929
  private resolveHitlRunId;
928
930
  /**
@@ -507,7 +507,8 @@ interface ChatUiConfigSerializable {
507
507
  /** Font size scale: small (13px), normal (14px), large (16px), extra-large (18px) */
508
508
  fontSize?: ChatFontSize;
509
509
  accentColor?: string;
510
- locale?: Locale;
510
+ /** Supports Locale plus BCP-47 variants (e.g. zh-CN, en-US) */
511
+ locale?: Locale | string;
511
512
  strings?: Partial<ChatUiStrings>;
512
513
  alwaysOnTop?: boolean;
513
514
  }
@@ -922,7 +923,8 @@ declare class ChatPanel {
922
923
  private saveState;
923
924
  private getSdk;
924
925
  /**
925
- * Resolve HITL runId. Prefer explicit runId from renderer; fallback to latest active stream runId.
926
+ * Resolve HITL runId with deterministic binding for concurrent streams.
927
+ * Priority: explicit runId -> streamId-bound runId -> single active stream fallback.
926
928
  */
927
929
  private resolveHitlRunId;
928
930
  /**
@@ -366,6 +366,65 @@ var import_electron = require("electron");
366
366
  var import_fs = __toESM(require("fs"));
367
367
  var import_os = __toESM(require("os"));
368
368
  var import_path = __toESM(require("path"));
369
+
370
+ // src/main/hitl.ts
371
+ function resolveHitlRunIdFromStreams(params, activeStreams) {
372
+ if (params.runId) {
373
+ return params.runId;
374
+ }
375
+ if (params.streamId) {
376
+ const stream = activeStreams.get(params.streamId);
377
+ if (!stream || stream.cancelled) {
378
+ return null;
379
+ }
380
+ return stream.runId ?? null;
381
+ }
382
+ const activeRunIds = /* @__PURE__ */ new Set();
383
+ for (const stream of activeStreams.values()) {
384
+ if (stream.cancelled || !stream.runId) continue;
385
+ activeRunIds.add(stream.runId);
386
+ if (activeRunIds.size > 1) {
387
+ return null;
388
+ }
389
+ }
390
+ const first = activeRunIds.values().next();
391
+ return first.done ? null : first.value;
392
+ }
393
+ function resolveOwnedHitlRunIdFromStreams(params, activeStreams, senderWebContentsId) {
394
+ if (params.streamId) {
395
+ const stream = activeStreams.get(params.streamId);
396
+ if (!stream || stream.cancelled) {
397
+ return null;
398
+ }
399
+ if (stream.ownerWebContentsId !== senderWebContentsId) {
400
+ return null;
401
+ }
402
+ return stream.runId ?? null;
403
+ }
404
+ if (params.runId) {
405
+ for (const stream of activeStreams.values()) {
406
+ if (stream.cancelled || !stream.runId) continue;
407
+ if (stream.ownerWebContentsId !== senderWebContentsId) continue;
408
+ if (stream.runId === params.runId) {
409
+ return params.runId;
410
+ }
411
+ }
412
+ return null;
413
+ }
414
+ const activeRunIds = /* @__PURE__ */ new Set();
415
+ for (const stream of activeStreams.values()) {
416
+ if (stream.cancelled || !stream.runId) continue;
417
+ if (stream.ownerWebContentsId !== senderWebContentsId) continue;
418
+ activeRunIds.add(stream.runId);
419
+ if (activeRunIds.size > 1) {
420
+ return null;
421
+ }
422
+ }
423
+ const first = activeRunIds.values().next();
424
+ return first.done ? null : first.value;
425
+ }
426
+
427
+ // src/main/FloatingWindow.ts
369
428
  var ipcHandlersRegistered = false;
370
429
  var activeInstance = null;
371
430
  var setActiveInstance = (instance) => {
@@ -399,16 +458,11 @@ var FloatingWindow = class _FloatingWindow {
399
458
  import_electron.app.whenReady().then(() => this.registerShortcut());
400
459
  }
401
460
  }
402
- resolveHitlRunId(runId) {
403
- if (runId) return runId;
404
- const streams = Array.from(this.activeStreams.values());
405
- for (let i = streams.length - 1; i >= 0; i -= 1) {
406
- const stream = streams[i];
407
- if (stream && stream.runId && !stream.cancelled) {
408
- return stream.runId;
409
- }
461
+ resolveHitlRunId(params, senderWebContentsId) {
462
+ if (typeof senderWebContentsId === "number") {
463
+ return resolveOwnedHitlRunIdFromStreams(params, this.activeStreams, senderWebContentsId);
410
464
  }
411
- return null;
465
+ return resolveHitlRunIdFromStreams(params, this.activeStreams);
412
466
  }
413
467
  /**
414
468
  * Get SDK instance from either getClient or getSdk
@@ -650,7 +704,13 @@ var FloatingWindow = class _FloatingWindow {
650
704
  webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: "SDK or agent not ready" } });
651
705
  return;
652
706
  }
653
- const streamState = { cancelled: false, runId: null, cancelSignalSent: false };
707
+ const streamState = {
708
+ cancelled: false,
709
+ runId: null,
710
+ cancelSignalSent: false,
711
+ pendingHitlResponse: null,
712
+ ownerWebContentsId: webContents.id
713
+ };
654
714
  activeInstance?.activeStreams.set(streamId, streamState);
655
715
  try {
656
716
  await sdk.ensureReady();
@@ -665,6 +725,14 @@ var FloatingWindow = class _FloatingWindow {
665
725
  if (evtWithRunId.run_id && !streamState.runId) {
666
726
  streamState.runId = evtWithRunId.run_id;
667
727
  }
728
+ if (streamState.pendingHitlResponse && streamState.runId) {
729
+ try {
730
+ sdk.sendHitlResponse(streamState.runId, streamState.pendingHitlResponse);
731
+ streamState.pendingHitlResponse = null;
732
+ } catch (e) {
733
+ console.warn("[FloatingWindow] Failed to flush queued HITL response:", e);
734
+ }
735
+ }
668
736
  if (streamState.cancelled && streamState.runId && !streamState.cancelSignalSent) {
669
737
  try {
670
738
  sdk.cancelRun(streamState.runId);
@@ -726,31 +794,50 @@ var FloatingWindow = class _FloatingWindow {
726
794
  activeInstance?.activeStreams.delete(streamId);
727
795
  }
728
796
  });
729
- import_electron.ipcMain.handle("sanqian-chat:cancelStream", (_, params) => {
797
+ import_electron.ipcMain.handle("sanqian-chat:cancelStream", (event, params) => {
730
798
  const stream = activeInstance?.activeStreams.get(params.streamId);
731
- if (stream) {
732
- stream.cancelled = true;
733
- if (stream.runId && !stream.cancelSignalSent) {
734
- const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
735
- if (sdk) {
736
- try {
737
- sdk.cancelRun(stream.runId);
738
- stream.cancelSignalSent = true;
739
- } catch (e) {
740
- console.warn("[FloatingWindow] Failed to cancel run:", e);
741
- }
799
+ if (!stream) {
800
+ return { success: false, error: "stream_not_found" };
801
+ }
802
+ if (stream.ownerWebContentsId !== event.sender.id) {
803
+ console.warn("[FloatingWindow] Rejecting cancelStream from non-owner sender");
804
+ return { success: false, error: "stream_not_owned_by_sender" };
805
+ }
806
+ stream.cancelled = true;
807
+ if (stream.runId && !stream.cancelSignalSent) {
808
+ const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
809
+ if (sdk) {
810
+ try {
811
+ sdk.cancelRun(stream.runId);
812
+ stream.cancelSignalSent = true;
813
+ } catch (e) {
814
+ console.warn("[FloatingWindow] Failed to cancel run:", e);
742
815
  }
743
816
  }
744
817
  }
745
818
  return { success: true };
746
819
  });
747
- import_electron.ipcMain.handle("sanqian-chat:hitlResponse", (_, params) => {
820
+ import_electron.ipcMain.handle("sanqian-chat:hitlResponse", (event, params) => {
748
821
  const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
749
- const runId = activeInstance?.resolveHitlRunId(params.runId) ?? null;
822
+ const senderWebContentsId = event.sender.id;
823
+ const runId = activeInstance?.resolveHitlRunId(
824
+ { runId: params.runId, streamId: params.streamId },
825
+ senderWebContentsId
826
+ ) ?? null;
750
827
  if (sdk && runId) {
751
828
  sdk.sendHitlResponse(runId, params.response);
829
+ } else if (params.streamId) {
830
+ const stream = activeInstance?.activeStreams.get(params.streamId);
831
+ if (stream && !stream.cancelled && stream.ownerWebContentsId === senderWebContentsId) {
832
+ stream.pendingHitlResponse = params.response;
833
+ if (activeInstance?.options.devMode) {
834
+ console.warn("[FloatingWindow] Queued HITL response while waiting for runId");
835
+ }
836
+ } else {
837
+ console.warn("[FloatingWindow] HITL response dropped: stream not found/cancelled/not-owned");
838
+ }
752
839
  } else if (activeInstance?.options.devMode) {
753
- console.warn("[FloatingWindow] HITL response dropped: missing runId");
840
+ console.warn("[FloatingWindow] HITL response dropped: missing or unauthorized runId");
754
841
  }
755
842
  return { success: true };
756
843
  });
@@ -1700,18 +1787,14 @@ var ChatPanel = class {
1700
1787
  return client;
1701
1788
  }
1702
1789
  /**
1703
- * Resolve HITL runId. Prefer explicit runId from renderer; fallback to latest active stream runId.
1790
+ * Resolve HITL runId with deterministic binding for concurrent streams.
1791
+ * Priority: explicit runId -> streamId-bound runId -> single active stream fallback.
1704
1792
  */
1705
- resolveHitlRunId(runId) {
1706
- if (runId) return runId;
1707
- const streams = Array.from(this.activeStreams.values());
1708
- for (let i = streams.length - 1; i >= 0; i -= 1) {
1709
- const stream = streams[i];
1710
- if (stream && stream.runId && !stream.cancelled) {
1711
- return stream.runId;
1712
- }
1793
+ resolveHitlRunId(params, senderWebContentsId) {
1794
+ if (typeof senderWebContentsId === "number") {
1795
+ return resolveOwnedHitlRunIdFromStreams(params, this.activeStreams, senderWebContentsId);
1713
1796
  }
1714
- return null;
1797
+ return resolveHitlRunIdFromStreams(params, this.activeStreams);
1715
1798
  }
1716
1799
  /**
1717
1800
  * Setup session resource event forwarding from SDK to renderer
@@ -1774,7 +1857,13 @@ var ChatPanel = class {
1774
1857
  webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: "SDK or agent not ready" } });
1775
1858
  return;
1776
1859
  }
1777
- const streamState = { cancelled: false, runId: null, cancelSignalSent: false };
1860
+ const streamState = {
1861
+ cancelled: false,
1862
+ runId: null,
1863
+ cancelSignalSent: false,
1864
+ pendingHitlResponse: null,
1865
+ ownerWebContentsId: webContents.id
1866
+ };
1778
1867
  activeInstance2?.activeStreams.set(streamId, streamState);
1779
1868
  try {
1780
1869
  await sdk.ensureReady();
@@ -1794,6 +1883,14 @@ var ChatPanel = class {
1794
1883
  if (evtWithRunId.run_id && !streamState.runId) {
1795
1884
  streamState.runId = evtWithRunId.run_id;
1796
1885
  }
1886
+ if (streamState.pendingHitlResponse && streamState.runId) {
1887
+ try {
1888
+ sdk.sendHitlResponse(streamState.runId, streamState.pendingHitlResponse);
1889
+ streamState.pendingHitlResponse = null;
1890
+ } catch (e) {
1891
+ console.warn("[ChatPanel] Failed to flush queued HITL response:", e);
1892
+ }
1893
+ }
1797
1894
  if (streamState.cancelled && streamState.runId && !streamState.cancelSignalSent) {
1798
1895
  try {
1799
1896
  sdk.cancelRun(streamState.runId);
@@ -1855,31 +1952,50 @@ var ChatPanel = class {
1855
1952
  activeInstance2?.activeStreams.delete(streamId);
1856
1953
  }
1857
1954
  });
1858
- import_electron2.ipcMain.handle("sanqian-chat:cancelStream", (_, params) => {
1955
+ import_electron2.ipcMain.handle("sanqian-chat:cancelStream", (event, params) => {
1859
1956
  const stream = activeInstance2?.activeStreams.get(params.streamId);
1860
- if (stream) {
1861
- stream.cancelled = true;
1862
- if (stream.runId && !stream.cancelSignalSent) {
1863
- const sdk = activeInstance2?.getSdk();
1864
- if (sdk) {
1865
- try {
1866
- sdk.cancelRun(stream.runId);
1867
- stream.cancelSignalSent = true;
1868
- } catch (e) {
1869
- console.warn("[ChatPanel] Failed to cancel run:", e);
1870
- }
1957
+ if (!stream) {
1958
+ return { success: false, error: "stream_not_found" };
1959
+ }
1960
+ if (stream.ownerWebContentsId !== event.sender.id) {
1961
+ console.warn("[ChatPanel] Rejecting cancelStream from non-owner sender");
1962
+ return { success: false, error: "stream_not_owned_by_sender" };
1963
+ }
1964
+ stream.cancelled = true;
1965
+ if (stream.runId && !stream.cancelSignalSent) {
1966
+ const sdk = activeInstance2?.getSdk();
1967
+ if (sdk) {
1968
+ try {
1969
+ sdk.cancelRun(stream.runId);
1970
+ stream.cancelSignalSent = true;
1971
+ } catch (e) {
1972
+ console.warn("[ChatPanel] Failed to cancel run:", e);
1871
1973
  }
1872
1974
  }
1873
1975
  }
1874
1976
  return { success: true };
1875
1977
  });
1876
- import_electron2.ipcMain.handle("sanqian-chat:hitlResponse", (_, params) => {
1978
+ import_electron2.ipcMain.handle("sanqian-chat:hitlResponse", (event, params) => {
1877
1979
  const sdk = activeInstance2?.getSdk();
1878
- const runId = activeInstance2?.resolveHitlRunId(params.runId) ?? null;
1980
+ const senderWebContentsId = event.sender.id;
1981
+ const runId = activeInstance2?.resolveHitlRunId(
1982
+ { runId: params.runId, streamId: params.streamId },
1983
+ senderWebContentsId
1984
+ ) ?? null;
1879
1985
  if (sdk && runId) {
1880
1986
  sdk.sendHitlResponse(runId, params.response);
1987
+ } else if (params.streamId) {
1988
+ const stream = activeInstance2?.activeStreams.get(params.streamId);
1989
+ if (stream && !stream.cancelled && stream.ownerWebContentsId === senderWebContentsId) {
1990
+ stream.pendingHitlResponse = params.response;
1991
+ if (activeInstance2?.config.devMode) {
1992
+ console.warn("[ChatPanel] Queued HITL response while waiting for runId");
1993
+ }
1994
+ } else {
1995
+ console.warn("[ChatPanel] HITL response dropped: stream not found/cancelled/not-owned");
1996
+ }
1881
1997
  } else if (activeInstance2?.config.devMode) {
1882
- console.warn("[ChatPanel] HITL response dropped: missing runId");
1998
+ console.warn("[ChatPanel] HITL response dropped: missing or unauthorized runId");
1883
1999
  }
1884
2000
  return { success: true };
1885
2001
  });
@@ -335,6 +335,65 @@ import { BrowserWindow, globalShortcut, screen, ipcMain, app } from "electron";
335
335
  import fs from "fs";
336
336
  import os from "os";
337
337
  import path from "path";
338
+
339
+ // src/main/hitl.ts
340
+ function resolveHitlRunIdFromStreams(params, activeStreams) {
341
+ if (params.runId) {
342
+ return params.runId;
343
+ }
344
+ if (params.streamId) {
345
+ const stream = activeStreams.get(params.streamId);
346
+ if (!stream || stream.cancelled) {
347
+ return null;
348
+ }
349
+ return stream.runId ?? null;
350
+ }
351
+ const activeRunIds = /* @__PURE__ */ new Set();
352
+ for (const stream of activeStreams.values()) {
353
+ if (stream.cancelled || !stream.runId) continue;
354
+ activeRunIds.add(stream.runId);
355
+ if (activeRunIds.size > 1) {
356
+ return null;
357
+ }
358
+ }
359
+ const first = activeRunIds.values().next();
360
+ return first.done ? null : first.value;
361
+ }
362
+ function resolveOwnedHitlRunIdFromStreams(params, activeStreams, senderWebContentsId) {
363
+ if (params.streamId) {
364
+ const stream = activeStreams.get(params.streamId);
365
+ if (!stream || stream.cancelled) {
366
+ return null;
367
+ }
368
+ if (stream.ownerWebContentsId !== senderWebContentsId) {
369
+ return null;
370
+ }
371
+ return stream.runId ?? null;
372
+ }
373
+ if (params.runId) {
374
+ for (const stream of activeStreams.values()) {
375
+ if (stream.cancelled || !stream.runId) continue;
376
+ if (stream.ownerWebContentsId !== senderWebContentsId) continue;
377
+ if (stream.runId === params.runId) {
378
+ return params.runId;
379
+ }
380
+ }
381
+ return null;
382
+ }
383
+ const activeRunIds = /* @__PURE__ */ new Set();
384
+ for (const stream of activeStreams.values()) {
385
+ if (stream.cancelled || !stream.runId) continue;
386
+ if (stream.ownerWebContentsId !== senderWebContentsId) continue;
387
+ activeRunIds.add(stream.runId);
388
+ if (activeRunIds.size > 1) {
389
+ return null;
390
+ }
391
+ }
392
+ const first = activeRunIds.values().next();
393
+ return first.done ? null : first.value;
394
+ }
395
+
396
+ // src/main/FloatingWindow.ts
338
397
  var ipcHandlersRegistered = false;
339
398
  var activeInstance = null;
340
399
  var setActiveInstance = (instance) => {
@@ -368,16 +427,11 @@ var FloatingWindow = class _FloatingWindow {
368
427
  app.whenReady().then(() => this.registerShortcut());
369
428
  }
370
429
  }
371
- resolveHitlRunId(runId) {
372
- if (runId) return runId;
373
- const streams = Array.from(this.activeStreams.values());
374
- for (let i = streams.length - 1; i >= 0; i -= 1) {
375
- const stream = streams[i];
376
- if (stream && stream.runId && !stream.cancelled) {
377
- return stream.runId;
378
- }
430
+ resolveHitlRunId(params, senderWebContentsId) {
431
+ if (typeof senderWebContentsId === "number") {
432
+ return resolveOwnedHitlRunIdFromStreams(params, this.activeStreams, senderWebContentsId);
379
433
  }
380
- return null;
434
+ return resolveHitlRunIdFromStreams(params, this.activeStreams);
381
435
  }
382
436
  /**
383
437
  * Get SDK instance from either getClient or getSdk
@@ -619,7 +673,13 @@ var FloatingWindow = class _FloatingWindow {
619
673
  webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: "SDK or agent not ready" } });
620
674
  return;
621
675
  }
622
- const streamState = { cancelled: false, runId: null, cancelSignalSent: false };
676
+ const streamState = {
677
+ cancelled: false,
678
+ runId: null,
679
+ cancelSignalSent: false,
680
+ pendingHitlResponse: null,
681
+ ownerWebContentsId: webContents.id
682
+ };
623
683
  activeInstance?.activeStreams.set(streamId, streamState);
624
684
  try {
625
685
  await sdk.ensureReady();
@@ -634,6 +694,14 @@ var FloatingWindow = class _FloatingWindow {
634
694
  if (evtWithRunId.run_id && !streamState.runId) {
635
695
  streamState.runId = evtWithRunId.run_id;
636
696
  }
697
+ if (streamState.pendingHitlResponse && streamState.runId) {
698
+ try {
699
+ sdk.sendHitlResponse(streamState.runId, streamState.pendingHitlResponse);
700
+ streamState.pendingHitlResponse = null;
701
+ } catch (e) {
702
+ console.warn("[FloatingWindow] Failed to flush queued HITL response:", e);
703
+ }
704
+ }
637
705
  if (streamState.cancelled && streamState.runId && !streamState.cancelSignalSent) {
638
706
  try {
639
707
  sdk.cancelRun(streamState.runId);
@@ -695,31 +763,50 @@ var FloatingWindow = class _FloatingWindow {
695
763
  activeInstance?.activeStreams.delete(streamId);
696
764
  }
697
765
  });
698
- ipcMain.handle("sanqian-chat:cancelStream", (_, params) => {
766
+ ipcMain.handle("sanqian-chat:cancelStream", (event, params) => {
699
767
  const stream = activeInstance?.activeStreams.get(params.streamId);
700
- if (stream) {
701
- stream.cancelled = true;
702
- if (stream.runId && !stream.cancelSignalSent) {
703
- const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
704
- if (sdk) {
705
- try {
706
- sdk.cancelRun(stream.runId);
707
- stream.cancelSignalSent = true;
708
- } catch (e) {
709
- console.warn("[FloatingWindow] Failed to cancel run:", e);
710
- }
768
+ if (!stream) {
769
+ return { success: false, error: "stream_not_found" };
770
+ }
771
+ if (stream.ownerWebContentsId !== event.sender.id) {
772
+ console.warn("[FloatingWindow] Rejecting cancelStream from non-owner sender");
773
+ return { success: false, error: "stream_not_owned_by_sender" };
774
+ }
775
+ stream.cancelled = true;
776
+ if (stream.runId && !stream.cancelSignalSent) {
777
+ const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
778
+ if (sdk) {
779
+ try {
780
+ sdk.cancelRun(stream.runId);
781
+ stream.cancelSignalSent = true;
782
+ } catch (e) {
783
+ console.warn("[FloatingWindow] Failed to cancel run:", e);
711
784
  }
712
785
  }
713
786
  }
714
787
  return { success: true };
715
788
  });
716
- ipcMain.handle("sanqian-chat:hitlResponse", (_, params) => {
789
+ ipcMain.handle("sanqian-chat:hitlResponse", (event, params) => {
717
790
  const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
718
- const runId = activeInstance?.resolveHitlRunId(params.runId) ?? null;
791
+ const senderWebContentsId = event.sender.id;
792
+ const runId = activeInstance?.resolveHitlRunId(
793
+ { runId: params.runId, streamId: params.streamId },
794
+ senderWebContentsId
795
+ ) ?? null;
719
796
  if (sdk && runId) {
720
797
  sdk.sendHitlResponse(runId, params.response);
798
+ } else if (params.streamId) {
799
+ const stream = activeInstance?.activeStreams.get(params.streamId);
800
+ if (stream && !stream.cancelled && stream.ownerWebContentsId === senderWebContentsId) {
801
+ stream.pendingHitlResponse = params.response;
802
+ if (activeInstance?.options.devMode) {
803
+ console.warn("[FloatingWindow] Queued HITL response while waiting for runId");
804
+ }
805
+ } else {
806
+ console.warn("[FloatingWindow] HITL response dropped: stream not found/cancelled/not-owned");
807
+ }
721
808
  } else if (activeInstance?.options.devMode) {
722
- console.warn("[FloatingWindow] HITL response dropped: missing runId");
809
+ console.warn("[FloatingWindow] HITL response dropped: missing or unauthorized runId");
723
810
  }
724
811
  return { success: true };
725
812
  });
@@ -1675,18 +1762,14 @@ var ChatPanel = class {
1675
1762
  return client;
1676
1763
  }
1677
1764
  /**
1678
- * Resolve HITL runId. Prefer explicit runId from renderer; fallback to latest active stream runId.
1765
+ * Resolve HITL runId with deterministic binding for concurrent streams.
1766
+ * Priority: explicit runId -> streamId-bound runId -> single active stream fallback.
1679
1767
  */
1680
- resolveHitlRunId(runId) {
1681
- if (runId) return runId;
1682
- const streams = Array.from(this.activeStreams.values());
1683
- for (let i = streams.length - 1; i >= 0; i -= 1) {
1684
- const stream = streams[i];
1685
- if (stream && stream.runId && !stream.cancelled) {
1686
- return stream.runId;
1687
- }
1768
+ resolveHitlRunId(params, senderWebContentsId) {
1769
+ if (typeof senderWebContentsId === "number") {
1770
+ return resolveOwnedHitlRunIdFromStreams(params, this.activeStreams, senderWebContentsId);
1688
1771
  }
1689
- return null;
1772
+ return resolveHitlRunIdFromStreams(params, this.activeStreams);
1690
1773
  }
1691
1774
  /**
1692
1775
  * Setup session resource event forwarding from SDK to renderer
@@ -1749,7 +1832,13 @@ var ChatPanel = class {
1749
1832
  webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: "SDK or agent not ready" } });
1750
1833
  return;
1751
1834
  }
1752
- const streamState = { cancelled: false, runId: null, cancelSignalSent: false };
1835
+ const streamState = {
1836
+ cancelled: false,
1837
+ runId: null,
1838
+ cancelSignalSent: false,
1839
+ pendingHitlResponse: null,
1840
+ ownerWebContentsId: webContents.id
1841
+ };
1753
1842
  activeInstance2?.activeStreams.set(streamId, streamState);
1754
1843
  try {
1755
1844
  await sdk.ensureReady();
@@ -1769,6 +1858,14 @@ var ChatPanel = class {
1769
1858
  if (evtWithRunId.run_id && !streamState.runId) {
1770
1859
  streamState.runId = evtWithRunId.run_id;
1771
1860
  }
1861
+ if (streamState.pendingHitlResponse && streamState.runId) {
1862
+ try {
1863
+ sdk.sendHitlResponse(streamState.runId, streamState.pendingHitlResponse);
1864
+ streamState.pendingHitlResponse = null;
1865
+ } catch (e) {
1866
+ console.warn("[ChatPanel] Failed to flush queued HITL response:", e);
1867
+ }
1868
+ }
1772
1869
  if (streamState.cancelled && streamState.runId && !streamState.cancelSignalSent) {
1773
1870
  try {
1774
1871
  sdk.cancelRun(streamState.runId);
@@ -1830,31 +1927,50 @@ var ChatPanel = class {
1830
1927
  activeInstance2?.activeStreams.delete(streamId);
1831
1928
  }
1832
1929
  });
1833
- ipcMain2.handle("sanqian-chat:cancelStream", (_, params) => {
1930
+ ipcMain2.handle("sanqian-chat:cancelStream", (event, params) => {
1834
1931
  const stream = activeInstance2?.activeStreams.get(params.streamId);
1835
- if (stream) {
1836
- stream.cancelled = true;
1837
- if (stream.runId && !stream.cancelSignalSent) {
1838
- const sdk = activeInstance2?.getSdk();
1839
- if (sdk) {
1840
- try {
1841
- sdk.cancelRun(stream.runId);
1842
- stream.cancelSignalSent = true;
1843
- } catch (e) {
1844
- console.warn("[ChatPanel] Failed to cancel run:", e);
1845
- }
1932
+ if (!stream) {
1933
+ return { success: false, error: "stream_not_found" };
1934
+ }
1935
+ if (stream.ownerWebContentsId !== event.sender.id) {
1936
+ console.warn("[ChatPanel] Rejecting cancelStream from non-owner sender");
1937
+ return { success: false, error: "stream_not_owned_by_sender" };
1938
+ }
1939
+ stream.cancelled = true;
1940
+ if (stream.runId && !stream.cancelSignalSent) {
1941
+ const sdk = activeInstance2?.getSdk();
1942
+ if (sdk) {
1943
+ try {
1944
+ sdk.cancelRun(stream.runId);
1945
+ stream.cancelSignalSent = true;
1946
+ } catch (e) {
1947
+ console.warn("[ChatPanel] Failed to cancel run:", e);
1846
1948
  }
1847
1949
  }
1848
1950
  }
1849
1951
  return { success: true };
1850
1952
  });
1851
- ipcMain2.handle("sanqian-chat:hitlResponse", (_, params) => {
1953
+ ipcMain2.handle("sanqian-chat:hitlResponse", (event, params) => {
1852
1954
  const sdk = activeInstance2?.getSdk();
1853
- const runId = activeInstance2?.resolveHitlRunId(params.runId) ?? null;
1955
+ const senderWebContentsId = event.sender.id;
1956
+ const runId = activeInstance2?.resolveHitlRunId(
1957
+ { runId: params.runId, streamId: params.streamId },
1958
+ senderWebContentsId
1959
+ ) ?? null;
1854
1960
  if (sdk && runId) {
1855
1961
  sdk.sendHitlResponse(runId, params.response);
1962
+ } else if (params.streamId) {
1963
+ const stream = activeInstance2?.activeStreams.get(params.streamId);
1964
+ if (stream && !stream.cancelled && stream.ownerWebContentsId === senderWebContentsId) {
1965
+ stream.pendingHitlResponse = params.response;
1966
+ if (activeInstance2?.config.devMode) {
1967
+ console.warn("[ChatPanel] Queued HITL response while waiting for runId");
1968
+ }
1969
+ } else {
1970
+ console.warn("[ChatPanel] HITL response dropped: stream not found/cancelled/not-owned");
1971
+ }
1856
1972
  } else if (activeInstance2?.config.devMode) {
1857
- console.warn("[ChatPanel] HITL response dropped: missing runId");
1973
+ console.warn("[ChatPanel] HITL response dropped: missing or unauthorized runId");
1858
1974
  }
1859
1975
  return { success: true };
1860
1976
  });