@yushaw/sanqian-chat 0.2.4 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -99,6 +99,19 @@ src/renderer/styles/
99
99
 
100
100
  ## Changelog
101
101
 
102
+ ### 0.2.7 (2026-01-06)
103
+ - **Added**: External resource reference support
104
+ - `useResourcePicker` hook - manage resource picker state, search, pagination
105
+ - `ResourcePicker` component - two-level picker (providers → resources)
106
+ - `ResourceChip` / `ResourceChipList` - display attached resources
107
+ - `AddResourceButton` - + button with dropdown menu
108
+ - **Added**: `ChatAdapter` extensions: `listResourceProviders`, `getResourceList`, `onLocaleChanged`
109
+ - **Added**: `FloatingWindow.notifyLocaleChanged(locale)` and `ChatPanel.notifyLocaleChanged(locale)` - notify renderer when locale changes
110
+ - **Added**: `sendMessage` now accepts `options.attachedResources`
111
+ - **Added**: Message rendering displays attached resources in user messages
112
+ - **Added**: New types: `AttachedResource`, `ResourcePickerItem`, `ContextProviderInfo`
113
+ - **Changed**: `AppContextProvider.getList` now accepts `options` parameter for search/pagination
114
+
102
115
  ### 2025-01-03
103
116
  - **Added**: Focus persistence - input keeps focus when clicking empty areas (unless selecting text)
104
117
  - **Added**: `Cmd+N` (macOS) / `Ctrl+N` (Windows) keyboard shortcut for new chat
@@ -114,3 +127,24 @@ src/renderer/styles/
114
127
  - **Fixed**: `console.log` only outputs in `devMode`
115
128
  - **Fixed**: Global style pollution - default to `'safe'` mode (no Tailwind preflight)
116
129
  - **Fixed**: `cancelStream` now calls `sdk.cancelRun()` for real cancellation
