@yushaw/sanqian-chat 0.1.1 → 0.2.0

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,39 +1,239 @@
1
+ // src/main/client.ts
2
+ import { SanqianSDK } from "@yushaw/sanqian-sdk";
3
+ var SanqianAppClient = class {
4
+ constructor(config) {
5
+ const tools = (config.tools || []).map((t) => ({
6
+ name: t.name,
7
+ description: t.description,
8
+ parameters: t.parameters,
9
+ handler: t.handler
10
+ }));
11
+ const sdkConfig = {
12
+ appName: config.appName,
13
+ appVersion: config.appVersion,
14
+ displayName: config.displayName,
15
+ launchCommand: config.launchCommand,
16
+ debug: config.debug,
17
+ tools
18
+ };
19
+ this.sdk = new SanqianSDK(sdkConfig);
20
+ }
21
+ // ============================================================
22
+ // Connection Management
23
+ // ============================================================
24
+ /**
25
+ * Connect to Sanqian and register this application
26
+ */
27
+ async connect() {
28
+ await this.sdk.connect();
29
+ }
30
+ /**
31
+ * Disconnect from Sanqian
32
+ */
33
+ async disconnect() {
34
+ await this.sdk.disconnect();
35
+ }
36
+ /**
37
+ * Check if connected to Sanqian
38
+ */
39
+ isConnected() {
40
+ return this.sdk.isConnected();
41
+ }
42
+ /**
43
+ * Ensure SDK is ready (connects if needed, waits for registration)
44
+ */
45
+ async ensureReady() {
46
+ await this.sdk.ensureReady();
47
+ }
48
+ // ============================================================
49
+ // Reconnection Control (for Chat UI)
50
+ // ============================================================
51
+ /**
52
+ * Request persistent connection (enables auto-reconnect)
53
+ * Call when a component needs the connection to stay alive
54
+ */
55
+ acquireReconnect() {
56
+ this.sdk.acquireReconnect();
57
+ }
58
+ /**
59
+ * Release persistent connection request
60
+ * Call when a component no longer needs the connection
61
+ */
62
+ releaseReconnect() {
63
+ this.sdk.releaseReconnect();
64
+ }
65
+ // ============================================================
66
+ // Agent Management
67
+ // ============================================================
68
+ /**
69
+ * Create or update a private agent
70
+ * @returns The full agent ID (app_name:agent_id)
71
+ */
72
+ async createAgent(config) {
73
+ const sdkConfig = {
74
+ agent_id: config.agentId,
75
+ name: config.name,
76
+ description: config.description,
77
+ system_prompt: config.systemPrompt,
78
+ tools: config.tools
79
+ };
80
+ const result = await this.sdk.createAgent(sdkConfig);
81
+ return { agentId: result.agent_id };
82
+ }
83
+ // ============================================================
84
+ // Embedding
85
+ // ============================================================
86
+ /**
87
+ * Get embedding configuration from Sanqian
88
+ * @returns Embedding config or null if not available
89
+ */
90
+ async getEmbeddingConfig() {
91
+ try {
92
+ const config = await this.sdk.getEmbeddingConfig();
93
+ return {
94
+ available: config.available,
95
+ apiUrl: config.apiUrl,
96
+ apiKey: config.apiKey,
97
+ modelName: config.modelName,
98
+ dimensions: config.dimensions
99
+ };
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+ // ============================================================
105
+ // Events
106
+ // ============================================================
107
+ /**
108
+ * Subscribe to client events
109
+ */
110
+ on(event, handler) {
111
+ this.sdk.on(event, handler);
112
+ return this;
113
+ }
114
+ /**
115
+ * Remove all event listeners
116
+ */
117
+ removeAllListeners() {
118
+ this.sdk.removeAllListeners();
119
+ }
120
+ // ============================================================
121
+ // Chat API
122
+ // ============================================================
123
+ /**
124
+ * Stream chat messages
125
+ */
126
+ chatStream(agentId, messages, options) {
127
+ return this.sdk.chatStream(agentId, messages, options);
128
+ }
129
+ /**
130
+ * Send HITL (Human-in-the-Loop) response
131
+ */
132
+ sendHitlResponse(runId, response) {
133
+ this.sdk.sendHitlResponse(runId, response);
134
+ }
135
+ // ============================================================
136
+ // Conversation Management
137
+ // ============================================================
138
+ /**
139
+ * List conversations for an agent
140
+ */
141
+ async listConversations(options) {
142
+ return this.sdk.listConversations(options);
143
+ }
144
+ /**
145
+ * Get conversation details
146
+ */
147
+ async getConversation(conversationId, options) {
148
+ return this.sdk.getConversation(conversationId, options);
149
+ }
150
+ /**
151
+ * Delete a conversation
152
+ */
153
+ async deleteConversation(conversationId) {
154
+ return this.sdk.deleteConversation(conversationId);
155
+ }
156
+ // ============================================================
157
+ // Internal Access (for FloatingWindow and other internal components)
158
+ // ============================================================
159
+ /**
160
+ * Get the underlying SDK instance
161
+ * @internal This is for internal use by other sanqian-chat components
162
+ */
163
+ _getSdk() {
164
+ return this.sdk;
165
+ }
166
+ };
167
+
1
168
  // src/main/FloatingWindow.ts
2
169
  import { BrowserWindow, globalShortcut, screen, ipcMain, app } from "electron";
170
+ import fs from "fs";
171
+ import os from "os";
172
+ import path from "path";
3
173
  var ipcHandlersRegistered = false;
4
174
  var activeInstance = null;
5
- var FloatingWindow = class {
175
+ var setActiveInstance = (instance) => {
176
+ activeInstance = instance;
177
+ };
178
+ var FloatingWindow = class _FloatingWindow {
6
179
  constructor(options) {
7
180
  this.window = null;
8
- this.savedPosition = null;
181
+ this.savedState = null;
182
+ this.stateSaveTimer = null;
9
183
  this.activeStreams = /* @__PURE__ */ new Map();
184
+ this.reconnectAcquired = false;
10
185
  if (activeInstance) {
11
186
  console.warn("[FloatingWindow] Only one instance supported. Destroying previous.");
12
187
  activeInstance.destroy();
13
188
  }
14
- activeInstance = this;
189
+ setActiveInstance(this);
15
190
  this.options = {
16
191
  width: 400,
17
192
  height: 500,
18
193
  alwaysOnTop: true,
19
194
  showInTaskbar: false,
20
- position: "center",
195
+ // position 默认不设置,让 rememberWindowState 优先读取已保存状态
196
+ // 若无保存状态则 fallback 到居中
197
+ rememberWindowState: true,
21
198
  ...options
22
199
  };
200
+ this.loadWindowState();
23
201
  this.setupIpcHandlers();
24
202
  if (this.options.shortcut) {
25
203
  app.whenReady().then(() => this.registerShortcut());
26
204
  }
27
205
  }
206
+ /**
207
+ * Get SDK instance from either getClient or getSdk
208
+ */
209
+ static getSdkFromOptions(options) {
210
+ if (options.getClient) {
211
+ const client = options.getClient();
212
+ return client?._getSdk() ?? null;
213
+ }
214
+ if (options.getSdk) {
215
+ return options.getSdk();
216
+ }
217
+ return null;
218
+ }
28
219
  createWindow() {
29
- const { width, height, alwaysOnTop, showInTaskbar, preloadPath } = this.options;
220
+ const { alwaysOnTop, showInTaskbar, preloadPath } = this.options;
221
+ const initialBounds = this.getInitialBounds();
222
+ const { minWidth, minHeight } = this.getMinSize();
30
223
  const win = new BrowserWindow({
31
- width,
32
- height,
224
+ width: initialBounds.width,
225
+ height: initialBounds.height,
226
+ x: initialBounds.x,
227
+ y: initialBounds.y,
33
228
  show: false,
34
229
  frame: false,
35
230
  transparent: true,
231
+ hasShadow: false,
232
+ // Disable system shadow to avoid white border on macOS
36
233
  resizable: true,
234
+ backgroundColor: "#00000000",
235
+ minWidth,
236
+ minHeight,
37
237
  alwaysOnTop,
38
238
  skipTaskbar: !showInTaskbar,
39
239
  webPreferences: {
@@ -49,19 +249,37 @@ var FloatingWindow = class {
49
249
  }
50
250
  win.on("blur", () => {
51
251
  });
252
+ win.on("move", () => {
253
+ this.scheduleSaveWindowState();
254
+ });
255
+ win.on("resize", () => {
256
+ this.scheduleSaveWindowState();
257
+ });
258
+ win.on("close", () => {
259
+ this.saveWindowState();
260
+ });
52
261
  win.on("closed", () => {
53
262
  this.window = null;
54
263
  });
55
264
  return win;
56
265
  }
57
- getInitialPosition() {
58
- const { position, width = 400, height = 500 } = this.options;
266
+ getInitialBounds() {
267
+ const { width = 400, height = 500 } = this.options;
268
+ const { minWidth, minHeight } = this.getMinSize();
269
+ const resolvedWidth = Math.max(width, minWidth);
270
+ const resolvedHeight = Math.max(height, minHeight);
271
+ const savedBounds = this.shouldUseSavedState() ? this.getSavedBounds() : null;
272
+ if (savedBounds) {
273
+ return savedBounds;
274
+ }
275
+ const pos = this.getInitialPosition(resolvedWidth, resolvedHeight);
276
+ return { ...pos, width: resolvedWidth, height: resolvedHeight };
277
+ }
278
+ getInitialPosition(width, height) {
279
+ const { position } = this.options;
59
280
  if (typeof position === "object" && "x" in position) {
60
281
  return position;
61
282
  }
62
- if (position === "remember" && this.savedPosition) {
63
- return this.savedPosition;
64
- }
65
283
  if (position === "cursor") {
66
284
  const cursorPos = screen.getCursorScreenPoint();
67
285
  const display = screen.getDisplayNearestPoint(cursorPos);
@@ -70,10 +288,118 @@ var FloatingWindow = class {
70
288
  return { x, y };
71
289
  }
72
290
  const primaryDisplay = screen.getPrimaryDisplay();
73
- const { width: screenWidth, height: screenHeight } = primaryDisplay.workAreaSize;
291
+ const workArea = primaryDisplay.workArea;
74
292
  return {
75
- x: Math.round((screenWidth - width) / 2),
76
- y: Math.round((screenHeight - height) / 2)
293
+ x: workArea.x + Math.round((workArea.width - width) / 2),
294
+ y: workArea.y + Math.round((workArea.height - height) / 2)
295
+ };
296
+ }
297
+ shouldPersistWindowState() {
298
+ return Boolean(this.options.rememberWindowState || this.options.position === "remember");
299
+ }
300
+ shouldUseSavedState() {
301
+ const { position } = this.options;
302
+ if (position && position !== "remember") return false;
303
+ return this.shouldPersistWindowState();
304
+ }
305
+ getMinSize() {
306
+ return {
307
+ minWidth: this.options.minWidth ?? 320,
308
+ minHeight: this.options.minHeight ?? 420
309
+ };
310
+ }
311
+ getWindowStatePath() {
312
+ if (!this.shouldPersistWindowState()) return null;
313
+ if (this.options.windowStatePath) return this.options.windowStatePath;
314
+ const stateDir = path.join(os.homedir(), ".sanqian-chat");
315
+ const key = this.options.windowStateKey || app.getName() || "sanqian-chat";
316
+ const safeKey = key.replace(/[^a-zA-Z0-9._-]/g, "_");
317
+ return path.join(stateDir, `${safeKey}-window.json`);
318
+ }
319
+ getResolvedUiConfig() {
320
+ const uiConfig = this.options.uiConfig ?? {};
321
+ const resolved = { ...uiConfig };
322
+ if (!resolved.theme && this.options.theme) {
323
+ resolved.theme = this.options.theme === "system" ? "auto" : this.options.theme;
324
+ }
325
+ if (typeof resolved.alwaysOnTop !== "boolean" && typeof this.options.alwaysOnTop === "boolean") {
326
+ resolved.alwaysOnTop = this.options.alwaysOnTop;
327
+ }
328
+ if (Object.keys(resolved).length === 0) {
329
+ return null;
330
+ }
331
+ return resolved;
332
+ }
333
+ loadWindowState() {
334
+ const statePath = this.getWindowStatePath();
335
+ if (!statePath) return;
336
+ try {
337
+ const raw = fs.readFileSync(statePath, "utf8");
338
+ const parsed = JSON.parse(raw);
339
+ if (typeof parsed.displayId === "number" && parsed.bounds && parsed.percent && typeof parsed.percent.x === "number" && typeof parsed.percent.y === "number" && typeof parsed.percent.width === "number" && typeof parsed.percent.height === "number") {
340
+ this.savedState = parsed;
341
+ }
342
+ } catch {
343
+ }
344
+ }
345
+ scheduleSaveWindowState() {
346
+ if (!this.shouldPersistWindowState()) return;
347
+ if (this.stateSaveTimer) clearTimeout(this.stateSaveTimer);
348
+ this.stateSaveTimer = setTimeout(() => {
349
+ this.stateSaveTimer = null;
350
+ this.saveWindowState();
351
+ }, 200);
352
+ }
353
+ saveWindowState() {
354
+ if (!this.window || !this.shouldPersistWindowState()) return;
355
+ const statePath = this.getWindowStatePath();
356
+ if (!statePath) return;
357
+ try {
358
+ const bounds = this.window.getBounds();
359
+ const display = screen.getDisplayMatching(bounds);
360
+ const percent = this.getPercentFromBounds(bounds, display);
361
+ const state = {
362
+ displayId: display.id,
363
+ bounds: { ...bounds },
364
+ percent,
365
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
366
+ };
367
+ fs.mkdirSync(path.dirname(statePath), { recursive: true });
368
+ fs.writeFileSync(statePath, JSON.stringify(state, null, 2), "utf8");
369
+ this.savedState = state;
370
+ } catch (e) {
371
+ console.warn("[FloatingWindow] Failed to save window state:", e);
372
+ }
373
+ }
374
+ getPercentFromBounds(bounds, display) {
375
+ const workArea = display.workArea;
376
+ const clamp = (value) => Math.min(1, Math.max(0, value));
377
+ return {
378
+ x: clamp((bounds.x - workArea.x) / workArea.width),
379
+ y: clamp((bounds.y - workArea.y) / workArea.height),
380
+ width: clamp(bounds.width / workArea.width),
381
+ height: clamp(bounds.height / workArea.height)
382
+ };
383
+ }
384
+ getSavedBounds() {
385
+ if (!this.shouldPersistWindowState() || !this.savedState) return null;
386
+ const displays = screen.getAllDisplays();
387
+ const display = displays.find((d) => d.id === this.savedState?.displayId) ?? screen.getPrimaryDisplay();
388
+ const workArea = display.workArea;
389
+ const { minWidth, minHeight } = this.getMinSize();
390
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
391
+ const percent = this.savedState.percent;
392
+ const width = clamp(Math.round(percent.width * workArea.width), minWidth, workArea.width);
393
+ const height = clamp(Math.round(percent.height * workArea.height), minHeight, workArea.height);
394
+ const x = Math.round(workArea.x + percent.x * workArea.width);
395
+ const y = Math.round(workArea.y + percent.y * workArea.height);
396
+ const maxX = workArea.x + workArea.width - width;
397
+ const maxY = workArea.y + workArea.height - height;
398
+ return {
399
+ x: clamp(x, workArea.x, maxX),
400
+ y: clamp(y, workArea.y, maxY),
401
+ width,
402
+ height
77
403
  };
78
404
  }
79
405
  registerShortcut() {
@@ -93,7 +419,7 @@ var FloatingWindow = class {
93
419
  ipcHandlersRegistered = true;
94
420
  ipcMain.handle("sanqian-chat:connect", async () => {
95
421
  try {
96
- const sdk = activeInstance?.options.getSdk();
422
+ const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
97
423
  if (!sdk) throw new Error("SDK not available");
98
424
  await sdk.ensureReady();
99
425
  return { success: true };
@@ -102,27 +428,42 @@ var FloatingWindow = class {
102
428
  }
103
429
  });
104
430
  ipcMain.handle("sanqian-chat:isConnected", () => {
105
- const sdk = activeInstance?.options.getSdk();
431
+ const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
106
432
  return sdk?.isConnected() ?? false;
107
433
  });
108
434
  ipcMain.handle("sanqian-chat:stream", async (event, params) => {
109
435
  const webContents = event.sender;
110
- const { streamId, messages, conversationId } = params;
111
- const sdk = activeInstance?.options.getSdk();
112
- const agentId = activeInstance?.options.getAgentId();
436
+ const { streamId, messages, conversationId, agentId: requestedAgentId } = params;
437
+ const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
438
+ const agentId = requestedAgentId ?? activeInstance?.options.getAgentId();
113
439
  if (!sdk || !agentId) {
114
440
  webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: "SDK or agent not ready" } });
115
441
  return;
116
442
  }
117
- const streamState = { cancelled: false };
443
+ const streamState = { cancelled: false, runId: null };
118
444
  activeInstance?.activeStreams.set(streamId, streamState);
119
445
  try {
120
446
  await sdk.ensureReady();
121
447
  const sdkMessages = messages.map((m) => ({ role: m.role, content: m.content }));
122
- const stream = sdk.chatStream(agentId, sdkMessages, { conversationId });
448
+ const stream = sdk.chatStream(
449
+ agentId,
450
+ sdkMessages,
451
+ { conversationId, persistHistory: true }
452
+ );
123
453
  for await (const evt of stream) {
124
454
  if (streamState.cancelled) break;
455
+ if (activeInstance?.options.devMode) {
456
+ console.log("[FloatingWindow] SDK event:", evt.type, JSON.stringify(evt).slice(0, 200));
457
+ }
125
458
  switch (evt.type) {
459
+ case "start": {
460
+ const startEvt = evt;
461
+ if (startEvt.run_id) {
462
+ streamState.runId = startEvt.run_id;
463
+ }
464
+ webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "start", run_id: startEvt.run_id } });
465
+ break;
466
+ }
126
467
  case "text":
127
468
  webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "text", content: evt.content } });
128
469
  break;
@@ -141,12 +482,13 @@ var FloatingWindow = class {
141
482
  case "error":
142
483
  webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "error", error: evt.error } });
143
484
  break;
144
- default:
485
+ default: {
145
486
  const anyEvt = evt;
146
487
  if (anyEvt.type === "interrupt") {
147
488
  webContents.send("sanqian-chat:streamEvent", { streamId, event: { type: "interrupt", interrupt_type: anyEvt.interrupt_type, interrupt_payload: anyEvt.interrupt_payload, run_id: anyEvt.run_id } });
148
489
  }
149
490
  break;
491
+ }
150
492
  }
151
493
  }
152
494
  } catch (e) {
@@ -161,40 +503,64 @@ var FloatingWindow = class {
161
503
  const stream = activeInstance?.activeStreams.get(params.streamId);
162
504
  if (stream) {
163
505
  stream.cancelled = true;
506
+ if (stream.runId) {
507
+ const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
508
+ if (sdk) {
509
+ try {
510
+ sdk.cancelRun(stream.runId);
511
+ } catch (e) {
512
+ console.warn("[FloatingWindow] Failed to cancel run:", e);
513
+ }
514
+ }
515
+ }
164
516
  activeInstance?.activeStreams.delete(params.streamId);
165
517
  }
166
518
  return { success: true };
167
519
  });
168
520
  ipcMain.handle("sanqian-chat:hitlResponse", (_, params) => {
169
- const sdk = activeInstance?.options.getSdk();
521
+ const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
170
522
  if (sdk && params.runId) {
171
523
  sdk.sendHitlResponse(params.runId, params.response);
172
524
  }
173
525
  return { success: true };
174
526
  });
175
527
  ipcMain.handle("sanqian-chat:listConversations", async (_, params) => {
176
- const sdk = activeInstance?.options.getSdk();
177
- const agentId = activeInstance?.options.getAgentId();
178
- if (!sdk || !agentId) return { success: false, error: "SDK not ready" };
528
+ const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
529
+ if (!sdk) return { success: false, error: "SDK not ready" };
179
530
  try {
180
- const result = await sdk.listConversations({ agentId, ...params });
531
+ const result = await sdk.listConversations({
532
+ limit: params?.limit,
533
+ offset: params?.offset
534
+ });
181
535
  return { success: true, data: result };
182
536
  } catch (e) {
183
537
  return { success: false, error: e instanceof Error ? e.message : "Failed to list" };
184
538
  }
185
539
  });
186
540
  ipcMain.handle("sanqian-chat:getConversation", async (_, params) => {
187
- const sdk = activeInstance?.options.getSdk();
541
+ const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
188
542
  if (!sdk) return { success: false, error: "SDK not ready" };
189
543
  try {
190
544
  const result = await sdk.getConversation(params.conversationId, { messageLimit: params.messageLimit });
191
- return { success: true, data: result };
545
+ let messages = result?.messages;
546
+ const sdkWithHistory = sdk;
547
+ if (typeof sdkWithHistory.getMessages === "function") {
548
+ try {
549
+ const history = await sdkWithHistory.getMessages(params.conversationId, { limit: params.messageLimit });
550
+ if (history?.messages && history.messages.length > 0) {
551
+ messages = history.messages;
552
+ }
553
+ } catch (e) {
554
+ console.warn("[sanqian-chat][main] getMessages failed, fallback to getConversation:", e);
555
+ }
556
+ }
557
+ return { success: true, data: { ...result, messages } };
192
558
  } catch (e) {
193
559
  return { success: false, error: e instanceof Error ? e.message : "Failed to get" };
194
560
  }
195
561
  });
196
562
  ipcMain.handle("sanqian-chat:deleteConversation", async (_, params) => {
197
- const sdk = activeInstance?.options.getSdk();
563
+ const sdk = activeInstance ? _FloatingWindow.getSdkFromOptions(activeInstance.options) : null;
198
564
  if (!sdk) return { success: false, error: "SDK not ready" };
199
565
  try {
200
566
  await sdk.deleteConversation(params.conversationId);
@@ -207,26 +573,63 @@ var FloatingWindow = class {
207
573
  activeInstance?.hide();
208
574
  return { success: true };
209
575
  });
576
+ ipcMain.handle("sanqian-chat:setAlwaysOnTop", (_event, params) => {
577
+ if (!activeInstance) return { success: false, error: "Window not available" };
578
+ activeInstance.setAlwaysOnTop(params.alwaysOnTop);
579
+ return { success: true };
580
+ });
581
+ ipcMain.handle("sanqian-chat:getAlwaysOnTop", () => {
582
+ if (!activeInstance) return { success: false, error: "Window not available" };
583
+ return { success: true, data: activeInstance.isAlwaysOnTop() };
584
+ });
585
+ ipcMain.handle("sanqian-chat:getUiConfig", () => {
586
+ if (!activeInstance) return { success: false, error: "Window not available" };
587
+ return { success: true, data: activeInstance.getResolvedUiConfig() };
588
+ });
210
589
  }
211
590
  // Public API
212
591
  show() {
213
592
  if (!this.window) {
214
593
  this.window = this.createWindow();
215
594
  }
216
- const pos = this.getInitialPosition();
217
- this.window.setPosition(pos.x, pos.y);
595
+ if (this.shouldPersistWindowState()) {
596
+ const bounds = this.getInitialBounds();
597
+ this.window.setBounds(bounds);
598
+ } else {
599
+ const pos = this.getInitialPosition(this.options.width ?? 400, this.options.height ?? 500);
600
+ this.window.setPosition(pos.x, pos.y);
601
+ }
218
602
  this.window.show();
219
603
  this.window.focus();
604
+ if (!this.reconnectAcquired) {
605
+ const sdk = _FloatingWindow.getSdkFromOptions(this.options);
606
+ sdk?.acquireReconnect();
607
+ this.reconnectAcquired = true;
608
+ }
220
609
  }
221
610
  hide() {
222
611
  if (this.window) {
223
- if (this.options.position === "remember") {
224
- const [x, y] = this.window.getPosition();
225
- this.savedPosition = { x, y };
226
- }
612
+ this.saveWindowState();
227
613
  this.window.hide();
614
+ if (this.reconnectAcquired) {
615
+ const sdk = _FloatingWindow.getSdkFromOptions(this.options);
616
+ sdk?.releaseReconnect();
617
+ this.reconnectAcquired = false;
618
+ }
619
+ }
620
+ }
621
+ setAlwaysOnTop(alwaysOnTop) {
622
+ this.options.alwaysOnTop = alwaysOnTop;
623
+ if (this.window) {
624
+ this.window.setAlwaysOnTop(alwaysOnTop);
228
625
  }
229
626
  }
627
+ isAlwaysOnTop() {
628
+ if (this.window) {
629
+ return this.window.isAlwaysOnTop();
630
+ }
631
+ return Boolean(this.options.alwaysOnTop);
632
+ }
230
633
  toggle() {
231
634
  if (this.window?.isVisible()) {
232
635
  this.hide();
@@ -241,6 +644,15 @@ var FloatingWindow = class {
241
644
  if (this.options.shortcut) {
242
645
  globalShortcut.unregister(this.options.shortcut);
243
646
  }
647
+ if (this.stateSaveTimer) {
648
+ clearTimeout(this.stateSaveTimer);
649
+ this.stateSaveTimer = null;
650
+ }
651
+ if (this.reconnectAcquired) {
652
+ const sdk = _FloatingWindow.getSdkFromOptions(this.options);
653
+ sdk?.releaseReconnect();
654
+ this.reconnectAcquired = false;
655
+ }
244
656
  this.window?.destroy();
245
657
  this.window = null;
246
658
  this.activeStreams.forEach((stream) => {
@@ -259,6 +671,9 @@ var FloatingWindow = class {
259
671
  ipcMain.removeHandler("sanqian-chat:getConversation");
260
672
  ipcMain.removeHandler("sanqian-chat:deleteConversation");
261
673
  ipcMain.removeHandler("sanqian-chat:hide");
674
+ ipcMain.removeHandler("sanqian-chat:setAlwaysOnTop");
675
+ ipcMain.removeHandler("sanqian-chat:getAlwaysOnTop");
676
+ ipcMain.removeHandler("sanqian-chat:getUiConfig");
262
677
  ipcHandlersRegistered = false;
263
678
  }
264
679
  }
@@ -268,5 +683,6 @@ var FloatingWindow = class {
268
683
  }
269
684
  };
270
685
  export {
271
- FloatingWindow
686
+ FloatingWindow,
687
+ SanqianAppClient
272
688
  };