@yushaw/sanqian-chat 0.2.4 → 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.
@@ -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 {
@@ -755,7 +762,1266 @@ var FloatingWindow = class _FloatingWindow {
755
762
  return this.window;
756
763
  }
757
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
+ };
758
2022
  export {
2023
+ ChatPanel,
759
2024
  FloatingWindow,
760
- SanqianAppClient
2025
+ SanqianAppClient,
2026
+ WindowAttachment
761
2027
  };