130
+
131
+ ## Tech Debt / Future Optimizations
132
+
133
+ ### Low Priority (记录于 2026-01-05)
134
+
135
+ 1. **FloatingChat 命名优化**
136
+ - `FloatingChat` 实际是通用组件(embedded + floating),名字有误导
137
+ - 可考虑改名为 `ChatContainer` 或 `ChatView`
138
+ - 文件:`src/renderer/components/FloatingChat.tsx`
139
+
140
+ 2. **Header 按钮复用**
141
+ - `FloatingChat` 内联了 `<ModeToggleButton>` + `<AttachButton>`
142
+ - 可改用 `<PanelHeaderButtons />` 与 `CompactChat` 保持一致
143
+ - 文件:`src/renderer/components/FloatingChat.tsx:134-136`
144
+
145
+
146
+ 3. **useCallback 依赖优化** (微优化)
147
+ - `useAttachState.ts:68`: toggle 依赖 state,每次 state 变化重建
148
+ - `useChatPanel.ts:86`: toggleMode 依赖 mode,每次 mode 变化重建
149
+ - 可用 useRef 避免重建,但当前影响可忽略
150
+ - 文件:`src/renderer/hooks/useAttachState.ts`, `src/renderer/hooks/useChatPanel.ts`
@@ -0,0 +1,86 @@
1
+ // src/preload/factories.ts
2
+ import { ipcRenderer } from "electron";
3
+ function createSanqianChatApi() {
4
+ return {
5
+ connect: () => ipcRenderer.invoke("sanqian-chat:connect"),
6
+ isConnected: () => ipcRenderer.invoke("sanqian-chat:isConnected"),
7
+ stream: (params) => ipcRenderer.invoke("sanqian-chat:stream", params),
8
+ cancelStream: (params) => ipcRenderer.invoke("sanqian-chat:cancelStream", params),
9
+ onStreamEvent: (callback) => {
10
+ const handler = (_, data) => {
11
+ callback(data.streamId, data.event);
12
+ };
13
+ ipcRenderer.on("sanqian-chat:streamEvent", handler);
14
+ return () => ipcRenderer.removeListener("sanqian-chat:streamEvent", handler);
15
+ },
16
+ sendHitlResponse: (params) => ipcRenderer.invoke("sanqian-chat:hitlResponse", params),
17
+ listConversations: (params) => ipcRenderer.invoke("sanqian-chat:listConversations", params),
18
+ getConversation: (params) => ipcRenderer.invoke("sanqian-chat:getConversation", params),
19
+ deleteConversation: (params) => ipcRenderer.invoke("sanqian-chat:deleteConversation", params),
20
+ hide: () => ipcRenderer.invoke("sanqian-chat:hide"),
21
+ setAlwaysOnTop: (params) => ipcRenderer.invoke("sanqian-chat:setAlwaysOnTop", params),
22
+ getAlwaysOnTop: () => ipcRenderer.invoke("sanqian-chat:getAlwaysOnTop"),
23
+ getUiConfig: () => ipcRenderer.invoke("sanqian-chat:getUiConfig"),
24
+ setBackgroundColor: (params) => ipcRenderer.invoke("sanqian-chat:setBackgroundColor", params),
25
+ getPlatform: () => process.platform,
26
+ // Resource Picker
27
+ listResourceProviders: () => ipcRenderer.invoke("sanqian-chat:listResourceProviders"),
28
+ getResourceList: (params) => ipcRenderer.invoke("sanqian-chat:getResourceList", params),
29
+ // Locale
30
+ onLocaleChanged: (callback) => {
31
+ const handler = (_, data) => {
32
+ callback(data.locale);
33
+ };
34
+ ipcRenderer.on("sanqian-chat:localeChanged", handler);
35
+ return () => ipcRenderer.removeListener("sanqian-chat:localeChanged", handler);
36
+ }
37
+ };
38
+ }
39
+ function createChatPanelApi() {
40
+ return {
41
+ // Mode
42
+ getMode: () => ipcRenderer.invoke("chatPanel:getMode"),
43
+ setMode: (mode) => ipcRenderer.invoke("chatPanel:setMode", mode),
44
+ toggleMode: () => ipcRenderer.invoke("chatPanel:toggleMode"),
45
+ onModeChanged: (callback) => {
46
+ const handler = (_, data) => {
47
+ callback(data.mode);
48
+ };
49
+ ipcRenderer.on("chatPanel:modeChanged", handler);
50
+ return () => ipcRenderer.removeListener("chatPanel:modeChanged", handler);
51
+ },
52
+ // Visibility
53
+ isVisible: () => ipcRenderer.invoke("chatPanel:isVisible"),
54
+ show: () => ipcRenderer.invoke("chatPanel:show"),
55
+ hide: () => ipcRenderer.invoke("chatPanel:hide"),
56
+ toggle: () => ipcRenderer.invoke("chatPanel:toggle"),
57
+ onVisibilityChanged: (callback) => {
58
+ const handler = (_, data) => {
59
+ callback(data.visible);
60
+ };
61
+ ipcRenderer.on("chatPanel:visibilityChanged", handler);
62
+ return () => ipcRenderer.removeListener("chatPanel:visibilityChanged", handler);
63
+ },
64
+ // Attach state
65
+ getAttachState: () => ipcRenderer.invoke("chatPanel:getAttachState"),
66
+ toggleAttach: () => ipcRenderer.invoke("chatPanel:toggleAttach"),
67
+ onAttachStateChanged: (callback) => {
68
+ const handler = (_, data) => {
69
+ callback(data.state);
70
+ };
71
+ ipcRenderer.on("chatPanel:attachStateChanged", handler);
72
+ return () => ipcRenderer.removeListener("chatPanel:attachStateChanged", handler);
73
+ },
74
+ // Width
75
+ getWidth: () => ipcRenderer.invoke("chatPanel:getWidth"),
76
+ setWidth: (width, animate) => ipcRenderer.invoke("chatPanel:setWidth", { width, animate }),
77
+ onResizeEnd: () => ipcRenderer.invoke("chatPanel:onResizeEnd"),
78
+ // UI Config
79
+ getUiConfig: () => ipcRenderer.invoke("chatPanel:getUiConfig")
80
+ };
81
+ }
82
+
83
+ export {
84
+ createSanqianChatApi,
85
+ createChatPanelApi
86
+ };
@@ -1,5 +1,5 @@
1
- import { HitlInterruptPayload, HitlResponse, SanqianSDK } from '@yushaw/sanqian-sdk';
2
- export { ChatStreamEvent, HitlInterruptPayload, HitlInterruptType, HitlResponse, HitlRiskLevel, ChatMessage as SdkChatMessage, ConversationDetail as SdkConversationDetail, ConversationInfo as SdkConversationInfo, ToolCall as SdkToolCall } from '@yushaw/sanqian-sdk';
1
+ import { ResourceType, HitlInterruptPayload, ContextListItem, HitlResponse, ResourceListOptions, SanqianSDK } from '@yushaw/sanqian-sdk';
2
+ export { ChatStreamEvent, ContextListItem, HitlInterruptPayload, HitlInterruptType, HitlResponse, HitlRiskLevel, ResourceListOptions, ResourceListResult, ResourceType, ChatMessage as SdkChatMessage, ConversationDetail as SdkConversationDetail, ConversationInfo as SdkConversationInfo, ToolCall as SdkToolCall } from '@yushaw/sanqian-sdk';
3
3
 
