@yushaw/sanqian-chat 0.2.32 → 0.2.34

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.
@@ -368,6 +368,53 @@ var import_os = __toESM(require("os"));
368
368
  var import_path = __toESM(require("path"));
369
369
 
370
370
  // src/main/hitl.ts
371
+ var DEFAULT_RUN_OWNER_INDEX_MAX_SIZE = 2048;
372
+ function wait(ms) {
373
+ return new Promise((resolve) => setTimeout(resolve, ms));
374
+ }
375
+ function createOwnedHitlStreamState(ownerWebContentsId) {
376
+ return {
377
+ cancelled: false,
378
+ runId: null,
379
+ cancelSignalSent: false,
380
+ ownerWebContentsId
381
+ };
382
+ }
383
+ function updateOwnedStreamRunId(stream, runId, rememberRunOwner, options = {}) {
384
+ if (typeof runId !== "string") {
385
+ return null;
386
+ }
387
+ const normalizedRunId = runId.trim();
388
+ if (!normalizedRunId) {
389
+ return null;
390
+ }
391
+ if (!stream.runId || options.overwrite) {
392
+ stream.runId = normalizedRunId;
393
+ }
394
+ rememberRunOwner(normalizedRunId, stream.ownerWebContentsId);
395
+ return normalizedRunId;
396
+ }
397
+ function resolveCancelledStreamLoopAction(stream, sendCancelRun, onCancelError) {
398
+ if (stream.cancelled && stream.runId && !stream.cancelSignalSent) {
399
+ try {
400
+ sendCancelRun(stream.runId);
401
+ stream.cancelSignalSent = true;
402
+ } catch (error) {
403
+ onCancelError?.(error);
404
+ }
405
+ }
406
+ if (!stream.cancelled) {
407
+ return "process";
408
+ }
409
+ return stream.cancelSignalSent ? "break" : "continue";
410
+ }
411
+ function emitStreamErrorAndMarkTerminal(stream, error, emitErrorEvent) {
412
+ if (stream.cancelled) {
413
+ return false;
414
+ }
415
+ emitErrorEvent(error instanceof Error ? error.message : "Stream error");
416
+ return true;
417
+ }
371
418
  function resolveHitlRunIdFromStreams(params, activeStreams) {
372
419
  if (params.runId) {
373
420
  return params.runId;
@@ -390,27 +437,38 @@ function resolveHitlRunIdFromStreams(params, activeStreams) {
390
437
  const first = activeRunIds.values().next();
391
438
  return first.done ? null : first.value;
392
439
  }
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) {
440
+ function resolveOwnedHitlRunIdFromStreams(params, activeStreams, senderWebContentsId, runOwnerByRunId) {
441
+ const resolveRunIdByOwnership = (runId) => {
405
442
  for (const stream of activeStreams.values()) {
406
443
  if (stream.cancelled || !stream.runId) continue;
407
444
  if (stream.ownerWebContentsId !== senderWebContentsId) continue;
408
- if (stream.runId === params.runId) {
409
- return params.runId;
445
+ if (stream.runId === runId) {
446
+ return runId;
447
+ }
448
+ }
449
+ if (runOwnerByRunId?.get(runId) === senderWebContentsId) {
450
+ return runId;
451
+ }
452
+ return null;
453
+ };
454
+ if (params.streamId) {
455
+ const stream = activeStreams.get(params.streamId);
456
+ if (stream) {
457
+ if (stream.ownerWebContentsId !== senderWebContentsId) {
458
+ return null;
410
459
  }
460
+ if (!stream.cancelled && stream.runId) {
461
+ return stream.runId;
462
+ }
463
+ }
464
+ if (params.runId) {
465
+ return resolveRunIdByOwnership(params.runId);
411
466
  }
412
467
  return null;
413
468
  }
469
+ if (params.runId) {
470
+ return resolveRunIdByOwnership(params.runId);
471
+ }
414
472
  const activeRunIds = /* @__PURE__ */ new Set();
415
473
  for (const stream of activeStreams.values()) {
416
474
  if (stream.cancelled || !stream.runId) continue;
@@ -423,6 +481,367 @@ function resolveOwnedHitlRunIdFromStreams(params, activeStreams, senderWebConten
423
481
  const first = activeRunIds.values().next();
424
482
  return first.done ? null : first.value;
425
483
  }
484
+ function rememberRunOwnerWithLru(runOwnerByRunId, runId, ownerWebContentsId, maxSize = DEFAULT_RUN_OWNER_INDEX_MAX_SIZE) {
485
+ if (!runId) return;
486
+ if (runOwnerByRunId.has(runId)) {
487
+ runOwnerByRunId.delete(runId);
488
+ }
489
+ runOwnerByRunId.set(runId, ownerWebContentsId);
490
+ while (runOwnerByRunId.size > maxSize) {
491
+ const oldestRunId = runOwnerByRunId.keys().next();
492
+ if (oldestRunId.done) break;
493
+ runOwnerByRunId.delete(oldestRunId.value);
494
+ }
495
+ }
496
+ async function waitForOwnedStreamRunId(streamId, ownerWebContentsId, activeStreams, timeoutMs = 1200, pollIntervalMs = 20) {
497
+ const deadline = Date.now() + Math.max(0, timeoutMs);
498
+ while (Date.now() <= deadline) {
499
+ const stream2 = activeStreams.get(streamId);
500
+ if (!stream2 || stream2.cancelled || stream2.ownerWebContentsId !== ownerWebContentsId) {
501
+ return null;
502
+ }
503
+ if (stream2.runId) {
504
+ return stream2.runId;
505
+ }
506
+ await wait(pollIntervalMs);
507
+ }
508
+ const stream = activeStreams.get(streamId);
509
+ if (!stream || stream.cancelled || stream.ownerWebContentsId !== ownerWebContentsId) {
510
+ return null;
511
+ }
512
+ return stream.runId ?? null;
513
+ }
514
+ function shouldForgetRunOwnerAfterStreamEnd(stream, sawTerminalEvent) {
515
+ if (!stream.runId) {
516
+ return false;
517
+ }
518
+ return sawTerminalEvent || stream.cancelled;
519
+ }
520
+ function registerStreamIpcHandlers(ipcMainHandle, ctx) {
521
+ ipcMainHandle("sanqian-chat:stream", async (event, rawParams) => {
522
+ const params = rawParams;
523
+ const webContents = event.sender;
524
+ const { streamId, messages, conversationId, agentId: requestedAgentId, attachedResources, sessionResources } = params;
525
+ const sdk = ctx.getSdk();
526
+ const agentId = ctx.getAgentId(requestedAgentId);
527
+ if (!sdk || !agentId) {
528
+ webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: "SDK or agent not ready" } });
529
+ return;
530
+ }
531
+ const activeStreams = ctx.getActiveStreams();
532
+ const streamState = createOwnedHitlStreamState(webContents.id);
533
+ activeStreams.set(streamId, streamState);
534
+ let sawTerminalEvent = false;
535
+ try {
536
+ await sdk.ensureReady();
537
+ const sdkMessages = messages.map((m) => ({ role: m.role, content: m.content }));
538
+ const validResources = attachedResources?.filter((r) => {
539
+ const parts = r.split(":");
540
+ return parts.length >= 3 && parts.every((p) => p.length > 0);
541
+ });
542
+ const chatOptions = {
543
+ conversationId,
544
+ persistHistory: true
545
+ };
546
+ if (validResources?.length) chatOptions.attachedResources = validResources;
547
+ if (sessionResources?.length) chatOptions.sessionResources = sessionResources;
548
+ const stream = sdk.chatStream(agentId, sdkMessages, chatOptions);
549
+ for await (const evt of stream) {
550
+ if (webContents.isDestroyed()) break;
551
+ const evtWithRunId = evt;
552
+ updateOwnedStreamRunId(
553
+ streamState,
554
+ evtWithRunId.run_id,
555
+ (runId, ownerWebContentsId) => ctx.rememberRunOwner(runId, ownerWebContentsId)
556
+ );
557
+ const cancelLoopAction = resolveCancelledStreamLoopAction(
558
+ streamState,
559
+ (runId) => sdk.cancelRun(runId),
560
+ (error) => {
561
+ console.warn(`[${ctx.logTag}] Failed to cancel run:`, error);
562
+ }
563
+ );
564
+ if (cancelLoopAction === "break") break;
565
+ if (cancelLoopAction === "continue") continue;
566
+ if (ctx.isDevMode()) {
567
+ console.log(`[${ctx.logTag}] SDK event:`, evt.type, JSON.stringify(evt).slice(0, 200));
568
+ }
569
+ switch (evt.type) {
570
+ case "start": {
571
+ const startEvt = evt;
572
+ updateOwnedStreamRunId(
573
+ streamState,
574
+ startEvt.run_id,
575
+ (runId, ownerWebContentsId) => ctx.rememberRunOwner(runId, ownerWebContentsId),
576
+ { overwrite: true }
577
+ );
578
+ webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "start", run_id: startEvt.run_id } });
579
+ break;
580
+ }
581
+ case "text":
582
+ webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "text", content: evt.content } });
583
+ break;
584
+ case "thinking":
585
+ webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "thinking", content: evt.content } });
586
+ break;
587
+ case "tool_call":
588
+ webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_call", tool_call: evt.tool_call } });
589
+ break;
590
+ case "tool_result":
591
+ webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_result", tool_call_id: evt.tool_call_id, result: evt.result } });
592
+ break;
593
+ case "done":
594
+ sawTerminalEvent = true;
595
+ webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "done", conversationId: evt.conversationId, title: evt.title } });
596
+ break;
597
+ case "error":
598
+ sawTerminalEvent = true;
599
+ webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: evt.error } });
600
+ break;
601
+ case "cancelled": {
602
+ sawTerminalEvent = true;
603
+ const cancelledEvt = evt;
604
+ webContents.send("sanqian-chat:streamEvent", {
605
+ streamId,
606
+ event: { type: "cancelled", run_id: cancelledEvt.run_id }
607
+ });
608
+ break;
609
+ }
610
+ default: {
611
+ const anyEvt = evt;
612
+ if (anyEvt.type === "interrupt") {
613
+ const interruptRunId = anyEvt.run_id || streamState.runId || void 0;
614
+ if (interruptRunId) {
615
+ updateOwnedStreamRunId(
616
+ streamState,
617
+ interruptRunId,
618
+ (runId, ownerWebContentsId) => ctx.rememberRunOwner(runId, ownerWebContentsId),
619
+ { overwrite: true }
620
+ );
621
+ }
622
+ webContents.send("sanqian-chat:streamEvent", {
623
+ streamId,
624
+ event: {
625
+ type: "interrupt",
626
+ interrupt_type: anyEvt.interrupt_type,
627
+ interrupt_payload: anyEvt.interrupt_payload,
628
+ run_id: interruptRunId
629
+ }
630
+ });
631
+ }
632
+ break;
633
+ }
634
+ }
635
+ }
636
+ } catch (e) {
637
+ sawTerminalEvent = emitStreamErrorAndMarkTerminal(
638
+ streamState,
639
+ e,
640
+ (errorMessage) => {
641
+ if (!webContents.isDestroyed()) {
642
+ webContents.send("sanqian-chat:streamEvent", {
643
+ streamId,
644
+ event: { type: "error", error: errorMessage }
645
+ });
646
+ }
647
+ }
648
+ ) || sawTerminalEvent;
649
+ } finally {
650
+ if (shouldForgetRunOwnerAfterStreamEnd(streamState, sawTerminalEvent)) {
651
+ ctx.forgetRunOwner(streamState.runId);
652
+ }
653
+ activeStreams.delete(streamId);
654
+ }
655
+ });
656
+ ipcMainHandle("sanqian-chat:cancelStream", async (event, rawParams) => {
657
+ const params = rawParams;
658
+ const senderWebContentsId = event.sender.id;
659
+ const runIdHint = typeof params.runId === "string" && params.runId.trim().length > 0 ? params.runId.trim() : void 0;
660
+ const sdk = ctx.getSdk();
661
+ const activeStreams = ctx.getActiveStreams();
662
+ const stream = activeStreams.get(params.streamId);
663
+ if (!stream) {
664
+ if (sdk && runIdHint && ctx.getRunOwnerByRunId().get(runIdHint) === senderWebContentsId) {
665
+ try {
666
+ await sdk.cancelRun(runIdHint);
667
+ ctx.forgetRunOwner(runIdHint);
668
+ return { success: true };
669
+ } catch (e) {
670
+ console.warn(`[${ctx.logTag}] Failed to cancel fallback run:`, e);
671
+ return { success: false, error: "cancel_failed" };
672
+ }
673
+ }
674
+ return { success: false, error: "stream_not_found" };
675
+ }
676
+ if (stream.ownerWebContentsId !== senderWebContentsId) {
677
+ console.warn(`[${ctx.logTag}] Rejecting cancelStream from non-owner sender`);
678
+ return { success: false, error: "stream_not_owned_by_sender" };
679
+ }
680
+ stream.cancelled = true;
681
+ const runId = stream.runId || runIdHint;
682
+ if (runId && !stream.cancelSignalSent) {
683
+ if (sdk) {
684
+ try {
685
+ await sdk.cancelRun(runId);
686
+ stream.cancelSignalSent = true;
687
+ ctx.rememberRunOwner(runId, senderWebContentsId);
688
+ } catch (e) {
689
+ console.warn(`[${ctx.logTag}] Failed to cancel run:`, e);
690
+ }
691
+ }
692
+ }
693
+ return { success: true };
694
+ });
695
+ ipcMainHandle("sanqian-chat:hitlResponse", async (event, rawParams) => {
696
+ const params = rawParams;
697
+ const sdk = ctx.getSdk();
698
+ const senderWebContentsId = event.sender.id;
699
+ const runId = ctx.resolveHitlRunId(
700
+ { runId: params.runId, streamId: params.streamId },
701
+ senderWebContentsId
702
+ );
703
+ if (sdk && runId) {
704
+ ctx.rememberRunOwner(runId, senderWebContentsId);
705
+ sdk.sendHitlResponse(runId, params.response);
706
+ return { success: true };
707
+ }
708
+ if (params.streamId) {
709
+ const stream = ctx.getActiveStreams().get(params.streamId);
710
+ if (!stream) {
711
+ console.warn(`[${ctx.logTag}] HITL response dropped: stream not found`);
712
+ return { success: false, error: "stream_not_found" };
713
+ }
714
+ if (stream.cancelled) {
715
+ console.warn(`[${ctx.logTag}] HITL response dropped: stream already cancelled`);
716
+ return { success: false, error: "stream_cancelled" };
717
+ }
718
+ if (stream.ownerWebContentsId !== senderWebContentsId) {
719
+ console.warn(`[${ctx.logTag}] HITL response dropped: stream not owned by sender`);
720
+ return { success: false, error: "stream_not_owned_by_sender" };
721
+ }
722
+ if (!sdk) {
723
+ return { success: false, error: "sdk_not_ready" };
724
+ }
725
+ const resolvedRunId = await ctx.waitForOwnedStreamRunId(
726
+ params.streamId,
727
+ senderWebContentsId
728
+ );
729
+ if (resolvedRunId) {
730
+ ctx.rememberRunOwner(resolvedRunId, senderWebContentsId);
731
+ sdk.sendHitlResponse(resolvedRunId, params.response);
732
+ return { success: true };
733
+ }
734
+ if (ctx.isDevMode()) {
735
+ console.warn(
736
+ `[${ctx.logTag}] HITL response dropped: runId unresolved after wait window`,
737
+ params.streamId
738
+ );
739
+ }
740
+ return { success: false, error: "run_id_unresolved" };
741
+ }
742
+ if (ctx.isDevMode()) {
743
+ console.warn(`[${ctx.logTag}] HITL response dropped: missing or unauthorized runId`);
744
+ }
745
+ return { success: false, error: "run_id_unresolved" };
746
+ });
747
+ }
748
+
749
+ // src/main/common-ipc.ts
750
+ function registerCommonChatIpcHandlers(handle, ctx) {
751
+ handle("sanqian-chat:connect", async () => {
752
+ try {
753
+ const sdk = ctx.getSdk();
754
+ if (!sdk) throw new Error("SDK not available");
755
+ await sdk.ensureReady();
756
+ ctx.onConnected?.();
757
+ return { success: true };
758
+ } catch (e) {
759
+ return { success: false, error: e instanceof Error ? e.message : "Connection failed" };
760
+ }
761
+ });
762
+ handle("sanqian-chat:isConnected", () => {
763
+ const sdk = ctx.getSdk();
764
+ return sdk?.isConnected() ?? false;
765
+ });
766
+ handle("sanqian-chat:listConversations", async (_, params) => {
767
+ const sdk = ctx.getSdk();
768
+ if (!sdk) return { success: false, error: "SDK not ready" };
769
+ try {
770
+ const result = await sdk.listConversations({
771
+ limit: params?.limit,
772
+ offset: params?.offset
773
+ });
774
+ return { success: true, data: result };
775
+ } catch (e) {
776
+ return { success: false, error: e instanceof Error ? e.message : "Failed to list" };
777
+ }
778
+ });
779
+ handle("sanqian-chat:getConversation", async (_, params) => {
780
+ const sdk = ctx.getSdk();
781
+ if (!sdk) return { success: false, error: "SDK not ready" };
782
+ try {
783
+ const result = await sdk.getConversation(params.conversationId, { messageLimit: params.messageLimit });
784
+ let messages = result?.messages;
785
+ const sdkWithHistory = sdk;
786
+ if (typeof sdkWithHistory.getMessages === "function") {
787
+ try {
788
+ const history = await sdkWithHistory.getMessages(params.conversationId, { limit: params.messageLimit });
789
+ if (history?.messages && history.messages.length > 0) {
790
+ messages = history.messages;
791
+ }
792
+ } catch (e) {
793
+ console.warn(`[${ctx.logTag}] getMessages failed, fallback to getConversation:`, e);
794
+ }
795
+ }
796
+ return { success: true, data: { ...result, messages } };
797
+ } catch (e) {
798
+ return { success: false, error: e instanceof Error ? e.message : "Failed to get" };
799
+ }
800
+ });
801
+ handle("sanqian-chat:deleteConversation", async (_, params) => {
802
+ const sdk = ctx.getSdk();
803
+ if (!sdk) return { success: false, error: "SDK not ready" };
804
+ try {
805
+ await sdk.deleteConversation(params.conversationId);
806
+ return { success: true };
807
+ } catch (e) {
808
+ return { success: false, error: e instanceof Error ? e.message : "Failed to delete" };
809
+ }
810
+ });
811
+ handle("sanqian-chat:listResourceProviders", async () => {
812
+ const sdk = ctx.getSdk();
813
+ if (!sdk) return { success: false, error: "SDK not ready" };
814
+ try {
815
+ const agentId = ctx.getAgentId();
816
+ const queryParams = agentId ? `?agent_id=${encodeURIComponent(agentId)}` : "";
817
+ const url = `http://127.0.0.1:${sdk.getPort()}/api/sdk/contexts${queryParams}`;
818
+ const response = await fetch(url);
819
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
820
+ const data = await response.json();
821
+ return { success: true, data: data.contexts || [] };
822
+ } catch (e) {
823
+ return { success: false, error: e instanceof Error ? e.message : "Failed to list providers" };
824
+ }
825
+ });
826
+ handle("sanqian-chat:getResourceList", async (_, params) => {
827
+ const sdk = ctx.getSdk();
828
+ if (!sdk) return { success: false, error: "SDK not ready" };
829
+ try {
830
+ const queryString = new URLSearchParams();
831
+ if (params.query) queryString.set("query", params.query);
832
+ if (params.offset !== void 0) queryString.set("offset", String(params.offset));
833
+ if (params.limit !== void 0) queryString.set("limit", String(params.limit));
834
+ const url = `http://127.0.0.1:${sdk.getPort()}/api/sdk/contexts/${encodeURIComponent(params.providerId)}/list?${queryString}`;
835
+ const response = await fetch(url);
836
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
837
+ const data = await response.json();
838
+ if (data.error) throw new Error(data.error);
839
+ return { success: true, data: { items: data.items || [], hasMore: data.has_more } };
840
+ } catch (e) {
841
+ return { success: false, error: e instanceof Error ? e.message : "Failed to get resource list" };
842
+ }
843
+ });
844
+ }
426
845
 
