@yushaw/sanqian-chat 0.2.3 → 0.2.6
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.
- package/README.md +21 -0
- package/dist/core/index.d.mts +154 -1
- package/dist/core/index.d.ts +154 -1
- package/dist/main/index.d.mts +426 -2
- package/dist/main/index.d.ts +426 -2
- package/dist/main/index.js +1269 -4
- package/dist/main/index.mjs +1279 -3
- package/dist/preload/index.d.ts +49 -1
- package/dist/preload/index.js +42 -0
- package/dist/renderer/index.d.mts +300 -1
- package/dist/renderer/index.d.ts +300 -1
- package/dist/renderer/index.js +1068 -385
- package/dist/renderer/index.mjs +1003 -329
- package/package.json +1 -1
- package/src/renderer/styles/chat.css +37 -0
- package/src/renderer/styles/coreCss.ts +37 -0
- package/src/renderer/styles/safe.css +37 -0
package/dist/main/index.mjs
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
2
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
3
|
+
}) : x)(function(x) {
|
|
4
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
5
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
6
|
+
});
|
|
7
|
+
|
|
1
8
|
// src/main/client.ts
|
|
2
9
|
import { SanqianSDK } from "@yushaw/sanqian-sdk";
|
|
3
10
|
var SanqianAppClient = class {
|
|
@@ -8,13 +15,22 @@ var SanqianAppClient = class {
|
|
|
8
15
|
parameters: t.parameters,
|
|
9
16
|
handler: t.handler
|
|
10
17
|
}));
|
|
18
|
+
const contexts = config.contexts?.map((c) => ({
|
|
19
|
+
id: c.id,
|
|
20
|
+
name: c.name,
|
|
21
|
+
description: c.description,
|
|
22
|
+
getCurrent: c.getCurrent,
|
|
23
|
+
getList: c.getList,
|
|
24
|
+
getById: c.getById
|
|
25
|
+
}));
|
|
11
26
|
const sdkConfig = {
|
|
12
27
|
appName: config.appName,
|
|
13
28
|
appVersion: config.appVersion,
|
|
14
29
|
displayName: config.displayName,
|
|
15
30
|
launchCommand: config.launchCommand,
|
|
16
31
|
debug: config.debug,
|
|
17
|
-
tools
|
|
32
|
+
tools,
|
|
33
|
+
contexts
|
|
18
34
|
};
|
|
19
35
|
this.sdk = new SanqianSDK(sdkConfig);
|
|
20
36
|
}
|
|
@@ -76,7 +92,8 @@ var SanqianAppClient = class {
|
|
|
76
92
|
description: config.description,
|
|
77
93
|
system_prompt: config.systemPrompt,
|
|
78
94
|
tools: config.tools,
|
|
79
|
-
skills: config.skills
|
|
95
|
+
skills: config.skills,
|
|
96
|
+
attached_contexts: config.attachedContexts
|
|
80
97
|
};
|
|
81
98
|
const result = await this.sdk.createAgent(sdkConfig);
|
|
82
99
|
return { agentId: result.agent_id };
|
|
@@ -745,7 +762,1266 @@ var FloatingWindow = class _FloatingWindow {
|
|
|
745
762
|
return this.window;
|
|
746
763
|
}
|
|
747
764
|
};
|
|
765
|
+
|
|
766
|
+
// src/main/ChatPanel.ts
|
|
767
|
+
import {
|
|
768
|
+
BrowserWindow as BrowserWindow2,
|
|
769
|
+
globalShortcut as globalShortcut2,
|
|
770
|
+
screen as screen2,
|
|
771
|
+
ipcMain as ipcMain2,
|
|
772
|
+
app as app2
|
|
773
|
+
} from "electron";
|
|
774
|
+
import fs2 from "fs";
|
|
775
|
+
import os2 from "os";
|
|
776
|
+
import path2 from "path";
|
|
777
|
+
var BaseWindowClass;
|
|
778
|
+
var WebContentsViewClass;
|
|
779
|
+
try {
|
|
780
|
+
const electron = __require("electron");
|
|
781
|
+
BaseWindowClass = electron.BaseWindow;
|
|
782
|
+
WebContentsViewClass = electron.WebContentsView;
|
|
783
|
+
} catch {
|
|
784
|
+
}
|
|
785
|
+
var ipcHandlersRegistered2 = false;
|
|
786
|
+
var activeInstance2 = null;
|
|
787
|
+
var DEFAULT_EMBEDDED_WIDTH = 360;
|
|
788
|
+
var DEFAULT_FLOATING_WIDTH = 360;
|
|
789
|
+
var DEFAULT_FLOATING_HEIGHT = 540;
|
|
790
|
+
var ChatPanel = class {
|
|
791
|
+
constructor(config) {
|
|
792
|
+
this.visible = false;
|
|
793
|
+
// Embedded mode view
|
|
794
|
+
this.embeddedView = null;
|
|
795
|
+
// Floating mode window and view
|
|
796
|
+
this.floatingWindow = null;
|
|
797
|
+
this.floatingView = null;
|
|
798
|
+
// Shortcuts tracking
|
|
799
|
+
this.registeredShortcuts = [];
|
|
800
|
+
// State save timer
|
|
801
|
+
this.stateSaveTimer = null;
|
|
802
|
+
// Active streams for cancel support
|
|
803
|
+
this.activeStreams = /* @__PURE__ */ new Map();
|
|
804
|
+
// === Private: Host Window Listeners ===
|
|
805
|
+
this.hostResizeHandler = null;
|
|
806
|
+
// === Private: Mode Switching ===
|
|
807
|
+
// Single shared view that moves between containers
|
|
808
|
+
this.sharedView = null;
|
|
809
|
+
if (activeInstance2) {
|
|
810
|
+
console.warn("[ChatPanel] Only one instance supported. Destroying previous.");
|
|
811
|
+
activeInstance2.destroy();
|
|
812
|
+
}
|
|
813
|
+
activeInstance2 = this;
|
|
814
|
+
this.config = this.normalizeConfig(config);
|
|
815
|
+
this.embeddedModeAvailable = this.checkEmbeddedModeAvailable();
|
|
816
|
+
this.mode = this.determineInitialMode();
|
|
817
|
+
this.embeddedWidth = this.config.width ?? DEFAULT_EMBEDDED_WIDTH;
|
|
818
|
+
this.floatingWidth = DEFAULT_FLOATING_WIDTH;
|
|
819
|
+
this.floatingHeight = DEFAULT_FLOATING_HEIGHT;
|
|
820
|
+
this.currentWidth = this.embeddedWidth;
|
|
821
|
+
this.loadState();
|
|
822
|
+
this.currentWidth = this.mode === "embedded" ? this.embeddedWidth : this.floatingWidth;
|
|
823
|
+
this.initWebContents();
|
|
824
|
+
this.setupIpcHandlers();
|
|
825
|
+
app2.whenReady().then(() => this.setupShortcuts());
|
|
826
|
+
this.setupHostWindowListeners();
|
|
827
|
+
}
|
|
828
|
+
setupHostWindowListeners() {
|
|
829
|
+
let resizePending = false;
|
|
830
|
+
this.hostResizeHandler = () => {
|
|
831
|
+
if (resizePending) return;
|
|
832
|
+
resizePending = true;
|
|
833
|
+
setImmediate(() => {
|
|
834
|
+
resizePending = false;
|
|
835
|
+
if (this.mode === "embedded") {
|
|
836
|
+
this.handleResponsiveResize();
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
};
|
|
840
|
+
const hostWindow = this.config.hostWindow;
|
|
841
|
+
if ("on" in hostWindow) {
|
|
842
|
+
hostWindow.on("resize", this.hostResizeHandler);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
cleanupHostWindowListeners() {
|
|
846
|
+
if (this.hostResizeHandler) {
|
|
847
|
+
const hostWindow = this.config.hostWindow;
|
|
848
|
+
if ("off" in hostWindow) {
|
|
849
|
+
hostWindow.off("resize", this.hostResizeHandler);
|
|
850
|
+
}
|
|
851
|
+
this.hostResizeHandler = null;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
/**
|
|
855
|
+
* Handle window resize - update layout
|
|
856
|
+
*/
|
|
857
|
+
handleResponsiveResize() {
|
|
858
|
+
if (this.visible) {
|
|
859
|
+
this.updateEmbeddedLayout();
|
|
860
|
+
} else {
|
|
861
|
+
this.updateHostLayoutForHidden();
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
// === Public API ===
|
|
865
|
+
/**
|
|
866
|
+
* Show panel
|
|
867
|
+
*/
|
|
868
|
+
show() {
|
|
869
|
+
if (this.visible) return;
|
|
870
|
+
this.visible = true;
|
|
871
|
+
if (this.mode === "embedded") {
|
|
872
|
+
this.showEmbedded();
|
|
873
|
+
} else {
|
|
874
|
+
this.showFloating();
|
|
875
|
+
}
|
|
876
|
+
this.notifyVisibilityChange();
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Hide panel
|
|
880
|
+
*/
|
|
881
|
+
hide() {
|
|
882
|
+
if (!this.visible) return;
|
|
883
|
+
this.visible = false;
|
|
884
|
+
if (this.mode === "embedded") {
|
|
885
|
+
this.hideEmbedded();
|
|
886
|
+
} else {
|
|
887
|
+
this.hideFloating();
|
|
888
|
+
}
|
|
889
|
+
this.notifyVisibilityChange();
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Toggle visibility
|
|
893
|
+
*/
|
|
894
|
+
toggle() {
|
|
895
|
+
if (this.visible) {
|
|
896
|
+
this.hide();
|
|
897
|
+
} else {
|
|
898
|
+
this.show();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Get current mode
|
|
903
|
+
*/
|
|
904
|
+
getMode() {
|
|
905
|
+
return this.mode;
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Set mode
|
|
909
|
+
*/
|
|
910
|
+
setMode(mode) {
|
|
911
|
+
if (mode === this.mode) return;
|
|
912
|
+
if (mode === "embedded" && !this.embeddedModeAvailable) {
|
|
913
|
+
console.warn("[ChatPanel] Embedded mode not available, staying in floating mode");
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
const wasVisible = this.visible;
|
|
917
|
+
if (wasVisible) {
|
|
918
|
+
if (this.mode === "embedded") {
|
|
919
|
+
this.hideEmbedded();
|
|
920
|
+
} else {
|
|
921
|
+
this.hideFloating();
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
this.currentWidth = mode === "embedded" ? this.embeddedWidth : this.floatingWidth;
|
|
925
|
+
if (mode === "embedded") {
|
|
926
|
+
this.switchToEmbedded();
|
|
927
|
+
} else {
|
|
928
|
+
this.switchToFloating();
|
|
929
|
+
}
|
|
930
|
+
if (wasVisible) {
|
|
931
|
+
if (mode === "embedded") {
|
|
932
|
+
this.showEmbedded();
|
|
933
|
+
} else {
|
|
934
|
+
this.showFloating();
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
this.mode = mode;
|
|
938
|
+
this.notifyModeChange();
|
|
939
|
+
this.saveState();
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Toggle mode
|
|
943
|
+
*/
|
|
944
|
+
toggleMode() {
|
|
945
|
+
const newMode = this.mode === "embedded" ? "floating" : "embedded";
|
|
946
|
+
this.setMode(newMode);
|
|
947
|
+
return this.mode;
|
|
948
|
+
}
|
|
949
|
+
/**
|
|
950
|
+
* Check if visible
|
|
951
|
+
*/
|
|
952
|
+
isVisible() {
|
|
953
|
+
return this.visible;
|
|
954
|
+
}
|
|
955
|
+
/**
|
|
956
|
+
* Set width with optional animation
|
|
957
|
+
*/
|
|
958
|
+
setWidth(width, animate = false) {
|
|
959
|
+
const clampedWidth = Math.max(this.config.minWidth, width);
|
|
960
|
+
if (animate && this.currentWidth !== clampedWidth) {
|
|
961
|
+
this.animateWidth(this.currentWidth, clampedWidth);
|
|
962
|
+
} else {
|
|
963
|
+
this.currentWidth = clampedWidth;
|
|
964
|
+
if (this.mode === "embedded") {
|
|
965
|
+
this.embeddedWidth = clampedWidth;
|
|
966
|
+
this.updateEmbeddedLayout();
|
|
967
|
+
} else {
|
|
968
|
+
this.floatingWidth = clampedWidth;
|
|
969
|
+
if (this.floatingWindow) {
|
|
970
|
+
const bounds = this.floatingWindow.getBounds();
|
|
971
|
+
this.floatingWindow.setBounds({ ...bounds, width: clampedWidth });
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
this.scheduleSaveState();
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Called when resize drag ends - update window min width constraint
|
|
979
|
+
*/
|
|
980
|
+
onResizeEnd() {
|
|
981
|
+
if (this.mode === "embedded" && this.visible) {
|
|
982
|
+
this.updateHostWindowMinWidth(true);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Animate width change smoothly
|
|
987
|
+
*/
|
|
988
|
+
animateWidth(fromWidth, toWidth, duration = 200) {
|
|
989
|
+
const startTime = Date.now();
|
|
990
|
+
const deltaWidth = toWidth - fromWidth;
|
|
991
|
+
const step = () => {
|
|
992
|
+
const elapsed = Date.now() - startTime;
|
|
993
|
+
const progress = Math.min(elapsed / duration, 1);
|
|
994
|
+
const eased = 1 - Math.pow(1 - progress, 3);
|
|
995
|
+
this.currentWidth = Math.round(fromWidth + deltaWidth * eased);
|
|
996
|
+
if (this.mode === "embedded") {
|
|
997
|
+
this.updateEmbeddedLayout();
|
|
998
|
+
} else if (this.floatingWindow) {
|
|
999
|
+
const bounds = this.floatingWindow.getBounds();
|
|
1000
|
+
this.floatingWindow.setBounds({ ...bounds, width: this.currentWidth });
|
|
1001
|
+
}
|
|
1002
|
+
if (progress < 1) {
|
|
1003
|
+
setImmediate(step);
|
|
1004
|
+
} else {
|
|
1005
|
+
this.currentWidth = toWidth;
|
|
1006
|
+
if (this.mode === "embedded") {
|
|
1007
|
+
this.embeddedWidth = toWidth;
|
|
1008
|
+
if (this.visible) {
|
|
1009
|
+
this.updateHostWindowMinWidth(true);
|
|
1010
|
+
}
|
|
1011
|
+
} else {
|
|
1012
|
+
this.floatingWidth = toWidth;
|
|
1013
|
+
}
|
|
1014
|
+
this.scheduleSaveState();
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
step();
|
|
1018
|
+
}
|
|
1019
|
+
/**
|
|
1020
|
+
* Get width
|
|
1021
|
+
*/
|
|
1022
|
+
getWidth() {
|
|
1023
|
+
return this.currentWidth;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Get attach state (for floating mode)
|
|
1027
|
+
* Note: Attachment feature not implemented yet - see FloatingWindow.ts for reference
|
|
1028
|
+
*/
|
|
1029
|
+
getAttachState() {
|
|
1030
|
+
return "unavailable";
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Toggle attach state
|
|
1034
|
+
* Note: Attachment feature not implemented yet - see FloatingWindow.ts for reference
|
|
1035
|
+
*/
|
|
1036
|
+
toggleAttach() {
|
|
1037
|
+
return "unavailable";
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Get webContents (for IPC)
|
|
1041
|
+
*/
|
|
1042
|
+
getWebContents() {
|
|
1043
|
+
return this.webContents;
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Destroy panel
|
|
1047
|
+
*/
|
|
1048
|
+
destroy() {
|
|
1049
|
+
this.cleanupHostWindowListeners();
|
|
1050
|
+
this.registeredShortcuts.forEach((key) => globalShortcut2.unregister(key));
|
|
1051
|
+
this.registeredShortcuts = [];
|
|
1052
|
+
if (this.stateSaveTimer) {
|
|
1053
|
+
clearTimeout(this.stateSaveTimer);
|
|
1054
|
+
this.stateSaveTimer = null;
|
|
1055
|
+
}
|
|
1056
|
+
if (this.floatingWindow) {
|
|
1057
|
+
this.floatingWindow.destroy();
|
|
1058
|
+
this.floatingWindow = null;
|
|
1059
|
+
this.floatingView = null;
|
|
1060
|
+
}
|
|
1061
|
+
if (this.embeddedView && this.embeddedModeAvailable) {
|
|
1062
|
+
const hostWindow = this.config.hostWindow;
|
|
1063
|
+
try {
|
|
1064
|
+
hostWindow.contentView.removeChildView(this.embeddedView);
|
|
1065
|
+
} catch {
|
|
1066
|
+
}
|
|
1067
|
+
this.embeddedView = null;
|
|
1068
|
+
}
|
|
1069
|
+
this.sharedView = null;
|
|
1070
|
+
if (activeInstance2 === this) {
|
|
1071
|
+
activeInstance2 = null;
|
|
1072
|
+
this.cleanupIpcHandlers();
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
// === Private: Initialization ===
|
|
1076
|
+
normalizeConfig(config) {
|
|
1077
|
+
return {
|
|
1078
|
+
hostWindow: config.hostWindow,
|
|
1079
|
+
hostMainView: config.hostMainView,
|
|
1080
|
+
initialMode: config.initialMode ?? "embedded",
|
|
1081
|
+
position: config.position ?? "right",
|
|
1082
|
+
width: config.width ?? 360,
|
|
1083
|
+
minWidth: config.minWidth ?? 240,
|
|
1084
|
+
minHostContentWidth: config.minHostContentWidth ?? 0,
|
|
1085
|
+
resizable: config.resizable ?? true,
|
|
1086
|
+
preloadPath: config.preloadPath,
|
|
1087
|
+
rendererPath: config.rendererPath,
|
|
1088
|
+
devMode: config.devMode ?? false,
|
|
1089
|
+
getClient: config.getClient,
|
|
1090
|
+
getAgentId: config.getAgentId,
|
|
1091
|
+
onLayoutChange: config.onLayoutChange,
|
|
1092
|
+
shortcuts: {
|
|
1093
|
+
toggle: config.shortcuts?.toggle ?? "CommandOrControl+Shift+Space",
|
|
1094
|
+
toggleMode: config.shortcuts?.toggleMode ?? "CommandOrControl+Shift+E"
|
|
1095
|
+
},
|
|
1096
|
+
stateKey: config.stateKey ?? "default",
|
|
1097
|
+
uiConfig: config.uiConfig
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
checkEmbeddedModeAvailable() {
|
|
1101
|
+
const isBaseWindow = this.config.hostWindow.constructor.name === "BaseWindow";
|
|
1102
|
+
const hasMainView = this.config.hostMainView !== void 0;
|
|
1103
|
+
return isBaseWindow && hasMainView;
|
|
1104
|
+
}
|
|
1105
|
+
determineInitialMode() {
|
|
1106
|
+
if (!this.embeddedModeAvailable) {
|
|
1107
|
+
return "floating";
|
|
1108
|
+
}
|
|
1109
|
+
return this.config.initialMode;
|
|
1110
|
+
}
|
|
1111
|
+
initWebContents() {
|
|
1112
|
+
if (!WebContentsViewClass) {
|
|
1113
|
+
throw new Error("[ChatPanel] WebContentsView not available. Requires Electron >= 30.0.0");
|
|
1114
|
+
}
|
|
1115
|
+
const view = this.getOrCreateSharedView();
|
|
1116
|
+
if (this.mode === "embedded" && this.embeddedModeAvailable) {
|
|
1117
|
+
this.embeddedView = view;
|
|
1118
|
+
} else {
|
|
1119
|
+
this.floatingWindow = this.createFloatingWindow();
|
|
1120
|
+
this.floatingView = view;
|
|
1121
|
+
this.floatingWindow.contentView.addChildView(view);
|
|
1122
|
+
this.updateFloatingViewBounds();
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
// === Private: Embedded Mode ===
|
|
1126
|
+
showEmbedded() {
|
|
1127
|
+
if (!this.embeddedView || !this.embeddedModeAvailable) return;
|
|
1128
|
+
const hostWindow = this.config.hostWindow;
|
|
1129
|
+
this.updateHostWindowMinWidth(true);
|
|
1130
|
+
this.expandHostWindowIfNeeded();
|
|
1131
|
+
hostWindow.contentView.addChildView(this.embeddedView);
|
|
1132
|
+
this.updateEmbeddedLayout();
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Update host window minimum width constraint based on chat panel visibility.
|
|
1136
|
+
* When chat is visible: minWidth = minHostContentWidth + chatWidth
|
|
1137
|
+
* When chat is hidden: minWidth = minHostContentWidth
|
|
1138
|
+
*/
|
|
1139
|
+
updateHostWindowMinWidth(chatVisible) {
|
|
1140
|
+
const { minHostContentWidth } = this.config;
|
|
1141
|
+
if (minHostContentWidth <= 0) return;
|
|
1142
|
+
const hostWindow = this.config.hostWindow;
|
|
1143
|
+
const [, minHeight] = hostWindow.getMinimumSize();
|
|
1144
|
+
const newMinWidth = chatVisible ? minHostContentWidth + this.currentWidth : minHostContentWidth;
|
|
1145
|
+
hostWindow.setMinimumSize(newMinWidth, minHeight);
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Expand host window to the right if main content area would be too narrow.
|
|
1149
|
+
* If expansion would exceed screen bounds, shift window left as needed.
|
|
1150
|
+
*/
|
|
1151
|
+
expandHostWindowIfNeeded() {
|
|
1152
|
+
const { minHostContentWidth } = this.config;
|
|
1153
|
+
if (minHostContentWidth <= 0) return;
|
|
1154
|
+
const hostWindow = this.config.hostWindow;
|
|
1155
|
+
const bounds = hostWindow.getBounds();
|
|
1156
|
+
const requiredWidth = minHostContentWidth + this.currentWidth;
|
|
1157
|
+
if (bounds.width < requiredWidth) {
|
|
1158
|
+
const display = screen2.getDisplayMatching(bounds);
|
|
1159
|
+
const screenBounds = display.workArea;
|
|
1160
|
+
const screenRight = screenBounds.x + screenBounds.width;
|
|
1161
|
+
let newX = bounds.x;
|
|
1162
|
+
const newWidth = requiredWidth;
|
|
1163
|
+
if (newX + newWidth > screenRight) {
|
|
1164
|
+
newX = Math.max(screenBounds.x, screenRight - newWidth);
|
|
1165
|
+
}
|
|
1166
|
+
hostWindow.setBounds({
|
|
1167
|
+
x: newX,
|
|
1168
|
+
y: bounds.y,
|
|
1169
|
+
width: newWidth,
|
|
1170
|
+
height: bounds.height
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
hideEmbedded() {
|
|
1175
|
+
if (!this.embeddedView || !this.embeddedModeAvailable) return;
|
|
1176
|
+
const hostWindow = this.config.hostWindow;
|
|
1177
|
+
hostWindow.contentView.removeChildView(this.embeddedView);
|
|
1178
|
+
this.updateHostWindowMinWidth(false);
|
|
1179
|
+
this.updateHostLayoutForHidden();
|
|
1180
|
+
}
|
|
1181
|
+
updateEmbeddedLayout() {
|
|
1182
|
+
if (!this.embeddedView || !this.visible) return;
|
|
1183
|
+
const hostWindow = this.config.hostWindow;
|
|
1184
|
+
const { width, height } = hostWindow.getBounds();
|
|
1185
|
+
const chatWidth = this.currentWidth;
|
|
1186
|
+
const mainWidth = width - chatWidth;
|
|
1187
|
+
if (this.config.position === "left") {
|
|
1188
|
+
this.embeddedView.setBounds({ x: 0, y: 0, width: chatWidth, height });
|
|
1189
|
+
} else {
|
|
1190
|
+
this.embeddedView.setBounds({ x: mainWidth, y: 0, width: chatWidth, height });
|
|
1191
|
+
}
|
|
1192
|
+
this.config.onLayoutChange?.({
|
|
1193
|
+
mainWidth,
|
|
1194
|
+
chatWidth,
|
|
1195
|
+
chatVisible: true
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
updateHostLayoutForHidden() {
|
|
1199
|
+
const hostWindow = this.config.hostWindow;
|
|
1200
|
+
const { width } = hostWindow.getBounds();
|
|
1201
|
+
this.config.onLayoutChange?.({
|
|
1202
|
+
mainWidth: width,
|
|
1203
|
+
chatWidth: 0,
|
|
1204
|
+
chatVisible: false
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
// === Private: Floating Mode ===
|
|
1208
|
+
createFloatingWindow() {
|
|
1209
|
+
const isWindows = process.platform === "win32";
|
|
1210
|
+
const windowOptions = {
|
|
1211
|
+
width: this.floatingWidth,
|
|
1212
|
+
height: this.floatingHeight,
|
|
1213
|
+
frame: false,
|
|
1214
|
+
transparent: !isWindows,
|
|
1215
|
+
backgroundColor: isWindows ? "#1F1F1F" : "#00000000",
|
|
1216
|
+
alwaysOnTop: true,
|
|
1217
|
+
skipTaskbar: true,
|
|
1218
|
+
show: false,
|
|
1219
|
+
resizable: this.config.resizable,
|
|
1220
|
+
minWidth: this.config.minWidth,
|
|
1221
|
+
minHeight: 300
|
|
1222
|
+
};
|
|
1223
|
+
if (typeof this.floatingX === "number" && typeof this.floatingY === "number") {
|
|
1224
|
+
windowOptions.x = this.floatingX;
|
|
1225
|
+
windowOptions.y = this.floatingY;
|
|
1226
|
+
}
|
|
1227
|
+
const win = new BrowserWindow2(windowOptions);
|
|
1228
|
+
win.on("resize", () => {
|
|
1229
|
+
this.updateFloatingViewBounds();
|
|
1230
|
+
this.scheduleSaveState();
|
|
1231
|
+
});
|
|
1232
|
+
win.on("move", () => {
|
|
1233
|
+
this.scheduleSaveState();
|
|
1234
|
+
});
|
|
1235
|
+
return win;
|
|
1236
|
+
}
|
|
1237
|
+
showFloating() {
|
|
1238
|
+
const isNewWindow = !this.floatingWindow;
|
|
1239
|
+
if (isNewWindow) {
|
|
1240
|
+
this.floatingWindow = this.createFloatingWindow();
|
|
1241
|
+
const view = this.getOrCreateSharedView();
|
|
1242
|
+
this.floatingView = view;
|
|
1243
|
+
this.floatingWindow.contentView.addChildView(view);
|
|
1244
|
+
this.updateFloatingViewBounds();
|
|
1245
|
+
if (typeof this.floatingX !== "number" || typeof this.floatingY !== "number") {
|
|
1246
|
+
this.positionFloatingWindow();
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
this.floatingWindow.show();
|
|
1250
|
+
this.floatingWindow.focus();
|
|
1251
|
+
}
|
|
1252
|
+
hideFloating() {
|
|
1253
|
+
if (this.floatingWindow) {
|
|
1254
|
+
this.floatingWindow.hide();
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
updateFloatingViewBounds() {
|
|
1258
|
+
if (!this.floatingWindow || !this.floatingView) return;
|
|
1259
|
+
const { width, height } = this.floatingWindow.getBounds();
|
|
1260
|
+
this.floatingView.setBounds({ x: 0, y: 0, width, height });
|
|
1261
|
+
}
|
|
1262
|
+
positionFloatingWindow() {
|
|
1263
|
+
if (!this.floatingWindow) return;
|
|
1264
|
+
const hostBounds = this.getHostWindowBounds();
|
|
1265
|
+
const x = this.config.position === "left" ? hostBounds.x - this.currentWidth : hostBounds.x + hostBounds.width;
|
|
1266
|
+
this.floatingWindow.setBounds({
|
|
1267
|
+
x,
|
|
1268
|
+
y: hostBounds.y,
|
|
1269
|
+
width: this.currentWidth,
|
|
1270
|
+
height: hostBounds.height
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
getHostWindowBounds() {
|
|
1274
|
+
return this.config.hostWindow.getBounds();
|
|
1275
|
+
}
|
|
1276
|
+
getOrCreateSharedView() {
|
|
1277
|
+
if (!this.sharedView) {
|
|
1278
|
+
this.sharedView = new WebContentsViewClass({
|
|
1279
|
+
webPreferences: {
|
|
1280
|
+
preload: this.config.preloadPath,
|
|
1281
|
+
contextIsolation: true,
|
|
1282
|
+
nodeIntegration: false
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
this.webContents = this.sharedView.webContents;
|
|
1286
|
+
this.webContents.on("context-menu", (event, params) => {
|
|
1287
|
+
if (this.config.devMode) {
|
|
1288
|
+
event.preventDefault();
|
|
1289
|
+
const { Menu, MenuItem } = __require("electron");
|
|
1290
|
+
const menu = new Menu();
|
|
1291
|
+
menu.append(new MenuItem({
|
|
1292
|
+
label: "Inspect Element",
|
|
1293
|
+
click: () => {
|
|
1294
|
+
this.webContents.inspectElement(params.x, params.y);
|
|
1295
|
+
}
|
|
1296
|
+
}));
|
|
1297
|
+
menu.append(new MenuItem({
|
|
1298
|
+
label: "Toggle Developer Tools",
|
|
1299
|
+
click: () => {
|
|
1300
|
+
if (this.webContents.isDevToolsOpened()) {
|
|
1301
|
+
this.webContents.closeDevTools();
|
|
1302
|
+
} else {
|
|
1303
|
+
this.webContents.openDevTools({ mode: "detach" });
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}));
|
|
1307
|
+
menu.popup();
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
if (this.config.devMode) {
|
|
1311
|
+
this.webContents.loadURL(this.config.rendererPath);
|
|
1312
|
+
} else {
|
|
1313
|
+
this.webContents.loadFile(this.config.rendererPath);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return this.sharedView;
|
|
1317
|
+
}
|
|
1318
|
+
switchToEmbedded() {
|
|
1319
|
+
if (!this.embeddedModeAvailable) {
|
|
1320
|
+
console.warn("[ChatPanel] Cannot switch to embedded mode - not available");
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
const view = this.getOrCreateSharedView();
|
|
1324
|
+
if (this.floatingWindow) {
|
|
1325
|
+
try {
|
|
1326
|
+
this.floatingWindow.contentView.removeChildView(view);
|
|
1327
|
+
} catch {
|
|
1328
|
+
}
|
|
1329
|
+
this.floatingWindow.destroy();
|
|
1330
|
+
this.floatingWindow = null;
|
|
1331
|
+
}
|
|
1332
|
+
this.embeddedView = view;
|
|
1333
|
+
this.floatingView = null;
|
|
1334
|
+
}
|
|
1335
|
+
switchToFloating() {
|
|
1336
|
+
const view = this.getOrCreateSharedView();
|
|
1337
|
+
if (this.embeddedModeAvailable) {
|
|
1338
|
+
const hostWindow = this.config.hostWindow;
|
|
1339
|
+
try {
|
|
1340
|
+
hostWindow.contentView.removeChildView(view);
|
|
1341
|
+
} catch {
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
this.floatingWindow = this.createFloatingWindow();
|
|
1345
|
+
this.floatingView = view;
|
|
1346
|
+
this.floatingWindow.contentView.addChildView(view);
|
|
1347
|
+
this.updateFloatingViewBounds();
|
|
1348
|
+
this.embeddedView = null;
|
|
1349
|
+
this.updateHostLayoutForHidden();
|
|
1350
|
+
}
|
|
1351
|
+
// === Private: Notifications ===
|
|
1352
|
+
notifyVisibilityChange() {
|
|
1353
|
+
this.webContents.send("chatPanel:visibilityChanged", { visible: this.visible });
|
|
1354
|
+
}
|
|
1355
|
+
notifyModeChange() {
|
|
1356
|
+
this.webContents.send("chatPanel:modeChanged", { mode: this.mode });
|
|
1357
|
+
}
|
|
1358
|
+
// === Private: Shortcuts ===
|
|
1359
|
+
setupShortcuts() {
|
|
1360
|
+
const { shortcuts } = this.config;
|
|
1361
|
+
if (shortcuts.toggle !== false) {
|
|
1362
|
+
const key = shortcuts.toggle;
|
|
1363
|
+
if (globalShortcut2.register(key, () => this.toggle())) {
|
|
1364
|
+
this.registeredShortcuts.push(key);
|
|
1365
|
+
} else {
|
|
1366
|
+
console.warn(`[ChatPanel] Failed to register shortcut: ${key}`);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
if (shortcuts.toggleMode !== false) {
|
|
1370
|
+
const key = shortcuts.toggleMode;
|
|
1371
|
+
if (globalShortcut2.register(key, () => this.toggleMode())) {
|
|
1372
|
+
this.registeredShortcuts.push(key);
|
|
1373
|
+
} else {
|
|
1374
|
+
console.warn(`[ChatPanel] Failed to register shortcut: ${key}`);
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
// === Private: State Persistence ===
|
|
1379
|
+
getStatePath() {
|
|
1380
|
+
const stateDir = path2.join(os2.homedir(), ".sanqian-chat");
|
|
1381
|
+
const safeKey = this.config.stateKey.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
1382
|
+
return path2.join(stateDir, `${safeKey}-panel.json`);
|
|
1383
|
+
}
|
|
1384
|
+
loadState() {
|
|
1385
|
+
try {
|
|
1386
|
+
const statePath = this.getStatePath();
|
|
1387
|
+
const raw = fs2.readFileSync(statePath, "utf8");
|
|
1388
|
+
const state = JSON.parse(raw);
|
|
1389
|
+
if (typeof state.embeddedWidth === "number") {
|
|
1390
|
+
this.embeddedWidth = Math.max(this.config.minWidth, state.embeddedWidth);
|
|
1391
|
+
} else if (typeof state.width === "number") {
|
|
1392
|
+
this.embeddedWidth = Math.max(this.config.minWidth, state.width);
|
|
1393
|
+
}
|
|
1394
|
+
if (typeof state.floatingWidth === "number") {
|
|
1395
|
+
this.floatingWidth = Math.max(this.config.minWidth, state.floatingWidth);
|
|
1396
|
+
}
|
|
1397
|
+
if (typeof state.floatingHeight === "number") {
|
|
1398
|
+
this.floatingHeight = Math.max(300, state.floatingHeight);
|
|
1399
|
+
}
|
|
1400
|
+
if (typeof state.floatingX === "number") {
|
|
1401
|
+
this.floatingX = state.floatingX;
|
|
1402
|
+
}
|
|
1403
|
+
if (typeof state.floatingY === "number") {
|
|
1404
|
+
this.floatingY = state.floatingY;
|
|
1405
|
+
}
|
|
1406
|
+
if (state.preferredMode && this.embeddedModeAvailable) {
|
|
1407
|
+
this.mode = state.preferredMode;
|
|
1408
|
+
}
|
|
1409
|
+
} catch {
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
scheduleSaveState() {
|
|
1413
|
+
if (this.stateSaveTimer) clearTimeout(this.stateSaveTimer);
|
|
1414
|
+
this.stateSaveTimer = setTimeout(() => {
|
|
1415
|
+
this.stateSaveTimer = null;
|
|
1416
|
+
this.saveState();
|
|
1417
|
+
}, 200);
|
|
1418
|
+
}
|
|
1419
|
+
saveState() {
|
|
1420
|
+
try {
|
|
1421
|
+
const statePath = this.getStatePath();
|
|
1422
|
+
if (this.floatingWindow) {
|
|
1423
|
+
const bounds = this.floatingWindow.getBounds();
|
|
1424
|
+
this.floatingWidth = bounds.width;
|
|
1425
|
+
this.floatingHeight = bounds.height;
|
|
1426
|
+
this.floatingX = bounds.x;
|
|
1427
|
+
this.floatingY = bounds.y;
|
|
1428
|
+
}
|
|
1429
|
+
const state = {
|
|
1430
|
+
embeddedWidth: this.embeddedWidth,
|
|
1431
|
+
floatingWidth: this.floatingWidth,
|
|
1432
|
+
floatingHeight: this.floatingHeight,
|
|
1433
|
+
floatingX: this.floatingX,
|
|
1434
|
+
floatingY: this.floatingY,
|
|
1435
|
+
preferredMode: this.mode
|
|
1436
|
+
};
|
|
1437
|
+
fs2.mkdirSync(path2.dirname(statePath), { recursive: true });
|
|
1438
|
+
fs2.writeFileSync(statePath, JSON.stringify(state, null, 2), "utf8");
|
|
1439
|
+
} catch (e) {
|
|
1440
|
+
console.warn("[ChatPanel] Failed to save state:", e);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
// === Private: SDK Access ===
|
|
1444
|
+
getSdk() {
|
|
1445
|
+
const client = this.config.getClient();
|
|
1446
|
+
return client;
|
|
1447
|
+
}
|
|
1448
|
+
// === Private: IPC Handlers ===
|
|
1449
|
+
setupIpcHandlers() {
|
|
1450
|
+
if (ipcHandlersRegistered2) return;
|
|
1451
|
+
ipcHandlersRegistered2 = true;
|
|
1452
|
+
ipcMain2.handle("sanqian-chat:connect", async () => {
|
|
1453
|
+
try {
|
|
1454
|
+
const sdk = activeInstance2?.getSdk();
|
|
1455
|
+
if (!sdk) throw new Error("SDK not available");
|
|
1456
|
+
await sdk.ensureReady();
|
|
1457
|
+
return { success: true };
|
|
1458
|
+
} catch (e) {
|
|
1459
|
+
return { success: false, error: e instanceof Error ? e.message : "Connection failed" };
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
ipcMain2.handle("sanqian-chat:isConnected", () => {
|
|
1463
|
+
const sdk = activeInstance2?.getSdk();
|
|
1464
|
+
return sdk?.isConnected() ?? false;
|
|
1465
|
+
});
|
|
1466
|
+
ipcMain2.handle("sanqian-chat:stream", async (event, params) => {
|
|
1467
|
+
const webContents = event.sender;
|
|
1468
|
+
const { streamId, messages, conversationId, agentId: requestedAgentId } = params;
|
|
1469
|
+
const sdk = activeInstance2?.getSdk();
|
|
1470
|
+
const agentId = requestedAgentId ?? activeInstance2?.config.getAgentId();
|
|
1471
|
+
if (!sdk || !agentId) {
|
|
1472
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: "SDK or agent not ready" } });
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
const streamState = { cancelled: false, runId: null };
|
|
1476
|
+
activeInstance2?.activeStreams.set(streamId, streamState);
|
|
1477
|
+
try {
|
|
1478
|
+
await sdk.ensureReady();
|
|
1479
|
+
const sdkMessages = messages.map((m) => ({ role: m.role, content: m.content }));
|
|
1480
|
+
const stream = sdk.chatStream(agentId, sdkMessages, { conversationId, persistHistory: true });
|
|
1481
|
+
for await (const evt of stream) {
|
|
1482
|
+
if (streamState.cancelled) break;
|
|
1483
|
+
if (activeInstance2?.config.devMode) {
|
|
1484
|
+
console.log("[ChatPanel] SDK event:", evt.type, JSON.stringify(evt).slice(0, 200));
|
|
1485
|
+
}
|
|
1486
|
+
switch (evt.type) {
|
|
1487
|
+
case "start": {
|
|
1488
|
+
const startEvt = evt;
|
|
1489
|
+
if (startEvt.run_id) {
|
|
1490
|
+
streamState.runId = startEvt.run_id;
|
|
1491
|
+
}
|
|
1492
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "start", run_id: startEvt.run_id } });
|
|
1493
|
+
break;
|
|
1494
|
+
}
|
|
1495
|
+
case "text":
|
|
1496
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "text", content: evt.content } });
|
|
1497
|
+
break;
|
|
1498
|
+
case "thinking":
|
|
1499
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "thinking", content: evt.content } });
|
|
1500
|
+
break;
|
|
1501
|
+
case "tool_call":
|
|
1502
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_call", tool_call: evt.tool_call } });
|
|
1503
|
+
break;
|
|
1504
|
+
case "tool_result":
|
|
1505
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "tool_result", tool_call_id: evt.tool_call_id, result: evt.result } });
|
|
1506
|
+
break;
|
|
1507
|
+
case "done":
|
|
1508
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "done", conversationId: evt.conversationId, title: evt.title } });
|
|
1509
|
+
break;
|
|
1510
|
+
case "error":
|
|
1511
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: evt.error } });
|
|
1512
|
+
break;
|
|
1513
|
+
default: {
|
|
1514
|
+
const anyEvt = evt;
|
|
1515
|
+
if (anyEvt.type === "interrupt") {
|
|
1516
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "interrupt", interrupt_type: anyEvt.interrupt_type, interrupt_payload: anyEvt.interrupt_payload, run_id: anyEvt.run_id } });
|
|
1517
|
+
}
|
|
1518
|
+
break;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
} catch (e) {
|
|
1523
|
+
if (!streamState.cancelled) {
|
|
1524
|
+
webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: e instanceof Error ? e.message : "Stream error" } });
|
|
1525
|
+
}
|
|
1526
|
+
} finally {
|
|
1527
|
+
activeInstance2?.activeStreams.delete(streamId);
|
|
1528
|
+
}
|
|
1529
|
+
});
|
|
1530
|
+
ipcMain2.handle("sanqian-chat:cancelStream", (_, params) => {
|
|
1531
|
+
const stream = activeInstance2?.activeStreams.get(params.streamId);
|
|
1532
|
+
if (stream) {
|
|
1533
|
+
stream.cancelled = true;
|
|
1534
|
+
if (stream.runId) {
|
|
1535
|
+
const sdk = activeInstance2?.getSdk();
|
|
1536
|
+
if (sdk) {
|
|
1537
|
+
try {
|
|
1538
|
+
sdk.cancelRun(stream.runId);
|
|
1539
|
+
} catch (e) {
|
|
1540
|
+
console.warn("[ChatPanel] Failed to cancel run:", e);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
activeInstance2?.activeStreams.delete(params.streamId);
|
|
1545
|
+
}
|
|
1546
|
+
return { success: true };
|
|
1547
|
+
});
|
|
1548
|
+
ipcMain2.handle("sanqian-chat:hitlResponse", (_, params) => {
|
|
1549
|
+
const sdk = activeInstance2?.getSdk();
|
|
1550
|
+
if (sdk && params.runId) {
|
|
1551
|
+
sdk.sendHitlResponse(params.runId, params.response);
|
|
1552
|
+
}
|
|
1553
|
+
return { success: true };
|
|
1554
|
+
});
|
|
1555
|
+
ipcMain2.handle("sanqian-chat:listConversations", async (_, params) => {
|
|
1556
|
+
const sdk = activeInstance2?.getSdk();
|
|
1557
|
+
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
1558
|
+
try {
|
|
1559
|
+
const result = await sdk.listConversations({
|
|
1560
|
+
limit: params?.limit,
|
|
1561
|
+
offset: params?.offset
|
|
1562
|
+
});
|
|
1563
|
+
return { success: true, data: result };
|
|
1564
|
+
} catch (e) {
|
|
1565
|
+
return { success: false, error: e instanceof Error ? e.message : "Failed to list" };
|
|
1566
|
+
}
|
|
1567
|
+
});
|
|
1568
|
+
ipcMain2.handle("sanqian-chat:getConversation", async (_, params) => {
|
|
1569
|
+
const sdk = activeInstance2?.getSdk();
|
|
1570
|
+
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
1571
|
+
try {
|
|
1572
|
+
const result = await sdk.getConversation(params.conversationId, { messageLimit: params.messageLimit });
|
|
1573
|
+
let messages = result?.messages;
|
|
1574
|
+
const sdkWithHistory = sdk;
|
|
1575
|
+
if (typeof sdkWithHistory.getMessages === "function") {
|
|
1576
|
+
try {
|
|
1577
|
+
const history = await sdkWithHistory.getMessages(params.conversationId, { limit: params.messageLimit });
|
|
1578
|
+
if (history?.messages && history.messages.length > 0) {
|
|
1579
|
+
messages = history.messages;
|
|
1580
|
+
}
|
|
1581
|
+
} catch (e) {
|
|
1582
|
+
console.warn("[ChatPanel] getMessages failed, fallback to getConversation:", e);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
return { success: true, data: { ...result, messages } };
|
|
1586
|
+
} catch (e) {
|
|
1587
|
+
return { success: false, error: e instanceof Error ? e.message : "Failed to get" };
|
|
1588
|
+
}
|
|
1589
|
+
});
|
|
1590
|
+
ipcMain2.handle("sanqian-chat:deleteConversation", async (_, params) => {
|
|
1591
|
+
const sdk = activeInstance2?.getSdk();
|
|
1592
|
+
if (!sdk) return { success: false, error: "SDK not ready" };
|
|
1593
|
+
try {
|
|
1594
|
+
await sdk.deleteConversation(params.conversationId);
|
|
1595
|
+
return { success: true };
|
|
1596
|
+
} catch (e) {
|
|
1597
|
+
return { success: false, error: e instanceof Error ? e.message : "Failed to delete" };
|
|
1598
|
+
}
|
|
1599
|
+
});
|
|
1600
|
+
ipcMain2.handle("sanqian-chat:hide", () => {
|
|
1601
|
+
activeInstance2?.hide();
|
|
1602
|
+
return { success: true };
|
|
1603
|
+
});
|
|
1604
|
+
ipcMain2.handle("sanqian-chat:setAlwaysOnTop", (_event, params) => {
|
|
1605
|
+
if (!activeInstance2?.floatingWindow) return { success: false, error: "Floating window not available" };
|
|
1606
|
+
activeInstance2.floatingWindow.setAlwaysOnTop(params.alwaysOnTop);
|
|
1607
|
+
return { success: true };
|
|
1608
|
+
});
|
|
1609
|
+
ipcMain2.handle("sanqian-chat:getAlwaysOnTop", () => {
|
|
1610
|
+
if (!activeInstance2?.floatingWindow) return { success: false, error: "Floating window not available" };
|
|
1611
|
+
return { success: true, data: activeInstance2.floatingWindow.isAlwaysOnTop() };
|
|
1612
|
+
});
|
|
1613
|
+
ipcMain2.handle("sanqian-chat:getUiConfig", () => {
|
|
1614
|
+
return { success: true, data: activeInstance2?.config.uiConfig ?? null };
|
|
1615
|
+
});
|
|
1616
|
+
ipcMain2.handle("sanqian-chat:setBackgroundColor", (_event, params) => {
|
|
1617
|
+
if (!activeInstance2?.floatingWindow) return { success: false, error: "Floating window not available" };
|
|
1618
|
+
try {
|
|
1619
|
+
activeInstance2.floatingWindow.setBackgroundColor(params.color);
|
|
1620
|
+
return { success: true };
|
|
1621
|
+
} catch (e) {
|
|
1622
|
+
return { success: false, error: e instanceof Error ? e.message : "Failed to set background color" };
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
ipcMain2.handle("chatPanel:getMode", () => {
|
|
1626
|
+
return activeInstance2?.getMode() ?? "floating";
|
|
1627
|
+
});
|
|
1628
|
+
ipcMain2.handle("chatPanel:setMode", (_, mode) => {
|
|
1629
|
+
activeInstance2?.setMode(mode);
|
|
1630
|
+
return { success: true };
|
|
1631
|
+
});
|
|
1632
|
+
ipcMain2.handle("chatPanel:toggleMode", () => {
|
|
1633
|
+
return activeInstance2?.toggleMode() ?? "floating";
|
|
1634
|
+
});
|
|
1635
|
+
ipcMain2.handle("chatPanel:isVisible", () => {
|
|
1636
|
+
return activeInstance2?.isVisible() ?? false;
|
|
1637
|
+
});
|
|
1638
|
+
ipcMain2.handle("chatPanel:show", () => {
|
|
1639
|
+
activeInstance2?.show();
|
|
1640
|
+
return { success: true };
|
|
1641
|
+
});
|
|
1642
|
+
ipcMain2.handle("chatPanel:hide", () => {
|
|
1643
|
+
activeInstance2?.hide();
|
|
1644
|
+
return { success: true };
|
|
1645
|
+
});
|
|
1646
|
+
ipcMain2.handle("chatPanel:toggle", () => {
|
|
1647
|
+
activeInstance2?.toggle();
|
|
1648
|
+
return { success: true };
|
|
1649
|
+
});
|
|
1650
|
+
ipcMain2.handle("chatPanel:getWidth", () => {
|
|
1651
|
+
return activeInstance2?.getWidth() ?? 360;
|
|
1652
|
+
});
|
|
1653
|
+
ipcMain2.handle("chatPanel:setWidth", (_, params) => {
|
|
1654
|
+
activeInstance2?.setWidth(params.width, params.animate);
|
|
1655
|
+
return { success: true };
|
|
1656
|
+
});
|
|
1657
|
+
ipcMain2.handle("chatPanel:onResizeEnd", () => {
|
|
1658
|
+
activeInstance2?.onResizeEnd();
|
|
1659
|
+
return { success: true };
|
|
1660
|
+
});
|
|
1661
|
+
ipcMain2.handle("chatPanel:getAttachState", () => {
|
|
1662
|
+
return { success: true, data: activeInstance2?.getAttachState() ?? "unavailable" };
|
|
1663
|
+
});
|
|
1664
|
+
ipcMain2.handle("chatPanel:toggleAttach", () => {
|
|
1665
|
+
const newState = activeInstance2?.toggleAttach() ?? "unavailable";
|
|
1666
|
+
return { success: true, data: newState };
|
|
1667
|
+
});
|
|
1668
|
+
ipcMain2.handle("chatPanel:getUiConfig", () => {
|
|
1669
|
+
return { success: true, data: activeInstance2?.config.uiConfig ?? null };
|
|
1670
|
+
});
|
|
1671
|
+
}
|
|
1672
|
+
cleanupIpcHandlers() {
|
|
1673
|
+
if (!ipcHandlersRegistered2) return;
|
|
1674
|
+
ipcMain2.removeHandler("sanqian-chat:connect");
|
|
1675
|
+
ipcMain2.removeHandler("sanqian-chat:isConnected");
|
|
1676
|
+
ipcMain2.removeHandler("sanqian-chat:stream");
|
|
1677
|
+
ipcMain2.removeHandler("sanqian-chat:cancelStream");
|
|
1678
|
+
ipcMain2.removeHandler("sanqian-chat:hitlResponse");
|
|
1679
|
+
ipcMain2.removeHandler("sanqian-chat:listConversations");
|
|
1680
|
+
ipcMain2.removeHandler("sanqian-chat:getConversation");
|
|
1681
|
+
ipcMain2.removeHandler("sanqian-chat:deleteConversation");
|
|
1682
|
+
ipcMain2.removeHandler("sanqian-chat:hide");
|
|
1683
|
+
ipcMain2.removeHandler("sanqian-chat:setAlwaysOnTop");
|
|
1684
|
+
ipcMain2.removeHandler("sanqian-chat:getAlwaysOnTop");
|
|
1685
|
+
ipcMain2.removeHandler("sanqian-chat:getUiConfig");
|
|
1686
|
+
ipcMain2.removeHandler("sanqian-chat:setBackgroundColor");
|
|
1687
|
+
ipcMain2.removeHandler("chatPanel:getMode");
|
|
1688
|
+
ipcMain2.removeHandler("chatPanel:setMode");
|
|
1689
|
+
ipcMain2.removeHandler("chatPanel:toggleMode");
|
|
1690
|
+
ipcMain2.removeHandler("chatPanel:isVisible");
|
|
1691
|
+
ipcMain2.removeHandler("chatPanel:show");
|
|
1692
|
+
ipcMain2.removeHandler("chatPanel:hide");
|
|
1693
|
+
ipcMain2.removeHandler("chatPanel:toggle");
|
|
1694
|
+
ipcMain2.removeHandler("chatPanel:getWidth");
|
|
1695
|
+
ipcMain2.removeHandler("chatPanel:setWidth");
|
|
1696
|
+
ipcMain2.removeHandler("chatPanel:onResizeEnd");
|
|
1697
|
+
ipcMain2.removeHandler("chatPanel:getAttachState");
|
|
1698
|
+
ipcMain2.removeHandler("chatPanel:toggleAttach");
|
|
1699
|
+
ipcMain2.removeHandler("chatPanel:getUiConfig");
|
|
1700
|
+
ipcHandlersRegistered2 = false;
|
|
1701
|
+
}
|
|
1702
|
+
};
|
|
1703
|
+
|
|
1704
|
+
// src/main/WindowAttachment.ts
|
|
1705
|
+
import { screen as screen3 } from "electron";
|
|
1706
|
+
var WindowAttachment = class {
|
|
1707
|
+
constructor(chatWindow, config) {
|
|
1708
|
+
this.pollTimer = null;
|
|
1709
|
+
this.reattachHandler = null;
|
|
1710
|
+
// Throttle state
|
|
1711
|
+
this.updatePending = false;
|
|
1712
|
+
this.throttledUpdatePosition = () => {
|
|
1713
|
+
if (this.updatePending) return;
|
|
1714
|
+
this.updatePending = true;
|
|
1715
|
+
setImmediate(() => {
|
|
1716
|
+
this.updatePosition();
|
|
1717
|
+
this.updatePending = false;
|
|
1718
|
+
});
|
|
1719
|
+
};
|
|
1720
|
+
// === Event Handlers ===
|
|
1721
|
+
this.handleTargetMinimize = () => {
|
|
1722
|
+
switch (this.config.onMinimize) {
|
|
1723
|
+
case "hide":
|
|
1724
|
+
this.chatWindow.hide();
|
|
1725
|
+
break;
|
|
1726
|
+
case "minimize":
|
|
1727
|
+
this.chatWindow.minimize();
|
|
1728
|
+
break;
|
|
1729
|
+
case "detach":
|
|
1730
|
+
break;
|
|
1731
|
+
}
|
|
1732
|
+
};
|
|
1733
|
+
this.handleTargetRestore = () => {
|
|
1734
|
+
if (this.config.onMinimize === "hide") {
|
|
1735
|
+
this.chatWindow.show();
|
|
1736
|
+
} else if (this.config.onMinimize === "minimize") {
|
|
1737
|
+
this.chatWindow.restore();
|
|
1738
|
+
}
|
|
1739
|
+
this.updatePosition();
|
|
1740
|
+
};
|
|
1741
|
+
this.handleTargetMaximize = () => {
|
|
1742
|
+
const targetBounds = this.config.window.getBounds();
|
|
1743
|
+
const display = screen3.getDisplayMatching(targetBounds);
|
|
1744
|
+
const chatWidth = this.chatWindow.getBounds().width;
|
|
1745
|
+
if (targetBounds.width + chatWidth + this.config.gap > display.workArea.width) {
|
|
1746
|
+
this.chatWindow.hide();
|
|
1747
|
+
} else {
|
|
1748
|
+
this.updatePosition();
|
|
1749
|
+
}
|
|
1750
|
+
};
|
|
1751
|
+
this.handleTargetUnmaximize = () => {
|
|
1752
|
+
if (!this.chatWindow.isVisible() && this.state.isAttached) {
|
|
1753
|
+
this.chatWindow.show();
|
|
1754
|
+
}
|
|
1755
|
+
this.updatePosition();
|
|
1756
|
+
};
|
|
1757
|
+
this.handleTargetEnterFullScreen = () => {
|
|
1758
|
+
this.chatWindow.hide();
|
|
1759
|
+
};
|
|
1760
|
+
this.handleTargetLeaveFullScreen = () => {
|
|
1761
|
+
if (this.state.isAttached) {
|
|
1762
|
+
this.chatWindow.show();
|
|
1763
|
+
this.updatePosition();
|
|
1764
|
+
}
|
|
1765
|
+
};
|
|
1766
|
+
this.handleTargetClose = () => {
|
|
1767
|
+
};
|
|
1768
|
+
this.handleTargetClosed = () => {
|
|
1769
|
+
this.detach();
|
|
1770
|
+
if (this.config.onClose === "hide") {
|
|
1771
|
+
this.chatWindow.hide();
|
|
1772
|
+
} else {
|
|
1773
|
+
this.chatWindow.destroy();
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
this.handleChatMove = () => {
|
|
1777
|
+
if (!this.state.isAttached || !this.state.lastTargetBounds) return;
|
|
1778
|
+
const chatBounds = this.chatWindow.getBounds();
|
|
1779
|
+
const expectedBounds = this.calculateAttachedBounds(
|
|
1780
|
+
this.state.lastTargetBounds,
|
|
1781
|
+
chatBounds
|
|
1782
|
+
);
|
|
1783
|
+
const dx = Math.abs(chatBounds.x - expectedBounds.x);
|
|
1784
|
+
const dy = Math.abs(chatBounds.y - expectedBounds.y);
|
|
1785
|
+
if (dx > 10 || dy > 10) {
|
|
1786
|
+
this.state.isAttached = false;
|
|
1787
|
+
this.onDetached();
|
|
1788
|
+
}
|
|
1789
|
+
};
|
|
1790
|
+
this.chatWindow = chatWindow;
|
|
1791
|
+
this.config = this.normalizeConfig(config);
|
|
1792
|
+
this.state = {
|
|
1793
|
+
isAttached: true,
|
|
1794
|
+
position: this.config.position,
|
|
1795
|
+
lastTargetBounds: null
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Start attachment listeners
|
|
1800
|
+
*/
|
|
1801
|
+
attach() {
|
|
1802
|
+
const target = this.config.window;
|
|
1803
|
+
target.on("move", this.throttledUpdatePosition);
|
|
1804
|
+
target.on("resize", this.throttledUpdatePosition);
|
|
1805
|
+
target.on("minimize", this.handleTargetMinimize);
|
|
1806
|
+
target.on("restore", this.handleTargetRestore);
|
|
1807
|
+
target.on("maximize", this.handleTargetMaximize);
|
|
1808
|
+
target.on("unmaximize", this.handleTargetUnmaximize);
|
|
1809
|
+
target.on("enter-full-screen", this.handleTargetEnterFullScreen);
|
|
1810
|
+
target.on("leave-full-screen", this.handleTargetLeaveFullScreen);
|
|
1811
|
+
target.on("close", this.handleTargetClose);
|
|
1812
|
+
target.on("closed", this.handleTargetClosed);
|
|
1813
|
+
if (this.config.allowDetach) {
|
|
1814
|
+
this.chatWindow.on("move", this.handleChatMove);
|
|
1815
|
+
}
|
|
1816
|
+
this.updatePosition();
|
|
1817
|
+
if (process.platform === "win32") {
|
|
1818
|
+
this.startPolling();
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
/**
|
|
1822
|
+
* Stop attachment listeners
|
|
1823
|
+
*/
|
|
1824
|
+
detach() {
|
|
1825
|
+
const target = this.config.window;
|
|
1826
|
+
target.off("move", this.throttledUpdatePosition);
|
|
1827
|
+
target.off("resize", this.throttledUpdatePosition);
|
|
1828
|
+
target.off("minimize", this.handleTargetMinimize);
|
|
1829
|
+
target.off("restore", this.handleTargetRestore);
|
|
1830
|
+
target.off("maximize", this.handleTargetMaximize);
|
|
1831
|
+
target.off("unmaximize", this.handleTargetUnmaximize);
|
|
1832
|
+
target.off("enter-full-screen", this.handleTargetEnterFullScreen);
|
|
1833
|
+
target.off("leave-full-screen", this.handleTargetLeaveFullScreen);
|
|
1834
|
+
target.off("close", this.handleTargetClose);
|
|
1835
|
+
target.off("closed", this.handleTargetClosed);
|
|
1836
|
+
if (this.config.allowDetach) {
|
|
1837
|
+
this.chatWindow.off("move", this.handleChatMove);
|
|
1838
|
+
}
|
|
1839
|
+
this.stopPolling();
|
|
1840
|
+
this.stopReattachListener();
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Update chat window position to match target
|
|
1844
|
+
*/
|
|
1845
|
+
updatePosition() {
|
|
1846
|
+
if (!this.state.isAttached) return;
|
|
1847
|
+
const target = this.config.window;
|
|
1848
|
+
if (target.isDestroyed() || target.isMinimized()) return;
|
|
1849
|
+
const targetBounds = target.getBounds();
|
|
1850
|
+
const chatBounds = this.chatWindow.getBounds();
|
|
1851
|
+
const newBounds = this.calculateAttachedBounds(targetBounds, chatBounds);
|
|
1852
|
+
const safeBounds = this.constrainToScreen(newBounds);
|
|
1853
|
+
this.chatWindow.setBounds(safeBounds);
|
|
1854
|
+
this.state.lastTargetBounds = targetBounds;
|
|
1855
|
+
}
|
|
1856
|
+
/**
|
|
1857
|
+
* Set state change callback
|
|
1858
|
+
*/
|
|
1859
|
+
onStateChange(callback) {
|
|
1860
|
+
this.onStateChangeCallback = callback;
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Toggle attach state
|
|
1864
|
+
*/
|
|
1865
|
+
toggle() {
|
|
1866
|
+
if (this.state.isAttached) {
|
|
1867
|
+
this.manualDetach();
|
|
1868
|
+
return "detached";
|
|
1869
|
+
} else {
|
|
1870
|
+
this.manualAttach();
|
|
1871
|
+
return "attached";
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Manual detach
|
|
1876
|
+
*/
|
|
1877
|
+
manualDetach() {
|
|
1878
|
+
this.state.isAttached = false;
|
|
1879
|
+
this.notifyStateChange("detached");
|
|
1880
|
+
if (this.config.allowReattach) {
|
|
1881
|
+
this.startReattachListener();
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Manual attach
|
|
1886
|
+
*/
|
|
1887
|
+
manualAttach() {
|
|
1888
|
+
this.state.isAttached = true;
|
|
1889
|
+
this.stopReattachListener();
|
|
1890
|
+
this.updatePosition();
|
|
1891
|
+
this.notifyStateChange("attached");
|
|
1892
|
+
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Get current attach state
|
|
1895
|
+
*/
|
|
1896
|
+
get isAttached() {
|
|
1897
|
+
return this.state.isAttached;
|
|
1898
|
+
}
|
|
1899
|
+
/**
|
|
1900
|
+
* Get current position
|
|
1901
|
+
*/
|
|
1902
|
+
get position() {
|
|
1903
|
+
return this.state.position;
|
|
1904
|
+
}
|
|
1905
|
+
// === Private Methods ===
|
|
1906
|
+
normalizeConfig(config) {
|
|
1907
|
+
return {
|
|
1908
|
+
window: config.window,
|
|
1909
|
+
position: config.position ?? "right",
|
|
1910
|
+
gap: config.gap ?? 0,
|
|
1911
|
+
syncSize: config.syncSize ?? true,
|
|
1912
|
+
onMinimize: config.onMinimize ?? "hide",
|
|
1913
|
+
onClose: config.onClose ?? "hide",
|
|
1914
|
+
allowDetach: config.allowDetach ?? true,
|
|
1915
|
+
allowReattach: config.allowReattach ?? true,
|
|
1916
|
+
reattachThreshold: config.reattachThreshold ?? 20
|
|
1917
|
+
};
|
|
1918
|
+
}
|
|
1919
|
+
calculateAttachedBounds(targetBounds, chatBounds) {
|
|
1920
|
+
const { position, gap, syncSize } = this.config;
|
|
1921
|
+
const result = { ...chatBounds };
|
|
1922
|
+
switch (position) {
|
|
1923
|
+
case "right":
|
|
1924
|
+
result.x = targetBounds.x + targetBounds.width + gap;
|
|
1925
|
+
result.y = targetBounds.y;
|
|
1926
|
+
if (syncSize) result.height = targetBounds.height;
|
|
1927
|
+
break;
|
|
1928
|
+
case "left":
|
|
1929
|
+
result.x = targetBounds.x - chatBounds.width - gap;
|
|
1930
|
+
result.y = targetBounds.y;
|
|
1931
|
+
if (syncSize) result.height = targetBounds.height;
|
|
1932
|
+
break;
|
|
1933
|
+
case "top":
|
|
1934
|
+
result.x = targetBounds.x;
|
|
1935
|
+
result.y = targetBounds.y - chatBounds.height - gap;
|
|
1936
|
+
if (syncSize) result.width = targetBounds.width;
|
|
1937
|
+
break;
|
|
1938
|
+
case "bottom":
|
|
1939
|
+
result.x = targetBounds.x;
|
|
1940
|
+
result.y = targetBounds.y + targetBounds.height + gap;
|
|
1941
|
+
if (syncSize) result.width = targetBounds.width;
|
|
1942
|
+
break;
|
|
1943
|
+
}
|
|
1944
|
+
return result;
|
|
1945
|
+
}
|
|
1946
|
+
constrainToScreen(bounds) {
|
|
1947
|
+
const display = screen3.getDisplayMatching(bounds);
|
|
1948
|
+
const workArea = display.workArea;
|
|
1949
|
+
return {
|
|
1950
|
+
x: Math.max(workArea.x, Math.min(bounds.x, workArea.x + workArea.width - bounds.width)),
|
|
1951
|
+
y: Math.max(workArea.y, Math.min(bounds.y, workArea.y + workArea.height - bounds.height)),
|
|
1952
|
+
width: Math.min(bounds.width, workArea.width),
|
|
1953
|
+
height: Math.min(bounds.height, workArea.height)
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
notifyStateChange(state) {
|
|
1957
|
+
this.onStateChangeCallback?.(state);
|
|
1958
|
+
}
|
|
1959
|
+
onDetached() {
|
|
1960
|
+
console.log("[WindowAttachment] Detached from target window");
|
|
1961
|
+
this.notifyStateChange("detached");
|
|
1962
|
+
if (this.config.allowReattach) {
|
|
1963
|
+
this.startReattachListener();
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
// === Reattach Listener ===
|
|
1967
|
+
startReattachListener() {
|
|
1968
|
+
this.stopReattachListener();
|
|
1969
|
+
this.reattachHandler = () => {
|
|
1970
|
+
if (this.state.isAttached) {
|
|
1971
|
+
this.stopReattachListener();
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
const target = this.config.window;
|
|
1975
|
+
if (target.isDestroyed()) {
|
|
1976
|
+
this.stopReattachListener();
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
const targetBounds = target.getBounds();
|
|
1980
|
+
const chatBounds = this.chatWindow.getBounds();
|
|
1981
|
+
const threshold = this.config.reattachThreshold;
|
|
1982
|
+
const nearRight = Math.abs(chatBounds.x - (targetBounds.x + targetBounds.width)) < threshold;
|
|
1983
|
+
const nearLeft = Math.abs(chatBounds.x + chatBounds.width - targetBounds.x) < threshold;
|
|
1984
|
+
const verticalOverlap = chatBounds.y < targetBounds.y + targetBounds.height && chatBounds.y + chatBounds.height > targetBounds.y;
|
|
1985
|
+
if (verticalOverlap && (nearRight || nearLeft)) {
|
|
1986
|
+
this.state.isAttached = true;
|
|
1987
|
+
this.state.position = nearRight ? "right" : "left";
|
|
1988
|
+
this.updatePosition();
|
|
1989
|
+
this.notifyStateChange("attached");
|
|
1990
|
+
this.stopReattachListener();
|
|
1991
|
+
console.log("[WindowAttachment] Reattached to target window");
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
this.chatWindow.on("moved", this.reattachHandler);
|
|
1995
|
+
}
|
|
1996
|
+
stopReattachListener() {
|
|
1997
|
+
if (this.reattachHandler) {
|
|
1998
|
+
this.chatWindow.off("moved", this.reattachHandler);
|
|
1999
|
+
this.reattachHandler = null;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
// === Windows Polling (Aero Snap workaround) ===
|
|
2003
|
+
startPolling() {
|
|
2004
|
+
this.pollTimer = setInterval(() => {
|
|
2005
|
+
if (!this.state.isAttached) return;
|
|
2006
|
+
const target = this.config.window;
|
|
2007
|
+
if (target.isDestroyed() || target.isMinimized()) return;
|
|
2008
|
+
const currentBounds = target.getBounds();
|
|
2009
|
+
const lastBounds = this.state.lastTargetBounds;
|
|
2010
|
+
if (lastBounds && (currentBounds.x !== lastBounds.x || currentBounds.y !== lastBounds.y || currentBounds.width !== lastBounds.width || currentBounds.height !== lastBounds.height)) {
|
|
2011
|
+
this.updatePosition();
|
|
2012
|
+
}
|
|
2013
|
+
}, 100);
|
|
2014
|
+
}
|
|
2015
|
+
stopPolling() {
|
|
2016
|
+
if (this.pollTimer) {
|
|
2017
|
+
clearInterval(this.pollTimer);
|
|
2018
|
+
this.pollTimer = null;
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
748
2022
|
export {
|
|
2023
|
+
ChatPanel,
|
|
749
2024
|
FloatingWindow,
|
|
750
|
-
SanqianAppClient
|
|
2025
|
+
SanqianAppClient,
|
|
2026
|
+
WindowAttachment
|
|
751
2027
|
};
|