4
4
  /**
5
5
  * @yushaw/sanqian-chat Core Types
@@ -57,6 +57,12 @@ interface ChatUiStrings {
57
57
  hitlInputRequest: string;
58
58
  hitlApprovalRequired: string;
59
59
  hitlInputRequired: string;
60
+ attachWindow: string;
61
+ detachWindow: string;
62
+ floatWindow: string;
63
+ embedWindow: string;
64
+ collapseSidebar: string;
65
+ history: string;
60
66
  }
61
67
  type ChatThemeMode = 'light' | 'dark' | 'auto';
62
68
  type ChatFontSize = 'small' | 'normal' | 'large' | 'extra-large';
@@ -114,6 +120,8 @@ interface ChatMessage {
114
120
  finalContent?: string;
115
121
  isComplete?: boolean;
116
122
  filePaths?: string[];
123
+ /** Attached external resources (for user messages) */
124
+ attachedResources?: AttachedResource[];
117
125
  }
118
126
  /** Conversation info for UI */
119
127
  interface ConversationInfo {
@@ -154,6 +162,233 @@ interface FloatingWindowConfig {
154
162
  /** Optional full path for window state file */
155
163
  windowStatePath?: string;
156
164
  }
165
+ type ChatPanelMode = 'embedded' | 'floating';
166
+ type ChatPanelPosition = 'right' | 'left';
167
+ type AttachState = 'attached' | 'detached' | 'unavailable';
168
+ type AttachPosition = 'right' | 'left' | 'top' | 'bottom';
169
+ /**
170
+ * Window attachment configuration for floating mode
171
+ */
172
+ interface AttachConfig {
173
+ /**
174
+ * Target window to attach to (BrowserWindow instance)
175
+ */
176
+ window: unknown;
177
+ /**
178
+ * Attach position relative to target window
179
+ * @default 'right'
180
+ */
181
+ position?: AttachPosition;
182
+ /**
183
+ * Gap between windows (px)
184
+ * @default 0
185
+ */
186
+ gap?: number;
187
+ /**
188
+ * Sync height/width with target window
189
+ * @default true
190
+ */
191
+ syncSize?: boolean;
192
+ /**
193
+ * Behavior when target window is minimized
194
+ * @default 'hide'
195
+ */
196
+ onMinimize?: 'hide' | 'detach' | 'minimize';
197
+ /**
198
+ * Behavior when target window is closed
199
+ * @default 'hide'
200
+ */
201
+ onClose?: 'hide' | 'destroy';
202
+ /**
203
+ * Allow user to drag and detach
204
+ * @default true
205
+ */
206
+ allowDetach?: boolean;
207
+ /**
208
+ * Allow re-attach when dragged near target edge
209
+ * @default true
210
+ */
211
+ allowReattach?: boolean;
212
+ /**
213
+ * Distance threshold for re-attach (px)
214
+ * @default 20
215
+ */
216
+ reattachThreshold?: number;
217
+ }
218
+ /**
219
+ * ChatPanel configuration
220
+ */
221
+ interface ChatPanelConfig {
222
+ /**
223
+ * Host window (BaseWindow supports embedded, BrowserWindow floating only)
224
+ */
225
+ hostWindow: unknown;
226
+ /**
227
+ * Host window's main content view (required for embedded mode)
228
+ */
229
+ hostMainView?: unknown;
230
+ /**
231
+ * Initial mode
232
+ * @default 'embedded'
233
+ */
234
+ initialMode?: ChatPanelMode;
235
+ /**
236
+ * Embed position
237
+ * @default 'right'
238
+ */
239
+ position?: ChatPanelPosition;
240
+ /**
241
+ * Panel width
242
+ * @default 360
243
+ */
244
+ width?: number;
245
+ /**
246
+ * Minimum width
247
+ * @default 240
248
+ */
249
+ minWidth?: number;
250
+ /**
251
+ * Minimum host content width when showing panel.
252
+ * If window is too narrow, expand it to ensure main content has this width.
253
+ * Set to 0 to disable auto-expand.
254
+ * @default 0
255
+ */
256
+ minHostContentWidth?: number;
257
+ /**
258
+ * Allow resize
259
+ * @default true
260
+ */
261
+ resizable?: boolean;
262
+ /**
263
+ * Preload script path
264
+ */
265
+ preloadPath: string;
266
+ /**
267
+ * Renderer HTML path or URL
268
+ */
269
+ rendererPath: string;
270
+ /**
271
+ * Dev mode - load URL instead of file
272
+ */
273
+ devMode?: boolean;
274
+ /**
275
+ * SDK client getter
276
+ */
277
+ getClient: () => unknown;
278
+ /**
279
+ * Agent ID getter
280
+ */
281
+ getAgentId: () => string | null;
282
+ /**
283
+ * Layout change callback (for host app to adjust main content)
284
+ */
285
+ onLayoutChange?: (layout: {
286
+ mainWidth: number;
287
+ chatWidth: number;
288
+ chatVisible: boolean;
289
+ }) => void;
290
+ /**
291
+ * Shortcut configuration
292
+ */
293
+ shortcuts?: {
294
+ /** Toggle show/hide, false to disable */
295
+ toggle?: string | false;
296
+ /** Toggle mode, false to disable */
297
+ toggleMode?: string | false;
298
+ };
299
+ /**
300
+ * Floating window attach configuration
301
+ */
302
+ attach?: AttachConfig;
303
+ /**
304
+ * State persistence key
305
+ */
306
+ stateKey?: string;
307
+ /**
308
+ * UI config for renderer
309
+ */
310
+ uiConfig?: ChatUiConfigSerializable;
311
+ }
312
+ /**
313
+ * Context provider info for UI display (in + menu)
314
+ */
315
+ interface ContextProviderInfo {
316
+ /** Full provider ID (format: "appName:providerId") */
317
+ id: string;
318
+ /** Display name */
319
+ name: string;
320
+ /** Description */
321
+ description: string;
322
+ /** App name (extracted from id) */
323
+ appName: string;
324
+ /** Whether app is currently connected */
325
+ isConnected?: boolean;
326
+ /** Whether provider supports getList (resource picker) */
327
+ hasGetList?: boolean;
328
+ /** Whether provider supports getCurrent (auto-inject) */
329
+ hasGetCurrent?: boolean;
330
+ }
331
+ /**
332
+ * Resource item for picker display (extends ContextListItem with provider info)
333
+ */
334
+ interface ResourcePickerItem extends ContextListItem {
335
+ /** Full provider ID that owns this resource */
336
+ providerId: string;
337
+ /** Provider display name */
338
+ providerName?: string;
339
+ }
340
+ /**
341
+ * Attached resource reference (selected by user)
342
+ */
343
+ interface AttachedResource {
344
+ /** Full provider ID (format: "appName:providerId") */
345
+ providerId: string;
346
+ /** Resource ID (from ContextListItem.id) */
347
+ resourceId: string;
348
+ /** Display title */
349
+ title: string;
350
+ /** Optional summary for tooltip */
351
+ summary?: string;
352
+ /** Resource type for icon/styling */
353
+ type?: ResourceType;
354
+ /** Icon emoji or URL */
355
+ icon?: string;
356
+ }
357
+ /**
358
+ * Resource picker state
359
+ */
360
+ interface ResourcePickerState {
361
+ /** Currently loading */
362
+ isLoading: boolean;
363
+ /** Search query */
364
+ query: string;
365
+ /** Available items grouped by provider */
366
+ itemsByProvider: Record<string, ResourcePickerItem[]>;
367
+ /** Whether more items are available (for pagination) */
368
+ hasMoreByProvider: Record<string, boolean>;
369
+ /** Error message if any */
370
+ error?: string;
371
+ }
372
+ /**
373
+ * Attachment menu item type
374
+ */
375
+ type AttachmentMenuItemType = 'upload' | 'provider';
376
+ /**
377
+ * Attachment menu item for + button dropdown
378
+ */
379
+ interface AttachmentMenuItem {
380
+ type: AttachmentMenuItemType;
381
+ /** For 'upload': 'file' or 'image'. For 'provider': provider ID */
382
+ id: string;
383
+ /** Display label */
384
+ label: string;
385
+ /** Icon emoji or URL */
386
+ icon?: string;
387
+ /** Description or subtitle */
388
+ description?: string;
389
+ /** Whether currently available (e.g., app connected) */
390
+ available?: boolean;
391
+ }
157
392
 
158
393
  /**
159
394
  * Chat Adapter Interface
@@ -237,10 +472,23 @@ interface ChatAdapter {
237
472
  deleteConversation(id: string): Promise<void>;
238
473
  chatStream(messages: SendMessage[], conversationId: string | undefined, onEvent: (event: StreamEvent) => void, options?: {
239
474
  agentId?: string | null;
475
+ /** Attached context provider IDs (e.g., ["sanqian-notes:notes"]) - for getCurrent */
476
+ attachedContexts?: string[];
477
+ /** Attached resource references (e.g., ["sanqian-notes:notes:abc123"]) - for getById */
478
+ attachedResources?: string[];
240
479
  }): Promise<{
241
480
  cancel: () => void;
242
481
  }>;
