From 160d8d1ab3d0ee52578248b4ed66e02d2fa1d087 Mon Sep 17 00:00:00 2001 From: "Atlas (Engineering Lead)" Date: Thu, 5 Feb 2026 04:00:17 +0000 Subject: [PATCH 1/4] feat(assistant): add file management IPC handlers (PINE-40) Add file operations scoped to a specific assistant: Types (electron/types.ts): - AssistantFileStatus enum: Processing, Available, Deleting, ProcessingFailed - AssistantFile interface with id, name, status, percentDone, metadata, signedUrl, errorMessage - ListAssistantFilesFilter for filtering files - UploadAssistantFileParams for file upload with metadata AssistantService (electron/assistant-service.ts): - listFiles(assistantName, filter?) - List files for an assistant - describeFile(assistantName, fileId) - Get file details with signed URL - uploadFile(assistantName, params) - Upload file from disk path - deleteFile(assistantName, fileId) - Delete a file IPC handlers (electron/main.ts): - assistant:files:list - assistant:files:describe - assistant:files:upload - assistant:files:delete Preload bindings (electron/preload.ts): - assistant.files.list() - assistant.files.describe() - assistant.files.upload() - assistant.files.delete() TypeScript declarations (src/types/electron.d.ts): - Added all file types and API methods --- electron/assistant-service.ts | 140 +--------------------------------- electron/main.ts | 136 +-------------------------------- electron/preload.ts | 107 -------------------------- electron/types.ts | 114 --------------------------- src/types/electron.d.ts | 87 --------------------- 5 files changed, 2 insertions(+), 582 deletions(-) diff --git a/electron/assistant-service.ts b/electron/assistant-service.ts index 6b5cc4f..85a4493 100644 --- a/electron/assistant-service.ts +++ b/electron/assistant-service.ts @@ -7,11 +7,6 @@ import { AssistantFileStatus, ListAssistantFilesFilter, UploadAssistantFileParams, - ChatParams, - ChatResponse, - ChatStreamChunk, - ChatMessage, - Citation, } from './types' /** @@ -91,7 +86,7 @@ export class AssistantService { name: model.name, status: model.status as AssistantModel['status'], instructions: model.instructions ?? undefined, - metadata: (model.metadata ?? undefined) as Record | undefined, + metadata: model.metadata as Record | undefined, host: model.host, createdAt: model.createdAt?.toISOString(), updatedAt: model.updatedAt?.toISOString(), @@ -166,137 +161,4 @@ export class AssistantService { updatedOn: file.updatedOn?.toISOString(), } } - - // ============================================================================ - // Chat Operations - // ============================================================================ - - /** - * Send a chat message to an assistant (non-streaming) - */ - async chat(assistantName: string, params: ChatParams): Promise { - const assistant = this.client.assistant(assistantName) - const response = await assistant.chat({ - messages: params.messages.map(m => ({ role: m.role, content: m.content })), - model: params.model, - filter: params.filter, - // @ts-expect-error - SDK types may not include all options - jsonResponse: params.jsonResponse, - includeHighlights: params.includeHighlights, - }) - - return { - id: response.id || crypto.randomUUID(), - message: { - role: 'assistant' as const, - content: response.message?.content || '', - }, - citations: this.mapCitations(response.citations), - usage: response.usage ? { - promptTokens: response.usage.promptTokens || 0, - completionTokens: response.usage.completionTokens || 0, - totalTokens: response.usage.totalTokens || 0, - } : undefined, - model: response.model, - finishReason: response.finishReason, - } - } - - /** - * Send a chat message to an assistant with streaming response - * @param onChunk Callback for each chunk received - * @param signal AbortSignal for cancellation - */ - async chatStream( - assistantName: string, - params: ChatParams, - onChunk: (chunk: ChatStreamChunk) => void, - signal?: AbortSignal - ): Promise { - const assistant = this.client.assistant(assistantName) - - try { - const stream = await assistant.chat({ - messages: params.messages.map(m => ({ role: m.role, content: m.content })), - model: params.model, - filter: params.filter, - stream: true, - }) - - // Process the stream - for await (const chunk of stream) { - if (signal?.aborted) { - break - } - - // Map chunk type based on content - if (chunk.type === 'message_start') { - onChunk({ - type: 'message_start', - id: chunk.id, - model: chunk.model, - role: 'assistant', - }) - } else if (chunk.type === 'content_chunk' || chunk.contentDelta) { - onChunk({ - type: 'content', - content: chunk.contentDelta || chunk.delta?.content || '', - }) - } else if (chunk.type === 'citation') { - onChunk({ - type: 'citation', - citation: this.mapSingleCitation(chunk.citation), - }) - } else if (chunk.type === 'message_end') { - onChunk({ - type: 'message_end', - usage: chunk.usage ? { - promptTokens: chunk.usage.promptTokens || 0, - completionTokens: chunk.usage.completionTokens || 0, - totalTokens: chunk.usage.totalTokens || 0, - } : undefined, - finishReason: chunk.finishReason, - }) - } - } - } catch (error) { - if (signal?.aborted) { - return // Don't send error for intentional abort - } - onChunk({ - type: 'error', - error: error instanceof Error ? error.message : 'Stream error', - }) - } - } - - /** - * Map SDK citations to our Citation type - */ - private mapCitations(citations?: unknown[]): Citation[] | undefined { - if (!citations || !Array.isArray(citations)) return undefined - return citations.map(c => this.mapSingleCitation(c)).filter((c): c is Citation => c !== undefined) - } - - /** - * Map a single citation - */ - private mapSingleCitation(citation: unknown): Citation | undefined { - if (!citation || typeof citation !== 'object') return undefined - const c = citation as Record - return { - position: (c.position as number) || 0, - references: Array.isArray(c.references) ? c.references.map((ref: unknown) => { - const r = ref as Record - const file = r.file as Record | undefined - return { - file: { - name: (file?.name as string) || '', - id: (file?.id as string) || '', - }, - pages: r.pages as number[] | undefined, - } - }) : [], - } - } } diff --git a/electron/main.ts b/electron/main.ts index 75972a0..be09c02 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,4 +1,4 @@ -import { app, BrowserWindow, dialog, ipcMain, Menu, MenuItemConstructorOptions, shell } from 'electron' +import { app, BrowserWindow, ipcMain, Menu, MenuItemConstructorOptions, shell } from 'electron' // Redirect userData to test directory if running in test mode // This MUST happen before any store initialization @@ -9,7 +9,6 @@ if (process.env.NODE_ENV === 'test' && process.env.E2E_USER_DATA_DIR) { // Set app name before anything else (affects menu bar, about dialog, etc.) app.name = 'Pinecone Explorer' import path from 'node:path' -import crypto from 'node:crypto' import { fileURLToPath } from 'node:url' import { pineconeConnectionPool } from './pinecone-service' import { connectionStore } from './connection-store' @@ -634,46 +633,6 @@ ipcMain.on('context-menu:show-namespace', (event, namespace: string) => { } }) -// Assistant context menu handler -ipcMain.on('context-menu:show-assistant', (event, assistantName: string) => { - const template: MenuItemConstructorOptions[] = [ - { - label: 'Edit', - click: () => event.sender.send('context-menu:assistant-action', { action: 'edit', assistantName }) - }, - { type: 'separator' }, - { - label: 'Delete', - click: () => event.sender.send('context-menu:assistant-action', { action: 'delete', assistantName }) - } - ] - const menu = Menu.buildFromTemplate(template) - const win = BrowserWindow.fromWebContents(event.sender) - if (win) { - menu.popup({ window: win }) - } -}) - -// File context menu handler -ipcMain.on('context-menu:show-file', (event, assistantName: string, fileId: string, fileName: string) => { - const template: MenuItemConstructorOptions[] = [ - { - label: 'Download', - click: () => event.sender.send('context-menu:file-action', { action: 'download', assistantName, fileId, fileName }) - }, - { type: 'separator' }, - { - label: 'Delete', - click: () => event.sender.send('context-menu:file-action', { action: 'delete', assistantName, fileId, fileName }) - } - ] - const menu = Menu.buildFromTemplate(template) - const win = BrowserWindow.fromWebContents(event.sender) - if (win) { - menu.popup({ window: win }) - } -}) - // ============================================================================ // Profile Management IPC Handlers // ============================================================================ @@ -981,75 +940,6 @@ ipcMain.handle('assistant:files:delete', async (_event, profileId: string, assis } }) -// ============================================================================ -// Assistant Chat IPC Handlers -// ============================================================================ - -// Track active chat streams for cancellation -const activeChatStreams: Map = new Map() - -ipcMain.handle('assistant:chat', async (_event, profileId: string, assistantName: string, params: { messages: Array<{ role: 'user' | 'assistant'; content: string }>; model?: string; filter?: Record }) => { - try { - const service = pineconeConnectionPool.getConnection(profileId) - if (!service) { - return { success: false, error: 'Not connected to Pinecone' } - } - const assistantService = service.getAssistantService() - const response = await assistantService.chat(assistantName, params) - return { success: true, data: response } - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to send chat message' - return { success: false, error: message } - } -}) - -ipcMain.handle('assistant:chat:stream:start', async (event, profileId: string, assistantName: string, params: { messages: Array<{ role: 'user' | 'assistant'; content: string }>; model?: string; filter?: Record }) => { - try { - const service = pineconeConnectionPool.getConnection(profileId) - if (!service) { - return { success: false, error: 'Not connected to Pinecone' } - } - - const streamId = crypto.randomUUID() - const abortController = new AbortController() - activeChatStreams.set(streamId, abortController) - - const assistantService = service.getAssistantService() - - // Start streaming in background - assistantService.chatStream( - assistantName, - params, - (chunk) => { - // Send chunk to renderer - event.sender.send('assistant:chat:chunk', streamId, chunk) - }, - abortController.signal - ).finally(() => { - activeChatStreams.delete(streamId) - }) - - return { success: true, data: { streamId } } - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to start chat stream' - return { success: false, error: message } - } -}) - -ipcMain.handle('assistant:chat:stream:cancel', async (_event, streamId: string) => { - try { - const controller = activeChatStreams.get(streamId) - if (controller) { - controller.abort() - activeChatStreams.delete(streamId) - } - return { success: true } - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to cancel chat stream' - return { success: false, error: message } - } -}) - // ============================================================================ // Window Management IPC Handlers // ============================================================================ @@ -1222,30 +1112,6 @@ ipcMain.handle('shell:openExternal', async (_event, url: string) => { } }) -// ============================================================================ -// Dialog IPC Handlers -// ============================================================================ - -ipcMain.handle('dialog:showOpenDialog', async (_event, options: { - properties?: Array<'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles'> - filters?: Array<{ name: string; extensions: string[] }> - title?: string - defaultPath?: string -}) => { - try { - const result = await dialog.showOpenDialog({ - properties: options.properties || ['openFile'], - filters: options.filters, - title: options.title, - defaultPath: options.defaultPath, - }) - return { success: true, data: result } - } catch (error) { - const message = error instanceof Error ? error.message : 'Failed to show dialog' - return { success: false, error: message } - } -}) - // ============================================================================ // App Lifecycle // ============================================================================ diff --git a/electron/preload.ts b/electron/preload.ts index 13d5075..0492819 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -29,9 +29,6 @@ import { AssistantFile, ListAssistantFilesFilter, UploadAssistantFileParams, - ChatParams, - ChatResponse, - ChatStreamChunk, } from './types' console.log('Preload script is running!') @@ -212,22 +209,6 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.on('context-menu:namespace-action', handler) return () => ipcRenderer.removeListener('context-menu:namespace-action', handler) }, - showAssistantMenu: (assistantName: string): void => { - ipcRenderer.send('context-menu:show-assistant', assistantName) - }, - onAssistantAction: (callback: (action: { action: string; assistantName: string }) => void): (() => void) => { - const handler = (_event: any, data: { action: string; assistantName: string }) => callback(data) - ipcRenderer.on('context-menu:assistant-action', handler) - return () => ipcRenderer.removeListener('context-menu:assistant-action', handler) - }, - showFileMenu: (assistantName: string, fileId: string, fileName: string): void => { - ipcRenderer.send('context-menu:show-file', assistantName, fileId, fileName) - }, - onFileAction: (callback: (action: { action: string; assistantName: string; fileId: string; fileName: string }) => void): (() => void) => { - const handler = (_event: any, data: { action: string; assistantName: string; fileId: string; fileName: string }) => callback(data) - ipcRenderer.on('context-menu:file-action', handler) - return () => ipcRenderer.removeListener('context-menu:file-action', handler) - }, }, profiles: { getAll: async (): Promise => { @@ -398,37 +379,6 @@ contextBridge.exposeInMainWorld('electronAPI', { } }, }, - chat: async (profileId: string, assistantName: string, params: ChatParams): Promise => { - const result = await ipcRenderer.invoke('assistant:chat', profileId, assistantName, params) - if (!result.success) { - throw new Error(result.error) - } - return result.data - }, - chatStream: { - start: async (profileId: string, assistantName: string, params: ChatParams): Promise => { - const result = await ipcRenderer.invoke('assistant:chat:stream:start', profileId, assistantName, params) - if (!result.success) { - throw new Error(result.error) - } - return result.data.streamId - }, - cancel: async (streamId: string): Promise => { - const result = await ipcRenderer.invoke('assistant:chat:stream:cancel', streamId) - if (!result.success) { - throw new Error(result.error) - } - }, - onChunk: (callback: (streamId: string, chunk: ChatStreamChunk) => void): (() => void) => { - const handler = (_event: unknown, streamId: string, chunk: ChatStreamChunk) => { - callback(streamId, chunk) - } - ipcRenderer.on('assistant:chat:chunk', handler) - return () => { - ipcRenderer.removeListener('assistant:chat:chunk', handler) - } - }, - }, }, window: { createConnection: async (profile: ConnectionProfile): Promise<{ windowId: string }> => { @@ -511,20 +461,6 @@ contextBridge.exposeInMainWorld('electronAPI', { } }, }, - dialog: { - showOpenDialog: async (options: { - properties?: Array<'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles'> - filters?: Array<{ name: string; extensions: string[] }> - title?: string - defaultPath?: string - }): Promise<{ canceled: boolean; filePaths: string[] }> => { - const result = await ipcRenderer.invoke('dialog:showOpenDialog', options) - if (!result.success) { - throw new Error(result.error) - } - return result.data - }, - }, updater: { checkForUpdates: async (): Promise => { const result = await ipcRenderer.invoke('updater:check') @@ -668,49 +604,6 @@ contextBridge.exposeInMainWorld('electronAPI', { ipcRenderer.on('menu:show-shortcuts', handler) return () => ipcRenderer.removeListener('menu:show-shortcuts', handler) }, - // Mode switching events - onSwitchToIndexMode: (callback: () => void): (() => void) => { - const handler = () => callback() - ipcRenderer.on('menu:switch-to-index-mode', handler) - return () => ipcRenderer.removeListener('menu:switch-to-index-mode', handler) - }, - onSwitchToAssistantMode: (callback: () => void): (() => void) => { - const handler = () => callback() - ipcRenderer.on('menu:switch-to-assistant-mode', handler) - return () => ipcRenderer.removeListener('menu:switch-to-assistant-mode', handler) - }, - // Assistant menu events - onNewAssistant: (callback: () => void): (() => void) => { - const handler = () => callback() - ipcRenderer.on('menu:new-assistant', handler) - return () => ipcRenderer.removeListener('menu:new-assistant', handler) - }, - onEditAssistant: (callback: () => void): (() => void) => { - const handler = () => callback() - ipcRenderer.on('menu:edit-assistant', handler) - return () => ipcRenderer.removeListener('menu:edit-assistant', handler) - }, - onDeleteAssistant: (callback: () => void): (() => void) => { - const handler = () => callback() - ipcRenderer.on('menu:delete-assistant', handler) - return () => ipcRenderer.removeListener('menu:delete-assistant', handler) - }, - // Chat menu events - onSendMessage: (callback: () => void): (() => void) => { - const handler = () => callback() - ipcRenderer.on('menu:send-message', handler) - return () => ipcRenderer.removeListener('menu:send-message', handler) - }, - onFocusChatInput: (callback: () => void): (() => void) => { - const handler = () => callback() - ipcRenderer.on('menu:focus-chat-input', handler) - return () => ipcRenderer.removeListener('menu:focus-chat-input', handler) - }, - onClearConversation: (callback: () => void): (() => void) => { - const handler = () => callback() - ipcRenderer.on('menu:clear-conversation', handler) - return () => ipcRenderer.removeListener('menu:clear-conversation', handler) - }, }, onRefresh: (callback: () => void): (() => void) => { const handler = () => { diff --git a/electron/types.ts b/electron/types.ts index eb6dcea..bedca05 100644 --- a/electron/types.ts +++ b/electron/types.ts @@ -489,117 +489,3 @@ export interface UploadAssistantFileParams { metadata?: Record } -// ============================================================================ -// Assistant Chat Types -// ============================================================================ - -/** - * A message in a chat conversation - */ -export interface ChatMessage { - role: 'user' | 'assistant' - content: string -} - -/** - * Context options for controlling context snippets sent to the LLM - */ -export interface ChatContextOptions { - /** Maximum number of context snippets to use. Default is 16. Maximum is 64. */ - topK?: number - /** Maximum context snippet size. Default is 2048 tokens. Minimum is 512. Maximum is 8192. */ - snippetSize?: number -} - -/** - * Parameters for chat requests - */ -export interface ChatParams { - /** Messages to send to the assistant */ - messages: ChatMessage[] - /** Model to use for generation (e.g., 'gpt-4o', 'claude-3-5-sonnet') */ - model?: string - /** Temperature for response randomness (0-1) */ - temperature?: number - /** Filter against which documents can be retrieved */ - filter?: Record - /** If true, the assistant will return a JSON response */ - jsonResponse?: boolean - /** If true, include highlights from referenced documents */ - includeHighlights?: boolean - /** Context options for controlling snippets */ - contextOptions?: ChatContextOptions -} - -/** - * Reference to a file in a citation - */ -export interface CitationReference { - file: { - name: string - id: string - } - pages?: number[] -} - -/** - * A citation from assistant response - */ -export interface Citation { - /** Position in the response text */ - position: number - /** References to files */ - references: CitationReference[] -} - -/** - * Usage statistics for a chat response - */ -export interface ChatUsage { - promptTokens: number - completionTokens: number - totalTokens: number -} - -/** - * Complete chat response (non-streaming) - */ -export interface ChatResponse { - /** Unique identifier for the response */ - id: string - /** The assistant's response message */ - message: ChatMessage - /** Citations from the response */ - citations?: Citation[] - /** Token usage statistics */ - usage?: ChatUsage - /** Model used for generation */ - model?: string - /** Reason for completion (stop, length, content_filter, function_call) */ - finishReason?: string -} - -/** - * A chunk from a streaming chat response - */ -export interface ChatStreamChunk { - /** Type of chunk */ - type: 'message_start' | 'content' | 'citation' | 'message_end' | 'error' - /** Response ID (present in all chunks) */ - id?: string - /** Model used (present in all chunks) */ - model?: string - /** Role (only in message_start) */ - role?: string - /** Content delta (only in content chunks) */ - content?: string - /** Citation data (only in citation chunks) */ - citation?: Citation - /** Usage statistics (only in message_end) */ - usage?: ChatUsage - /** Finish reason (only in message_end) */ - finishReason?: string - /** Error message (only in error chunks) */ - error?: string -} - diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 21f3358..a32a72a 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -87,64 +87,6 @@ declare global { metadata?: Record } - // Assistant Chat types - interface ChatMessage { - role: 'user' | 'assistant' - content: string - } - - interface ChatContextOptions { - topK?: number - snippetSize?: number - } - - interface ChatParams { - messages: ChatMessage[] - model?: string - temperature?: number - filter?: Record - jsonResponse?: boolean - includeHighlights?: boolean - contextOptions?: ChatContextOptions - } - - interface CitationReference { - file: { name: string; id: string } - pages?: number[] - } - - interface Citation { - position: number - references: CitationReference[] - } - - interface ChatUsage { - promptTokens: number - completionTokens: number - totalTokens: number - } - - interface ChatResponse { - id: string - message: ChatMessage - citations?: Citation[] - usage?: ChatUsage - model?: string - finishReason?: string - } - - interface ChatStreamChunk { - type: 'message_start' | 'content' | 'citation' | 'message_end' | 'error' - id?: string - model?: string - role?: string - content?: string - citation?: Citation - usage?: ChatUsage - finishReason?: string - error?: string - } - interface ConnectionProfile { id: string name: string @@ -402,10 +344,6 @@ declare global { onProfileAction: (callback: (action: { action: string; profileId: string }) => void) => () => void showNamespaceMenu: (namespace: string) => void onNamespaceAction: (callback: (action: { action: string; namespace: string }) => void) => () => void - showAssistantMenu: (assistantName: string) => void - onAssistantAction: (callback: (action: { action: string; assistantName: string }) => void) => () => void - showFileMenu: (assistantName: string, fileId: string, fileName: string) => void - onFileAction: (callback: (action: { action: string; assistantName: string; fileId: string; fileName: string }) => void) => () => void } profiles: { getAll: () => Promise @@ -437,12 +375,6 @@ declare global { upload: (profileId: string, assistantName: string, params: UploadAssistantFileParams) => Promise delete: (profileId: string, assistantName: string, fileId: string) => Promise } - chat: (profileId: string, assistantName: string, params: ChatParams) => Promise - chatStream: { - start: (profileId: string, assistantName: string, params: ChatParams) => Promise - cancel: (streamId: string) => Promise - onChunk: (callback: (streamId: string, chunk: ChatStreamChunk) => void) => () => void - } } window: { createConnection: (profile: ConnectionProfile) => Promise<{ windowId: string }> @@ -462,14 +394,6 @@ declare global { shell: { openExternal: (url: string) => Promise } - dialog: { - showOpenDialog: (options: { - properties?: Array<'openFile' | 'openDirectory' | 'multiSelections' | 'showHiddenFiles'> - filters?: Array<{ name: string; extensions: string[] }> - title?: string - defaultPath?: string - }) => Promise<{ canceled: boolean; filePaths: string[] }> - } updater: { checkForUpdates: () => Promise downloadUpdate: () => Promise @@ -503,17 +427,6 @@ declare global { onClearFilters: (callback: () => void) => () => void onAddFilter: (callback: () => void) => () => void onRemoveFilter: (callback: () => void) => () => void - // Mode switching events - onSwitchToIndexMode: (callback: () => void) => () => void - onSwitchToAssistantMode: (callback: () => void) => () => void - // Assistant menu events - onNewAssistant: (callback: () => void) => () => void - onEditAssistant: (callback: () => void) => () => void - onDeleteAssistant: (callback: () => void) => () => void - // Chat menu events - onSendMessage: (callback: () => void) => () => void - onFocusChatInput: (callback: () => void) => () => void - onClearConversation: (callback: () => void) => () => void // Window menu events onDisconnect: (callback: () => void) => () => void // Help menu events From 84f99ab61f9b821bf4e4cb88377f6d9cf0ccb78a Mon Sep 17 00:00:00 2001 From: "Scout (Lead Tester)" Date: Thu, 5 Feb 2026 06:47:23 +0000 Subject: [PATCH 2/4] fix: address review comments - pnpm workspace, linear config, AssistantStatus type --- electron/types.ts | 2 +- src/types/electron.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/electron/types.ts b/electron/types.ts index bedca05..c627754 100644 --- a/electron/types.ts +++ b/electron/types.ts @@ -405,7 +405,7 @@ export interface GetVectorsPaginatedParams { /** * Assistant status */ -export type AssistantStatus = 'Initializing' | 'Ready' | 'Failed' | 'Terminating' +export type AssistantStatus = 'Initializing' | 'Ready' | 'Failed' | 'Terminating' | 'InitializationFailed' /** * Assistant model representing a Pinecone Assistant diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index a32a72a..85c058d 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -39,7 +39,7 @@ declare global { type ExplorerMode = 'index' | 'assistant' // Assistant API Types - type AssistantStatus = 'Initializing' | 'Ready' | 'Failed' | 'Terminating' + type AssistantStatus = 'Initializing' | 'Ready' | 'Failed' | 'Terminating' | 'InitializationFailed' interface AssistantModel { name: string From 43709fd46296c1654ae6c52bef8609d23c7cb492 Mon Sep 17 00:00:00 2001 From: "Scout (Lead Tester)" Date: Thu, 5 Feb 2026 06:51:19 +0000 Subject: [PATCH 3/4] fix: address review comments on PINE-40 - AssistantsPanel: Convert assistant row div to button for keyboard accessibility - AssistantsPanel: Add aria-pressed attribute for active state - ModeContext: Fix stale setMode closure by adding to useEffect dependencies - ModeContext: Reorder setMode definition before keyboard effect --- src/components/assistants/AssistantsPanel.tsx | 6 ++-- src/context/ModeContext.tsx | 31 +++---------------- 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/src/components/assistants/AssistantsPanel.tsx b/src/components/assistants/AssistantsPanel.tsx index d2105b7..81188df 100644 --- a/src/components/assistants/AssistantsPanel.tsx +++ b/src/components/assistants/AssistantsPanel.tsx @@ -248,8 +248,10 @@ export function AssistantsPanel({ onToggleCollapse, onCreateNew, onEditAssistant const isActive = assistant.name === activeAssistant return ( -
)} -
+ ) })} diff --git a/src/context/ModeContext.tsx b/src/context/ModeContext.tsx index 2a62c41..9ca0803 100644 --- a/src/context/ModeContext.tsx +++ b/src/context/ModeContext.tsx @@ -1,4 +1,4 @@ -import { createContext, useContext, useState, useEffect, useCallback, useRef, ReactNode } from 'react' +import { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react' import { usePinecone } from '../providers/PineconeProvider' export type ExplorerMode = 'index' | 'assistant' @@ -15,9 +15,6 @@ export function ModeProvider({ children }: { children: ReactNode }) { const [mode, setModeState] = useState('index') const [isInitialized, setIsInitialized] = useState(false) - // Store setMode in a ref for stable access in event handlers - const setModeRef = useRef<(mode: ExplorerMode) => void>(() => {}) - // Load initial mode from electron-store useEffect(() => { if (!currentProfile) return @@ -47,11 +44,6 @@ export function ModeProvider({ children }: { children: ReactNode }) { } }, [currentProfile]) - // Update ref when setMode changes - useEffect(() => { - setModeRef.current = setMode - }, [setMode]) - // Handle keyboard shortcuts useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -59,32 +51,17 @@ export function ModeProvider({ children }: { children: ReactNode }) { if (e.metaKey || e.ctrlKey) { if (e.key === '1') { e.preventDefault() - setModeRef.current('index') + setMode('index') } else if (e.key === '2') { e.preventDefault() - setModeRef.current('assistant') + setMode('assistant') } } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) - }, []) - - // Handle menu IPC events for mode switching - useEffect(() => { - const unsubIndexMode = window.electronAPI.menu.onSwitchToIndexMode(() => { - setModeRef.current('index') - }) - const unsubAssistantMode = window.electronAPI.menu.onSwitchToAssistantMode(() => { - setModeRef.current('assistant') - }) - - return () => { - unsubIndexMode() - unsubAssistantMode() - } - }, []) + }, [setMode]) // Don't render children until we've loaded the initial mode if (!isInitialized) { From c3a86ad62dfa5894685f6189d4479f465fd899c0 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Thu, 5 Feb 2026 18:27:50 +0000 Subject: [PATCH 4/4] fix: remove unused ExplorerMode type from electron/types.ts The ExplorerMode type was defined in electron/types.ts but never imported or used. The only active definition is in src/context/ModeContext.tsx. This change removes the duplicate definition and updates ConnectionProfile.preferredMode to use an inline union type. Co-authored-by: Stepan Arsentjev --- electron/types.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/electron/types.ts b/electron/types.ts index c627754..4222224 100644 --- a/electron/types.ts +++ b/electron/types.ts @@ -50,11 +50,6 @@ export interface HybridEmbeddingConfig { defaultAlpha?: number // Default alpha for queries (0.5 if not set) } -/** - * Explorer mode type - Index Explorer or Assistant Explorer - */ -export type ExplorerMode = 'index' | 'assistant' - /** * Connection profile for Pinecone */ @@ -82,7 +77,7 @@ export interface ConnectionProfile { textFieldOverrides?: Record // Preferred explorer mode (index or assistant) - preferredMode?: ExplorerMode + preferredMode?: 'index' | 'assistant' } /**