427
846
  // src/main/FloatingWindow.ts
428
847
  var ipcHandlersRegistered = false;
@@ -436,6 +855,7 @@ var FloatingWindow = class _FloatingWindow {
436
855
  this.savedState = null;
437
856
  this.stateSaveTimer = null;
438
857
  this.activeStreams = /* @__PURE__ */ new Map();
858
+ this.runOwnerByRunId = /* @__PURE__ */ new Map();
439
859
  this.reconnectAcquired = false;
440
860
  if (activeInstance) {
441
861
  console.warn("[FloatingWindow] Only one instance supported. Destroying previous.");
@@ -460,10 +880,31 @@ var FloatingWindow = class _FloatingWindow {
460
880
  }
461
881
  resolveHitlRunId(params, senderWebContentsId) {
462
882
  if (typeof senderWebContentsId === "number") {
463
- return resolveOwnedHitlRunIdFromStreams(params, this.activeStreams, senderWebContentsId);
883
+ return resolveOwnedHitlRunIdFromStreams(
884
+ params,
885
+ this.activeStreams,
886
+ senderWebContentsId,
887
+ this.runOwnerByRunId
888
+ );
464
889
  }
465
890
  return resolveHitlRunIdFromStreams(params, this.activeStreams);
466
891
  }
892
+ rememberRunOwner(runId, ownerWebContentsId) {
893
+ rememberRunOwnerWithLru(this.runOwnerByRunId, runId, ownerWebContentsId);
894
+ }
895
+ forgetRunOwner(runId) {
896
+ if (!runId) return;
897
+ this.runOwnerByRunId.delete(runId);
898
+ }
899
+ async waitForOwnedStreamRunId(streamId, ownerWebContentsId, timeoutMs = 1200, pollIntervalMs = 20) {
900
+ return waitForOwnedStreamRunId(
901
+ streamId,
902
+ ownerWebContentsId,
903
+ this.activeStreams,
904
+ timeoutMs,
905
+ pollIntervalMs
906
+ );
907
+ }
467
908
  /**
468
909
  * Get SDK instance from either getClient or getSdk
469
910
  */
@@ -502,6 +943,7 @@ var FloatingWindow = class _FloatingWindow {
502
943
  skipTaskbar: !showInTaskbar,
503
944
  webPreferences: {
504
945
  preload: preloadPath,
946
+ sandbox: true,
505
947
  contextIsolation: true,
506
948
  nodeIntegration: false
507
949
  }
@@ -681,211 +1123,35 @@ var FloatingWindow = class _FloatingWindow {
681
1123
  setupIpcHandlers() {
682
1124
  if (ipcHandlersRegistered) return;
683
1125
  ipcHandlersRegistered = true;
684
- import_electron.ipcMain.handle("sanqian-chat:connect", async () => {
685
- try {
686
- const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
687
- if (!sdk) throw new Error("SDK not available");
688
- await sdk.ensureReady();
689
- return { success: true };
690
- } catch (e) {
691
- return { success: false, error: e instanceof Error ? e.message : "Connection failed" };
692
- }
693
- });
694
- import_electron.ipcMain.handle("sanqian-chat:isConnected", () => {
695
- const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
696
- return sdk?.isConnected() ?? false;
697
- });
698
- import_electron.ipcMain.handle("sanqian-chat:stream", async (event, params) => {
699
- const webContents = event.sender;
700
- const { streamId, messages, conversationId, agentId: requestedAgentId } = params;
701
- const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
702
- const agentId = requestedAgentId ?? activeInstance?.options.getAgentId();
703
- if (!sdk || !agentId) {
704
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: "SDK or agent not ready" } });
705
- return;
706
- }
707
- const streamState = {
708
- cancelled: false,
709
- runId: null,
710
- cancelSignalSent: false,
711
- pendingHitlResponse: null,
712
- ownerWebContentsId: webContents.id
713
- };
714
- activeInstance?.activeStreams.set(streamId, streamState);
715
- try {
716
- await sdk.ensureReady();
717
- const sdkMessages = messages.map((m) => ({ role: m.role, content: m.content }));
718
- const stream = sdk.chatStream(
719
- agentId,
720
- sdkMessages,
721
- { conversationId, persistHistory: true }
722
- );
723
- for await (const evt of stream) {
724
- const evtWithRunId = evt;
725
- if (evtWithRunId.run_id && !streamState.runId) {
726
- streamState.runId = evtWithRunId.run_id;
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
- }
736
- if (streamState.cancelled && streamState.runId && !streamState.cancelSignalSent) {
737
- try {
738
- sdk.cancelRun(streamState.runId);
739
- streamState.cancelSignalSent = true;
740
- } catch (e) {
741
- console.warn("[FloatingWindow] Failed to cancel run:", e);
742
- }
743
- }
744
- if (streamState.cancelled) {
745
- if (streamState.cancelSignalSent) {
746
- break;
747
- }
748
- continue;
749
- }
750
- if (activeInstance?.options.devMode) {
751
- console.log("[FloatingWindow] SDK event:", evt.type, JSON.stringify(evt).slice(0, 200));
752
- }
753
- switch (evt.type) {
754
- case "start": {
755
- const startEvt = evt;
756
- if (startEvt.run_id) {
757
- streamState.runId = startEvt.run_id;
758
- }
759
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "start", run_id: startEvt.run_id } });
760
- break;
761
- }
762
- case "text":
763
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "text", content: evt.content } });
764
- break;
765
- case "thinking":
766
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "thinking", content: evt.content } });
767
- break;
768
- case "tool_call":
769
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_call", tool_call: evt.tool_call } });
770
- break;
771
- case "tool_result":
772
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_result", tool_call_id: evt.tool_call_id, result: evt.result } });
773
- break;
774
- case "done":
775
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "done", conversationId: evt.conversationId, title: evt.title } });
776
- break;
777
- case "error":
778
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: evt.error } });
779
- break;
780
- default: {
781
- const anyEvt = evt;
782
- if (anyEvt.type === "interrupt") {
783
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "interrupt", interrupt_type: anyEvt.interrupt_type, interrupt_payload: anyEvt.interrupt_payload, run_id: anyEvt.run_id } });
784
- }
785
- break;
786
- }
787
- }
788
- }
789
- } catch (e) {
790
- if (!streamState.cancelled) {
791
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: e instanceof Error ? e.message : "Stream error" } });
792
- }
793
- } finally {
794
- activeInstance?.activeStreams.delete(streamId);
795
- }
796
- });
797
- import_electron.ipcMain.handle("sanqian-chat:cancelStream", (event, params) => {
798
- const stream = activeInstance?.activeStreams.get(params.streamId);
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);
815
- }
816
- }
817
- }
818
- return { success: true };
819
- });
820
- import_electron.ipcMain.handle("sanqian-chat:hitlResponse", (event, params) => {
821
- const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
822
- const senderWebContentsId = event.sender.id;
823
- const runId = activeInstance?.resolveHitlRunId(
824
- { runId: params.runId, streamId: params.streamId },
825
- senderWebContentsId
826
- ) ?? null;
827
- if (sdk && runId) {
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
- }
839
- } else if (activeInstance?.options.devMode) {
840
- console.warn("[FloatingWindow] HITL response dropped: missing or unauthorized runId");
841
- }
842
- return { success: true };
843
- });
844
- import_electron.ipcMain.handle("sanqian-chat:listConversations", async (_, params) => {
845
- const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
846
- if (!sdk) return { success: false, error: "SDK not ready" };
847
- try {
848
- const result = await sdk.listConversations({
849
- limit: params?.limit,
850
- offset: params?.offset
851
- });
852
- return { success: true, data: result };
853
- } catch (e) {
854
- return { success: false, error: e instanceof Error ? e.message : "Failed to list" };
855
- }
856
- });
857
- import_electron.ipcMain.handle("sanqian-chat:getConversation", async (_, params) => {
858
- const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
859
- if (!sdk) return { success: false, error: "SDK not ready" };
860
- try {
861
- const result = await sdk.getConversation(params.conversationId, { messageLimit: params.messageLimit });
862
- let messages = result?.messages;
863
- const sdkWithHistory = sdk;
864
- if (typeof sdkWithHistory.getMessages === "function") {
865
- try {
866
- const history = await sdkWithHistory.getMessages(params.conversationId, { limit: params.messageLimit });
867
- if (history?.messages && history.messages.length > 0) {
868
- messages = history.messages;
869
- }
870
- } catch (e) {
871
- console.warn("[sanqian-chat][main] getMessages failed, fallback to getConversation:", e);
872
- }
873
- }
874
- return { success: true, data: { ...result, messages } };
875
- } catch (e) {
876
- return { success: false, error: e instanceof Error ? e.message : "Failed to get" };
877
- }
878
- });
879
- import_electron.ipcMain.handle("sanqian-chat:deleteConversation", async (_, params) => {
880
- const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
881
- if (!sdk) return { success: false, error: "SDK not ready" };
882
- try {
883
- await sdk.deleteConversation(params.conversationId);
884
- return { success: true };
885
- } catch (e) {
886
- return { success: false, error: e instanceof Error ? e.message : "Failed to delete" };
887
- }
888
- });
1126
+ registerCommonChatIpcHandlers(
1127
+ import_electron.ipcMain.handle.bind(import_electron.ipcMain),
1128
+ {
1129
+ getSdk: () => {
1130
+ const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
1131
+ return sdk;
1132
+ },
1133
+ getAgentId: () => activeInstance?.options.getAgentId() ?? void 0,
1134
+ logTag: "FloatingWindow"
1135
+ }
1136
+ );
1137
+ registerStreamIpcHandlers(
1138
+ import_electron.ipcMain.handle.bind(import_electron.ipcMain),
1139
+ {
1140
+ getActiveStreams: () => activeInstance?.["activeStreams"] ?? /* @__PURE__ */ new Map(),
1141
+ getRunOwnerByRunId: () => activeInstance?.["runOwnerByRunId"] ?? /* @__PURE__ */ new Map(),
1142
+ rememberRunOwner: (runId, ownerWebContentsId) => activeInstance?.["rememberRunOwner"](runId, ownerWebContentsId),
1143
+ forgetRunOwner: (runId) => activeInstance?.["forgetRunOwner"](runId),
1144
+ resolveHitlRunId: (params, senderId) => activeInstance?.["resolveHitlRunId"](params, senderId) ?? null,
1145
+ waitForOwnedStreamRunId: (streamId, senderId) => activeInstance?.["waitForOwnedStreamRunId"](streamId, senderId) ?? Promise.resolve(null),
1146
+ getSdk: () => {
1147
+ const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
1148
+ return sdk;
1149
+ },
1150
+ getAgentId: (requested) => requested ?? activeInstance?.options.getAgentId() ?? void 0,
1151
+ isDevMode: () => activeInstance?.options.devMode ?? false,
1152
+ logTag: "FloatingWindow"
1153
+ }
1154
+ );
889
1155
  import_electron.ipcMain.handle("sanqian-chat:hide", () => {
890
1156
  activeInstance?.hide();
891
1157
  return { success: true };
@@ -914,40 +1180,6 @@ var FloatingWindow = class _FloatingWindow {
914
1180
  return { success: false, error: e instanceof Error ? e.message : "Failed to set background color" };
915
1181
  }
916
1182
  });
