@yushaw/sanqian-chat 0.2.27 → 0.2.32

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
  }
@@ -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
  }
@@ -390,6 +390,39 @@ function resolveHitlRunIdFromStreams(params, activeStreams) {
390
390
  const first = activeRunIds.values().next();
391
391
  return first.done ? null : first.value;
392
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
+ }
393
426
 
394
427
  // src/main/FloatingWindow.ts
395
428
  var ipcHandlersRegistered = false;
@@ -425,7 +458,10 @@ var FloatingWindow = class _FloatingWindow {
425
458
  import_electron.app.whenReady().then(() => this.registerShortcut());
426
459
  }
427
460
  }
428
- resolveHitlRunId(params) {
461
+ resolveHitlRunId(params, senderWebContentsId) {
462
+ if (typeof senderWebContentsId === "number") {
463
+ return resolveOwnedHitlRunIdFromStreams(params, this.activeStreams, senderWebContentsId);
464
+ }
429
465
  return resolveHitlRunIdFromStreams(params, this.activeStreams);
430
466
  }
431
467
  /**
@@ -668,7 +704,13 @@ var FloatingWindow = class _FloatingWindow {
668
704
  webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: "SDK or agent not ready" } });
669
705
  return;
670
706
  }
671
- 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
+ };
672
714
  activeInstance?.activeStreams.set(streamId, streamState);
673
715
  try {
674
716
  await sdk.ensureReady();
@@ -683,6 +725,14 @@ var FloatingWindow = class _FloatingWindow {
683
725
  if (evtWithRunId.run_id && !streamState.runId) {
684
726
  streamState.runId = evtWithRunId.run_id;
685
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
+ }
686
736
  if (streamState.cancelled && streamState.runId && !streamState.cancelSignalSent) {
687
737
  try {
688
738
  sdk.cancelRun(streamState.runId);
@@ -744,31 +794,50 @@ var FloatingWindow = class _FloatingWindow {
744
794
  activeInstance?.activeStreams.delete(streamId);
745
795
  }
746
796
  });
747
- import_electron.ipcMain.handle("sanqian-chat:cancelStream", (_, params) => {
797
+ import_electron.ipcMain.handle("sanqian-chat:cancelStream", (event, params) => {
748
798
  const stream = activeInstance?.activeStreams.get(params.streamId);
749
- if (stream) {
750
- stream.cancelled = true;
751
- if (stream.runId && !stream.cancelSignalSent) {
752
- const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
753
- if (sdk) {
754
- try {
755
- sdk.cancelRun(stream.runId);
756
- stream.cancelSignalSent = true;
757
- } catch (e) {
758
- console.warn("[FloatingWindow] Failed to cancel run:", e);
759
- }
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);
760
815
  }
761
816
  }
762
817
  }
763
818
  return { success: true };
764
819
  });
765
- import_electron.ipcMain.handle("sanqian-chat:hitlResponse", (_, params) => {
820
+ import_electron.ipcMain.handle("sanqian-chat:hitlResponse", (event, params) => {
766
821
  const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
767
- const runId = activeInstance?.resolveHitlRunId({ runId: params.runId, streamId: params.streamId }) ?? null;
822
+ const senderWebContentsId = event.sender.id;
823
+ const runId = activeInstance?.resolveHitlRunId(
824
+ { runId: params.runId, streamId: params.streamId },
825
+ senderWebContentsId
826
+ ) ?? null;
768
827
  if (sdk && runId) {
769
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
+ }
770
839
  } else if (activeInstance?.options.devMode) {
771
- console.warn("[FloatingWindow] HITL response dropped: missing runId");
840
+ console.warn("[FloatingWindow] HITL response dropped: missing or unauthorized runId");
772
841
  }
773
842
  return { success: true };
774
843
  });
@@ -1721,7 +1790,10 @@ var ChatPanel = class {
1721
1790
  * Resolve HITL runId with deterministic binding for concurrent streams.
1722
1791
  * Priority: explicit runId -> streamId-bound runId -> single active stream fallback.
1723
1792
  */
1724
- resolveHitlRunId(params) {
1793
+ resolveHitlRunId(params, senderWebContentsId) {
1794
+ if (typeof senderWebContentsId === "number") {
1795
+ return resolveOwnedHitlRunIdFromStreams(params, this.activeStreams, senderWebContentsId);
1796
+ }
1725
1797
  return resolveHitlRunIdFromStreams(params, this.activeStreams);
1726
1798
  }
1727
1799
  /**
@@ -1785,7 +1857,13 @@ var ChatPanel = class {
1785
1857
  webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: "SDK or agent not ready" } });
1786
1858
  return;
1787
1859
  }
1788
- 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
+ };
1789
1867
  activeInstance2?.activeStreams.set(streamId, streamState);
1790
1868
  try {
1791
1869
  await sdk.ensureReady();
@@ -1805,6 +1883,14 @@ var ChatPanel = class {
1805
1883
  if (evtWithRunId.run_id && !streamState.runId) {
1806
1884
  streamState.runId = evtWithRunId.run_id;
1807
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
+ }
1808
1894
  if (streamState.cancelled && streamState.runId && !streamState.cancelSignalSent) {
1809
1895
  try {
1810
1896
  sdk.cancelRun(streamState.runId);
@@ -1866,31 +1952,50 @@ var ChatPanel = class {
1866
1952
  activeInstance2?.activeStreams.delete(streamId);
1867
1953
  }
1868
1954
  });
1869
- import_electron2.ipcMain.handle("sanqian-chat:cancelStream", (_, params) => {
1955
+ import_electron2.ipcMain.handle("sanqian-chat:cancelStream", (event, params) => {
1870
1956
  const stream = activeInstance2?.activeStreams.get(params.streamId);
1871
- if (stream) {
1872
- stream.cancelled = true;
1873
- if (stream.runId && !stream.cancelSignalSent) {
1874
- const sdk = activeInstance2?.getSdk();
1875
- if (sdk) {
1876
- try {
1877
- sdk.cancelRun(stream.runId);
1878
- stream.cancelSignalSent = true;
1879
- } catch (e) {
1880
- console.warn("[ChatPanel] Failed to cancel run:", e);
1881
- }
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);
1882
1973
  }
1883
1974
  }
1884
1975
  }
1885
1976
  return { success: true };
1886
1977
  });
1887
- import_electron2.ipcMain.handle("sanqian-chat:hitlResponse", (_, params) => {
1978
+ import_electron2.ipcMain.handle("sanqian-chat:hitlResponse", (event, params) => {
1888
1979
  const sdk = activeInstance2?.getSdk();
1889
- const runId = activeInstance2?.resolveHitlRunId({ runId: params.runId, streamId: params.streamId }) ?? null;
1980
+ const senderWebContentsId = event.sender.id;
1981
+ const runId = activeInstance2?.resolveHitlRunId(
1982
+ { runId: params.runId, streamId: params.streamId },
1983
+ senderWebContentsId
1984
+ ) ?? null;
1890
1985
  if (sdk && runId) {
1891
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
+ }
1892
1997
  } else if (activeInstance2?.config.devMode) {
1893
- console.warn("[ChatPanel] HITL response dropped: missing runId");
1998
+ console.warn("[ChatPanel] HITL response dropped: missing or unauthorized runId");
1894
1999
  }
1895
2000
  return { success: true };
1896
2001
  });
@@ -359,6 +359,39 @@ function resolveHitlRunIdFromStreams(params, activeStreams) {
359
359
  const first = activeRunIds.values().next();
360
360
  return first.done ? null : first.value;
361
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
+ }
362
395
 
363
396
  // src/main/FloatingWindow.ts
364
397
  var ipcHandlersRegistered = false;
@@ -394,7 +427,10 @@ var FloatingWindow = class _FloatingWindow {
394
427
  app.whenReady().then(() => this.registerShortcut());
395
428
  }
396
429
  }
397
- resolveHitlRunId(params) {
430
+ resolveHitlRunId(params, senderWebContentsId) {
431
+ if (typeof senderWebContentsId === "number") {
432
+ return resolveOwnedHitlRunIdFromStreams(params, this.activeStreams, senderWebContentsId);
433
+ }
398
434
  return resolveHitlRunIdFromStreams(params, this.activeStreams);
399
435
  }
400
436
  /**
@@ -637,7 +673,13 @@ var FloatingWindow = class _FloatingWindow {
637
673
  webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: "SDK or agent not ready" } });
638
674
  return;
639
675
  }
640
- 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
+ };
641
683
  activeInstance?.activeStreams.set(streamId, streamState);
642
684
  try {
643
685
  await sdk.ensureReady();
@@ -652,6 +694,14 @@ var FloatingWindow = class _FloatingWindow {
652
694
  if (evtWithRunId.run_id && !streamState.runId) {
653
695
  streamState.runId = evtWithRunId.run_id;
654
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
+ }
655
705
  if (streamState.cancelled && streamState.runId && !streamState.cancelSignalSent) {
656
706
  try {
657
707
  sdk.cancelRun(streamState.runId);
@@ -713,31 +763,50 @@ var FloatingWindow = class _FloatingWindow {
713
763
  activeInstance?.activeStreams.delete(streamId);
714
764
  }
715
765
  });
716
- ipcMain.handle("sanqian-chat:cancelStream", (_, params) => {
766
+ ipcMain.handle("sanqian-chat:cancelStream", (event, params) => {
717
767
  const stream = activeInstance?.activeStreams.get(params.streamId);
718
- if (stream) {
719
- stream.cancelled = true;
720
- if (stream.runId && !stream.cancelSignalSent) {
721
- const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
722
- if (sdk) {
723
- try {
724
- sdk.cancelRun(stream.runId);
725
- stream.cancelSignalSent = true;
726
- } catch (e) {
727
- console.warn("[FloatingWindow] Failed to cancel run:", e);
728
- }
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);
729
784
  }
730
785
  }
731
786
  }
732
787
  return { success: true };
733
788
  });
734
- ipcMain.handle("sanqian-chat:hitlResponse", (_, params) => {
789
+ ipcMain.handle("sanqian-chat:hitlResponse", (event, params) => {
735
790
  const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
736
- const runId = activeInstance?.resolveHitlRunId({ runId: params.runId, streamId: params.streamId }) ?? null;
791
+ const senderWebContentsId = event.sender.id;
792
+ const runId = activeInstance?.resolveHitlRunId(
793
+ { runId: params.runId, streamId: params.streamId },
794
+ senderWebContentsId
795
+ ) ?? null;
737
796
  if (sdk && runId) {
738
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
+ }
739
808
  } else if (activeInstance?.options.devMode) {
740
- console.warn("[FloatingWindow] HITL response dropped: missing runId");
809
+ console.warn("[FloatingWindow] HITL response dropped: missing or unauthorized runId");
741
810
  }
742
811
  return { success: true };
743
812
  });
@@ -1696,7 +1765,10 @@ var ChatPanel = class {
1696
1765
  * Resolve HITL runId with deterministic binding for concurrent streams.
1697
1766
  * Priority: explicit runId -> streamId-bound runId -> single active stream fallback.
1698
1767
  */
1699
- resolveHitlRunId(params) {
1768
+ resolveHitlRunId(params, senderWebContentsId) {
1769
+ if (typeof senderWebContentsId === "number") {
1770
+ return resolveOwnedHitlRunIdFromStreams(params, this.activeStreams, senderWebContentsId);
1771
+ }
1700
1772
  return resolveHitlRunIdFromStreams(params, this.activeStreams);
1701
1773
  }
1702
1774
  /**
@@ -1760,7 +1832,13 @@ var ChatPanel = class {
1760
1832
  webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: "SDK or agent not ready" } });
1761
1833
  return;
1762
1834
  }
1763
- 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
+ };
1764
1842
  activeInstance2?.activeStreams.set(streamId, streamState);
1765
1843
  try {
1766
1844
  await sdk.ensureReady();
@@ -1780,6 +1858,14 @@ var ChatPanel = class {
1780
1858
  if (evtWithRunId.run_id && !streamState.runId) {
1781
1859
  streamState.runId = evtWithRunId.run_id;
1782
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
+ }
1783
1869
  if (streamState.cancelled && streamState.runId && !streamState.cancelSignalSent) {
1784
1870
  try {
1785
1871
  sdk.cancelRun(streamState.runId);
@@ -1841,31 +1927,50 @@ var ChatPanel = class {
1841
1927
  activeInstance2?.activeStreams.delete(streamId);
1842
1928
  }
1843
1929
  });
1844
- ipcMain2.handle("sanqian-chat:cancelStream", (_, params) => {
1930
+ ipcMain2.handle("sanqian-chat:cancelStream", (event, params) => {
1845
1931
  const stream = activeInstance2?.activeStreams.get(params.streamId);
1846
- if (stream) {
1847
- stream.cancelled = true;
1848
- if (stream.runId && !stream.cancelSignalSent) {
1849
- const sdk = activeInstance2?.getSdk();
1850
- if (sdk) {
1851
- try {
1852
- sdk.cancelRun(stream.runId);
1853
- stream.cancelSignalSent = true;
1854
- } catch (e) {
1855
- console.warn("[ChatPanel] Failed to cancel run:", e);
1856
- }
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);
1857
1948
  }
1858
1949
  }
1859
1950
  }
1860
1951
  return { success: true };
1861
1952
  });
1862
- ipcMain2.handle("sanqian-chat:hitlResponse", (_, params) => {
1953
+ ipcMain2.handle("sanqian-chat:hitlResponse", (event, params) => {
1863
1954
  const sdk = activeInstance2?.getSdk();
1864
- const runId = activeInstance2?.resolveHitlRunId({ runId: params.runId, streamId: params.streamId }) ?? null;
1955
+ const senderWebContentsId = event.sender.id;
1956
+ const runId = activeInstance2?.resolveHitlRunId(
1957
+ { runId: params.runId, streamId: params.streamId },
1958
+ senderWebContentsId
1959
+ ) ?? null;
1865
1960
  if (sdk && runId) {
1866
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
+ }
1867
1972
  } else if (activeInstance2?.config.devMode) {
1868
- console.warn("[ChatPanel] HITL response dropped: missing runId");
1973
+ console.warn("[ChatPanel] HITL response dropped: missing or unauthorized runId");
1869
1974
  }
1870
1975
  return { success: true };
1871
1976
  });
@@ -72,7 +72,8 @@ interface ChatUiConfigSerializable {
72
72
  /** Font size scale: small (13px), normal (14px), large (16px), extra-large (18px) */
73
73
  fontSize?: ChatFontSize;
74
74
  accentColor?: string;
75
- locale?: Locale;
75
+ /** Supports Locale plus BCP-47 variants (e.g. zh-CN, en-US) */
76
+ locale?: Locale | string;
76
77
  strings?: Partial<ChatUiStrings>;
77
78
  alwaysOnTop?: boolean;
78
79
  }
@@ -72,7 +72,8 @@ interface ChatUiConfigSerializable {
72
72
  /** Font size scale: small (13px), normal (14px), large (16px), extra-large (18px) */
73
73
  fontSize?: ChatFontSize;
74
74
  accentColor?: string;
75
- locale?: Locale;
75
+ /** Supports Locale plus BCP-47 variants (e.g. zh-CN, en-US) */
76
+ locale?: Locale | string;
76
77
  strings?: Partial<ChatUiStrings>;
77
78
  alwaysOnTop?: boolean;
78
79
  }
@@ -77,7 +77,8 @@ interface ChatUiConfigSerializable {
77
77
  /** Font size scale: small (13px), normal (14px), large (16px), extra-large (18px) */
78
78
  fontSize?: ChatFontSize;
79
79
  accentColor?: string;
80
- locale?: Locale;
80
+ /** Supports Locale plus BCP-47 variants (e.g. zh-CN, en-US) */
81
+ locale?: Locale | string;
81
82
  strings?: Partial<ChatUiStrings>;
82
83
  alwaysOnTop?: boolean;
83
84
  }
@@ -1231,7 +1232,7 @@ interface HistoryModalProps {
1231
1232
  }
1232
1233
  declare const HistoryModal: react.NamedExoticComponent<HistoryModalProps>;
1233
1234
 
1234
- declare function resolveChatStrings(locale?: Locale, overrides?: Partial<ChatUiStrings>): ChatUiStrings;
1235
+ declare function resolveChatStrings(locale?: Locale | string, overrides?: Partial<ChatUiStrings>): ChatUiStrings;
1235
1236
 
1236
1237
  type AlertType = 'warning' | 'error';
1237
1238
  interface AlertAction {
@@ -77,7 +77,8 @@ interface ChatUiConfigSerializable {
77
77
  /** Font size scale: small (13px), normal (14px), large (16px), extra-large (18px) */
78
78
  fontSize?: ChatFontSize;
79
79
  accentColor?: string;
80
- locale?: Locale;
80
+ /** Supports Locale plus BCP-47 variants (e.g. zh-CN, en-US) */
81
+ locale?: Locale | string;
81
82
  strings?: Partial<ChatUiStrings>;
82
83
  alwaysOnTop?: boolean;
83
84
  }
@@ -1231,7 +1232,7 @@ interface HistoryModalProps {
1231
1232
  }
1232
1233
  declare const HistoryModal: react.NamedExoticComponent<HistoryModalProps>;
1233
1234
 
1234
- declare function resolveChatStrings(locale?: Locale, overrides?: Partial<ChatUiStrings>): ChatUiStrings;
1235
+ declare function resolveChatStrings(locale?: Locale | string, overrides?: Partial<ChatUiStrings>): ChatUiStrings;
1235
1236
 
1236
1237
  type AlertType = 'warning' | 'error';
1237
1238
  interface AlertAction {