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