917
- import_electron.ipcMain.handle("sanqian-chat:listResourceProviders", async () => {
918
- const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
919
- if (!sdk) return { success: false, error: "SDK not ready" };
920
- try {
921
- const port = sdk.getPort();
922
- const agentId = activeInstance?.options.getAgentId();
923
- const queryParams = agentId ? `?agent_id=${encodeURIComponent(agentId)}` : "";
924
- const response = await fetch(`http://127.0.0.1:${port}/api/sdk/contexts${queryParams}`);
925
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
926
- const data = await response.json();
927
- return { success: true, data: data.contexts || [] };
928
- } catch (e) {
929
- return { success: false, error: e instanceof Error ? e.message : "Failed to list providers" };
930
- }
931
- });
932
- import_electron.ipcMain.handle("sanqian-chat:getResourceList", async (_, params) => {
933
- const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
934
- if (!sdk) return { success: false, error: "SDK not ready" };
935
- try {
936
- const port = sdk.getPort();
937
- const queryParams = new URLSearchParams();
938
- if (params.query) queryParams.set("query", params.query);
939
- if (params.offset !== void 0) queryParams.set("offset", String(params.offset));
940
- if (params.limit !== void 0) queryParams.set("limit", String(params.limit));
941
- const url = `http://127.0.0.1:${port}/api/sdk/contexts/${encodeURIComponent(params.providerId)}/list?${queryParams}`;
942
- const response = await fetch(url);
943
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
944
- const data = await response.json();
945
- if (data.error) throw new Error(data.error);
946
- return { success: true, data: { items: data.items || [], hasMore: data.has_more } };
947
- } catch (e) {
948
- return { success: false, error: e instanceof Error ? e.message : "Failed to get resource list" };
949
- }
950
- });
951
1183
  }