243
482
  sendHitlResponse?(response: HitlResponse, runId?: string): void;
483
+ /** List available context providers */
484
+ listResourceProviders?(): Promise<ContextProviderInfo[]>;
485
+ /** Get resource list from a provider with search/pagination */
486
+ getResourceList?(providerId: string, options?: ResourceListOptions): Promise<{
487
+ items: ResourcePickerItem[];
488
+ hasMore?: boolean;
489
+ }>;
490
+ /** Subscribe to locale change events */
491
+ onLocaleChanged?(callback: (locale: string) => void): () => void;
244
492
  cleanup?(): void;
245
493
  }
246
494
  /** SDK adapter config */
@@ -327,4 +575,4 @@ declare function parseToolCalls(toolCalls: unknown): ToolCall[] | undefined;
327
575
  */
328
576
  declare function mergeConsecutiveAssistantMessages(rawMessages: ApiMessage[]): ChatMessage[];
329
577
 
330
- export { type ApiMessage, type ChatAdapter, type ChatAdapterConfig, type ChatFontSize, type ChatMessage, type ChatThemeMode, type ChatUiConfigSerializable, type ChatUiStrings, type ConnectionErrorCode, type ConnectionStatus, type ConversationDetail, type ConversationInfo, type FloatingWindowConfig, type HitlInterruptData, type Locale, type MessageBlock, type MessageRole, type SdkAdapterConfig, type SendMessage, type StreamEvent, type ToolCall, type ToolCallStatus, type WindowPosition, createChatAdapter, createSdkAdapter, mergeConsecutiveAssistantMessages, parseToolCalls };
578
+ export { type ApiMessage, type AttachConfig, type AttachPosition, type AttachState, type AttachedResource, type AttachmentMenuItem, type AttachmentMenuItemType, type ChatAdapter, type ChatAdapterConfig, type ChatFontSize, type ChatMessage, type ChatPanelConfig, type ChatPanelMode, type ChatPanelPosition, type ChatThemeMode, type ChatUiConfigSerializable, type ChatUiStrings, type ConnectionErrorCode, type ConnectionStatus, type ContextProviderInfo, type ConversationDetail, type ConversationInfo, type FloatingWindowConfig, type HitlInterruptData, type Locale, type MessageBlock, type MessageRole, type ResourcePickerItem, type ResourcePickerState, type SdkAdapterConfig, type SendMessage, type StreamEvent, type ToolCall, type ToolCallStatus, type WindowPosition, createChatAdapter, createSdkAdapter, mergeConsecutiveAssistantMessages, parseToolCalls };