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