952
1184
  // Public API
953
1185
  show() {
@@ -1021,6 +1253,7 @@ var FloatingWindow = class _FloatingWindow {
1021
1253
  stream.cancelled = true;
1022
1254
  });
1023
1255
  this.activeStreams.clear();
1256
+ this.runOwnerByRunId.clear();
1024
1257
  if (activeInstance === this) {
1025
1258
  activeInstance = null;
1026
1259
  if (ipcHandlersRegistered) {
@@ -1095,6 +1328,8 @@ var ChatPanel = class {
1095
1328
  this.stateSaveTimer = null;
1096
1329
  // Active streams for cancel support
1097
1330
  this.activeStreams = /* @__PURE__ */ new Map();
1331
+ // Run ownership index for HITL recovery when stream-map entries churn.
1332
+ this.runOwnerByRunId = /* @__PURE__ */ new Map();
1098
1333
  // Session resource event cleanup
1099
1334
  this.sessionResourceCleanup = null;
1100
1335
  // === Private: Host Window Listeners ===
@@ -1405,6 +1640,7 @@ var ChatPanel = class {
1405
1640
  this.embeddedView = null;
1406
1641
  }
1407
1642
  this.sharedView = null;
1643
+ this.runOwnerByRunId.clear();
1408
1644
  if (activeInstance2 === this) {
1409
1645
  activeInstance2 = null;
1410
1646
  this.cleanupIpcHandlers();
@@ -1619,6 +1855,7 @@ var ChatPanel = class {
1619
1855
  this.sharedView = new WebContentsViewClass({
1620
1856
  webPreferences: {
1621
1857
  preload: this.config.preloadPath,
1858
+ sandbox: true,
1622
1859
  contextIsolation: true,
1623
1860
  nodeIntegration: false
1624
1861
  }
@@ -1792,10 +2029,31 @@ var ChatPanel = class {
1792
2029
  */
1793
2030
  resolveHitlRunId(params, senderWebContentsId) {
1794
2031
  if (typeof senderWebContentsId === "number") {
1795
- return resolveOwnedHitlRunIdFromStreams(params, this.activeStreams, senderWebContentsId);
2032
+ return resolveOwnedHitlRunIdFromStreams(
2033
+ params,
2034
+ this.activeStreams,
2035
+ senderWebContentsId,
2036
+ this.runOwnerByRunId
2037
+ );
1796
2038
  }
1797
2039
  return resolveHitlRunIdFromStreams(params, this.activeStreams);
1798
2040
  }
2041
+ rememberRunOwner(runId, ownerWebContentsId) {
2042
+ rememberRunOwnerWithLru(this.runOwnerByRunId, runId, ownerWebContentsId);
2043
+ }
2044
+ forgetRunOwner(runId) {
2045
+ if (!runId) return;
2046
+ this.runOwnerByRunId.delete(runId);
2047
+ }
2048
+ async waitForOwnedStreamRunId(streamId, ownerWebContentsId, timeoutMs = 1200, pollIntervalMs = 20) {
2049
+ return waitForOwnedStreamRunId(
2050
+ streamId,
2051
+ ownerWebContentsId,
2052
+ this.activeStreams,
2053
+ timeoutMs,
2054
+ pollIntervalMs
2055
+ );
2056
+ }
1799
2057
  /**
1800
2058
  * Setup session resource event forwarding from SDK to renderer
1801
2059
  * Called when SDK becomes available (on connect)
@@ -1833,217 +2091,30 @@ var ChatPanel = class {
1833
2091
  setupIpcHandlers() {
1834
2092
  if (ipcHandlersRegistered2) return;
1835
2093
  ipcHandlersRegistered2 = true;
1836
- import_electron2.ipcMain.handle("sanqian-chat:connect", async () => {
1837
- try {
1838
- const sdk = activeInstance2?.getSdk();
1839
- if (!sdk) throw new Error("SDK not available");
1840
- await sdk.ensureReady();
1841
- activeInstance2?.setupSessionResourceEvents();
1842
- return { success: true };
1843
- } catch (e) {
1844
- return { success: false, error: e instanceof Error ? e.message : "Connection failed" };
1845
- }
1846
- });
1847
- import_electron2.ipcMain.handle("sanqian-chat:isConnected", () => {
1848
- const sdk = activeInstance2?.getSdk();
1849
- return sdk?.isConnected() ?? false;
1850
- });
1851
- import_electron2.ipcMain.handle("sanqian-chat:stream", async (event, params) => {
1852
- const webContents = event.sender;
1853
- const { streamId, messages, conversationId, agentId: requestedAgentId, attachedResources, sessionResources } = params;
1854
- const sdk = activeInstance2?.getSdk();
1855
- const agentId = requestedAgentId ?? activeInstance2?.config.getAgentId();
1856
- if (!sdk || !agentId) {
1857
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: "SDK or agent not ready" } });
1858
- return;
1859
- }
1860
- const streamState = {
1861
- cancelled: false,
1862
- runId: null,
1863
- cancelSignalSent: false,
1864
- pendingHitlResponse: null,
1865
- ownerWebContentsId: webContents.id
1866
- };
1867
- activeInstance2?.activeStreams.set(streamId, streamState);
1868
- try {
1869
- await sdk.ensureReady();
1870
- const sdkMessages = messages.map((m) => ({ role: m.role, content: m.content }));
1871
- const validResources = attachedResources?.filter((r) => {
1872
- const parts = r.split(":");
1873
- return parts.length >= 3 && parts.every((p) => p.length > 0);
1874
- });
1875
- const stream = sdk.chatStream(agentId, sdkMessages, {
1876
- conversationId,
1877
- persistHistory: true,
1878
- attachedResources: validResources?.length ? validResources : void 0,
1879
- sessionResources: sessionResources?.length ? sessionResources : void 0
1880
- });
1881
- for await (const evt of stream) {
1882
- const evtWithRunId = evt;
1883
- if (evtWithRunId.run_id && !streamState.runId) {
1884
- streamState.runId = evtWithRunId.run_id;
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
- }
1894
- if (streamState.cancelled && streamState.runId && !streamState.cancelSignalSent) {
1895
- try {
1896
- sdk.cancelRun(streamState.runId);
1897
- streamState.cancelSignalSent = true;
1898
- } catch (e) {
1899
- console.warn("[ChatPanel] Failed to cancel run:", e);
1900
- }
1901
- }
1902
- if (streamState.cancelled) {
1903
- if (streamState.cancelSignalSent) {
1904
- break;
1905
- }
1906
- continue;
1907
- }
1908
- if (activeInstance2?.config.devMode) {
1909
- console.log("[ChatPanel] SDK event:", evt.type, JSON.stringify(evt).slice(0, 200));
1910
- }
1911
- switch (evt.type) {
1912
- case "start": {
1913
- const startEvt = evt;
1914
- if (startEvt.run_id) {
1915
- streamState.runId = startEvt.run_id;
1916
- }
1917
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "start", run_id: startEvt.run_id } });
1918
- break;
1919
- }
1920
- case "text":
1921
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "text", content: evt.content } });
1922
- break;
1923
- case "thinking":
1924
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "thinking", content: evt.content } });
1925
- break;
1926
- case "tool_call":
1927
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_call", tool_call: evt.tool_call } });
1928
- break;
1929
- case "tool_result":
1930
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_result", tool_call_id: evt.tool_call_id, result: evt.result } });
1931
- break;
1932
- case "done":
1933
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "done", conversationId: evt.conversationId, title: evt.title } });
1934
- break;
1935
- case "error":
1936
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: evt.error } });
1937
- break;
1938
- default: {
1939
- const anyEvt = evt;
1940
- if (anyEvt.type === "interrupt") {
1941
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "interrupt", interrupt_type: anyEvt.interrupt_type, interrupt_payload: anyEvt.interrupt_payload, run_id: anyEvt.run_id } });
1942
- }
1943
- break;
1944
- }
1945
- }
1946
- }
1947
- } catch (e) {
1948
- if (!streamState.cancelled) {
1949
- webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: e instanceof Error ? e.message : "Stream error" } });
1950
- }
1951
- } finally {
1952
- activeInstance2?.activeStreams.delete(streamId);
1953
- }
1954
- });
1955
- import_electron2.ipcMain.handle("sanqian-chat:cancelStream", (event, params) => {
1956
- const stream = activeInstance2?.activeStreams.get(params.streamId);
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);
1973
- }
1974
- }
1975
- }
1976
- return { success: true };
1977
- });
1978
- import_electron2.ipcMain.handle("sanqian-chat:hitlResponse", (event, params) => {
1979
- const sdk = activeInstance2?.getSdk();
1980
- const senderWebContentsId = event.sender.id;
1981
- const runId = activeInstance2?.resolveHitlRunId(
1982
- { runId: params.runId, streamId: params.streamId },
1983
- senderWebContentsId
1984
- ) ?? null;
1985
- if (sdk && runId) {
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
- }
1997
- } else if (activeInstance2?.config.devMode) {
1998
- console.warn("[ChatPanel] HITL response dropped: missing or unauthorized runId");
1999
- }
2000
- return { success: true };
2001
- });
2002
- import_electron2.ipcMain.handle("sanqian-chat:listConversations", async (_, params) => {
2003
- const sdk = activeInstance2?.getSdk();
2004
- if (!sdk) return { success: false, error: "SDK not ready" };
2005
- try {
2006
- const result = await sdk.listConversations({
2007
- limit: params?.limit,
2008
- offset: params?.offset
2009
- });
2010
- return { success: true, data: result };
2011
- } catch (e) {
2012
- return { success: false, error: e instanceof Error ? e.message : "Failed to list" };
2013
- }
2014
- });
2015
- import_electron2.ipcMain.handle("sanqian-chat:getConversation", async (_, params) => {
2016
- const sdk = activeInstance2?.getSdk();
2017
- if (!sdk) return { success: false, error: "SDK not ready" };
2018
- try {
2019
- const result = await sdk.getConversation(params.conversationId, { messageLimit: params.messageLimit });
2020
- let messages = result?.messages;
2021
- const sdkWithHistory = sdk;
2022
- if (typeof sdkWithHistory.getMessages === "function") {
2023
- try {
2024
- const history = await sdkWithHistory.getMessages(params.conversationId, { limit: params.messageLimit });
2025
- if (history?.messages && history.messages.length > 0) {
2026
- messages = history.messages;
2027
- }
2028
- } catch (e) {
2029
- console.warn("[ChatPanel] getMessages failed, fallback to getConversation:", e);
2030
- }
2031
- }
2032
- return { success: true, data: { ...result, messages } };
2033
- } catch (e) {
2034
- return { success: false, error: e instanceof Error ? e.message : "Failed to get" };
2035
- }
2036
- });
2037
- import_electron2.ipcMain.handle("sanqian-chat:deleteConversation", async (_, params) => {
2038
- const sdk = activeInstance2?.getSdk();
2039
- if (!sdk) return { success: false, error: "SDK not ready" };
2040
- try {
2041
- await sdk.deleteConversation(params.conversationId);
2042
- return { success: true };
2043
- } catch (e) {
2044
- return { success: false, error: e instanceof Error ? e.message : "Failed to delete" };
2045
- }
2046
- });
2094
+ registerCommonChatIpcHandlers(
2095
+ import_electron2.ipcMain.handle.bind(import_electron2.ipcMain),
2096
+ {
2097
+ getSdk: () => activeInstance2?.getSdk() ?? null,
2098
+ getAgentId: () => activeInstance2?.config.getAgentId() ?? void 0,
2099
+ onConnected: () => activeInstance2?.setupSessionResourceEvents(),
2100
+ logTag: "ChatPanel"
2101
+ }
2102
+ );
2103
+ registerStreamIpcHandlers(
2104
+ import_electron2.ipcMain.handle.bind(import_electron2.ipcMain),
2105
+ {
2106
+ getActiveStreams: () => activeInstance2?.["activeStreams"] ?? /* @__PURE__ */ new Map(),
2107
+ getRunOwnerByRunId: () => activeInstance2?.["runOwnerByRunId"] ?? /* @__PURE__ */ new Map(),
2108
+ rememberRunOwner: (runId, ownerWebContentsId) => activeInstance2?.["rememberRunOwner"](runId, ownerWebContentsId),
2109
+ forgetRunOwner: (runId) => activeInstance2?.["forgetRunOwner"](runId),
2110
+ resolveHitlRunId: (params, senderId) => activeInstance2?.["resolveHitlRunId"](params, senderId) ?? null,
2111
+ waitForOwnedStreamRunId: (streamId, senderId) => activeInstance2?.["waitForOwnedStreamRunId"](streamId, senderId) ?? Promise.resolve(null),
2112
+ getSdk: () => activeInstance2?.getSdk() ?? null,
2113
+ getAgentId: (requested) => requested ?? activeInstance2?.config.getAgentId() ?? void 0,
2114
+ isDevMode: () => activeInstance2?.config.devMode ?? false,
2115
+ logTag: "ChatPanel"
2116
+ }
2117
+ );
2047
2118
  import_electron2.ipcMain.handle("sanqian-chat:hide", () => {
2048
2119
  activeInstance2?.hide();
2049
2120
  return { success: true };
@@ -2069,39 +2140,6 @@ var ChatPanel = class {
2069
2140
  return { success: false, error: e instanceof Error ? e.message : "Failed to set background color" };
2070
2141
  }
2071
2142
  });
2072
- import_electron2.ipcMain.handle("sanqian-chat:listResourceProviders", async () => {
2073
- const sdk = activeInstance2?.getSdk();
2074
- if (!sdk) return { success: false, error: "SDK not ready" };
2075
- try {
2076
- const agentId = activeInstance2?.config.getAgentId();
2077
- const queryParams = agentId ? `?agent_id=${encodeURIComponent(agentId)}` : "";
2078
- const url = `http://127.0.0.1:${sdk.getPort()}/api/sdk/contexts${queryParams}`;
2079
- const response = await fetch(url);
2080
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
2081
- const data = await response.json();
2082
- return { success: true, data: data.contexts || [] };
2083
- } catch (e) {
2084
- return { success: false, error: e instanceof Error ? e.message : "Failed to list providers" };
2085
- }
2086
- });
2087
- import_electron2.ipcMain.handle("sanqian-chat:getResourceList", async (_, params) => {
2088
- const sdk = activeInstance2?.getSdk();
2089
- if (!sdk) return { success: false, error: "SDK not ready" };
2090
- try {
2091
- const queryParams = new URLSearchParams();
2092
- if (params.query) queryParams.set("query", params.query);
2093
- if (params.offset !== void 0) queryParams.set("offset", String(params.offset));
2094
- if (params.limit !== void 0) queryParams.set("limit", String(params.limit));
2095
- const url = `http://127.0.0.1:${sdk.getPort()}/api/sdk/contexts/${encodeURIComponent(params.providerId)}/list?${queryParams}`;
2096
- const response = await fetch(url);
2097
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
2098
- const data = await response.json();
2099
- if (data.error) throw new Error(data.error);
2100
- return { success: true, data: { items: data.items || [], hasMore: data.has_more } };
2101
- } catch (e) {
2102
- return { success: false, error: e instanceof Error ? e.message : "Failed to get resource list" };
2103
- }
2104
- });
2105
2143
  import_electron2.ipcMain.on("sanqian-chat:getSessionResourcesSync", (event) => {
2106
2144
  const sdk = activeInstance2?.getSdk();
2107
2145
  event.returnValue = sdk?.getSessionResources?.() ?? [];