Skip to content

feat: Add ChatView main component#45

Merged
stepandel merged 1 commit intofeat/assistant-modefrom
pine-45-chat-view
Feb 5, 2026
Merged

feat: Add ChatView main component#45
stepandel merged 1 commit intofeat/assistant-modefrom
pine-45-chat-view

Conversation

@stepandel
Copy link
Copy Markdown
Owner

@stepandel stepandel commented Feb 5, 2026

Summary

Create the main chat interface that replaces VectorsView when in Assistant mode.

Changes

  • Add ChatView.tsx with scrollable message list and input area
  • Add useChatStream.ts hook for streaming state management
  • Update MainContent.tsx to render ChatView in assistant mode
  • Model selector (gpt-4o, claude-3-5-sonnet, gemini-2.0-flash)
  • Auto-scroll, citation display, clear conversation button
  • Submit on Enter, Shift+Enter for newline

Files Changed

  • src/components/chat/ChatView.tsx (new)
  • src/hooks/useChatStream.ts (new)
  • src/components/layout/MainContent.tsx

Testing

  • Build passes: pnpm test:build
  • E2E tests: ⚠️ Skipped (requires Electron system deps not installed on this machine)

Closes PINE-45

Summary by CodeRabbit

Release Notes

New Features

  • Added Assistant mode to explore Pinecone Assistants alongside the existing Index mode
  • Introduced a chat interface for real-time, streaming conversations with assistants
  • Added file upload and management capabilities for assistant knowledge bases
  • Implemented mode switcher to toggle between Index and Assistant explorers
  • Added support for citations in chat responses linking to source documents

UI Enhancements

  • New Assistant panel displaying available assistants with status indicators
  • Chat view with message history, model selection, and streaming indicators
  • File manager panel with search, upload, and deletion functionality
  • Dialog support for file selection workflows

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 5, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • 🔍 Trigger a full review
📝 Walkthrough

Walkthrough

This pull request introduces a comprehensive Pinecone Assistant feature suite, adding a new mode to the explorer application. It includes a backend service layer for Assistant API operations, IPC handlers to expose these operations to the renderer process, multiple React components for UI (panels, dialogs, chat interface), context providers for state management, React Query hooks for data fetching and mutations, and supporting type definitions across the stack. The feature enables users to switch between Index and Assistant modes, manage assistants, upload files, and engage in streaming chat conversations.

Changes

Cohort / File(s) Summary
Configuration & Workspace Setup
.linear.toml, pnpm-workspace.yaml
Added Linear CLI configuration and pnpm workspace settings for built-dependency management.
Backend Service Layer
electron/assistant-service.ts, electron/pinecone-service.ts
Introduced AssistantService class (302 lines) providing CRUD and chat operations for Pinecone Assistant API; integrated into PineconeService via lazy initialization.
Data Persistence
electron/connection-store.ts
Added getPreferredMode and setPreferredMode methods to persist user's mode preference per profile.
IPC Handlers
electron/main.ts
Expanded with 13+ new IPC handler endpoints for assistant management, file operations, chat streaming (with AbortController tracking), profile mode, and file dialogs; includes streaming chunk emission and stream cancellation.
Electron-Renderer Bridge
electron/preload.ts, src/types/electron.d.ts
Exposed new assistant, files, chat streaming, dialog, and profile preference APIs to renderer via preload; added type definitions for ExplorerMode, AssistantModel, ChatStreamChunk, and related structures. Note: electron/types.ts shows duplicate Assistant declarations (+213 lines, may indicate merge artifact).
Mode Management Context
src/context/ModeContext.tsx
Introduced mode context system with keyboard shortcuts (Cmd/Ctrl+1/2) and per-profile mode persistence via electron-store.
Assistant & File Selection Contexts
src/context/AssistantSelectionContext.tsx, src/context/FileSelectionContext.tsx
Added lightweight context providers for tracking active assistant and file selections.
Assistant List & Detail UI
src/components/assistants/AssistantsPanel.tsx
New panel component rendering assistants with status indicators, loading/error states, and active selection highlighting; includes placeholder context menu hooks.
File Management UI
src/components/files/FilesPanel.tsx, src/components/files/FileDetailPanel.tsx, src/components/files/UploadFileDialog.tsx, src/components/files/index.ts
Added FilesPanel (searchable, sortable file list with status), FileDetailPanel (metadata display, copy ID, download, delete), and UploadFileDialog (drag-drop support, metadata JSON editor, PDF-specific toggle); re-exported via index file.
Chat Interface
src/components/chat/ChatView.tsx
New ChatView component with message history, model selector, streaming controls, citation display, and responsive input with send/stop buttons.
Mode Switcher UI
src/components/mode/ModeSwitcher.tsx, src/components/layout/TopBar.tsx
Added ModeSwitcher tab component for Index/Assistant mode selection; integrated into TopBar center area.
Layout Integration
src/components/layout/MainContent.tsx
Conditionally renders ChatView when mode is 'assistant'; retains existing draftIndex/draftNamespace branches for index mode.
Data Fetching & State
src/hooks/useAssistantQueries.ts, src/hooks/useChatStream.ts
Implemented React Query hooks for assistant/file queries and mutations with optimistic updates; added useChatStream hook managing streaming chat with chunk-based message updates, model switching, and proper cleanup.
Provider Setup
src/windows/ConnectionWindow.tsx
Wrapped component tree with ModeProvider, AssistantSelectionProvider, and FileSelectionProvider to enable mode, assistant, and file selection contexts throughout the app.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as UI Components
    participant Hook as useChatStream
    participant IPC as Electron IPC
    participant Service as AssistantService
    participant API as Pinecone API

    User->>UI: Sends chat message
    UI->>Hook: sendMessage(text)
    Hook->>Hook: Add user message to messages
    Hook->>Hook: Create placeholder assistant message
    Hook->>IPC: assistant.chatStream.start(params)
    IPC->>Service: chatStream(assistantName, params, callback)
    Service->>API: Initiate streaming chat request
    
    loop Stream Processing
        API-->>Service: Emit chunk (message_start/content/citation/message_end)
        Service->>IPC: Invoke onChunk callback with chunk
        IPC->>Hook: Emit assistant:chat:chunk event
        Hook->>Hook: Update assistant message based on chunk type
        UI->>UI: Re-render with updated message
    end
    
    alt Streaming Completes
        API-->>Service: message_end chunk
        Hook->>Hook: Set usage & finalize message
    else User Cancels
        User->>UI: Click Cancel/Stop button
        UI->>Hook: cancelStream()
        Hook->>IPC: assistant.chatStream.cancel(streamId)
        IPC->>Service: Abort stream via AbortController
        Hook->>Hook: Clean up and stop streaming
    end
Loading
sequenceDiagram
    participant User
    participant TopBar as TopBar/ModeSwitcher
    participant ModeContext as ModeContext
    participant Store as electron-store
    participant UI as MainContent

    User->>TopBar: Click 'Assistant' tab (or Cmd/Ctrl+2)
    TopBar->>ModeContext: setMode('assistant')
    ModeContext->>Store: Save mode for current profile
    ModeContext->>UI: Provide updated mode value
    
    UI->>UI: Check mode === 'assistant'
    alt Active Assistant Selected
        UI->>UI: Render ChatView(assistantName)
    else No Assistant Selected
        UI->>UI: Show "Select an assistant" prompt
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Hops through assistants with glee,
Streaming chats flow wild and free,
Files upload with drag-and-drop delight,
Mode switching—index to assistant flight!
A feature fest, robust and right,
Pinecone Explorer takes flight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: Add ChatView main component' accurately describes the primary change—introducing a new ChatView component that serves as the main chat interface for the assistant mode.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch pine-45-chat-view

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown

claude bot commented Feb 5, 2026

Code review

No issues found. Checked for bugs and CLAUDE.md compliance.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Fix all issues with AI agents
In `@electron/assistant-service.ts`:
- Around line 177-203: The chat method in assistant-service.ts is suppressing
types and omitting ChatParams fields (the `@ts-expect-error` and missing
temperature and contextOptions) when calling assistant.chat; update the call in
async chat(assistantName: string, params: ChatParams) to forward all supported
fields from params (at minimum include temperature and contextOptions plus
jsonResponse, filter, model, messages, includeHighlights) without using
`@ts-expect-error` so the SDK's ChatOptions are fully passed through, and keep the
existing response mapping (id, message, citations, usage, model, finishReason)
unchanged.

In `@electron/main.ts`:
- Around line 966-997: The chat streaming call (ipcMain.handle
'assistant:chat:stream:start') must catch rejections and guard sends against a
destroyed renderer: attach .catch and .finally to
assistantService.chatStream(...) so any rejection is caught and event.sender is
notified (e.g., send an 'assistant:chat:error' with streamId and error message),
and before calling event.sender.send(...) verify the sender is alive (use
event.sender && !event.sender.isDestroyed()) and wrap sends in try/catch to
avoid thrown exceptions; ensure activeChatStreams.delete(streamId) remains in
finally so the stream is cleaned up and abortController.signal is still passed
to chatStream.

In `@electron/pinecone-service.ts`:
- Around line 49-60: The getAssistantService() method returns a cached
assistantService that is not cleared on disconnect(), causing stale
AssistantService instances to hold the old client; update disconnect() to null
out the assistantService property so a new AssistantService(this.client) is
created after reconnect, i.e., ensure disconnect() clears assistantService in
addition to client, embeddingService, and profile so getAssistantService() will
construct a fresh AssistantService tied to the new client.

In `@electron/types.ts`:
- Around line 53-56: The ExplorerMode type is duplicated across the repo; remove
the duplicate definitions and centralize the type by exporting the single
ExplorerMode from the existing global declaration and importing it where needed
(e.g., replace local type defs in ModeContext and in the electron types file).
Update references to the type to import ExplorerMode instead of redeclaring it,
ensuring ModeContext and any consumers use the exported type to maintain a
single source of truth.

In `@src/components/assistants/AssistantsPanel.tsx`:
- Around line 148-180: The assistant list item uses a plain clickable <div>
which is not keyboard-accessible; update the element (the one rendering
assistant.name and calling handleAssistantClick/handleAssistantContextMenu) to
be a semantic interactive element (preferably a <button>) or add role="button",
tabIndex={0} and key handlers for Enter/Space to call handleAssistantClick;
ensure existing onContextMenu behavior (handleAssistantContextMenu) and
title/getStatusTooltip/getStatusColor logic remain unchanged and preserve
styling and active state (isActive) and the assistant.instructions rendering.

In `@src/components/chat/ChatView.tsx`:
- Around line 141-149: The handleKeyDown keyboard handler submits on Enter and
should guard against IME composition; update handleKeyDown (the
KeyboardEvent<HTMLTextAreaElement> handler that calls handleSubmit) to check
e.nativeEvent.isComposing and return early when true so Enter during composition
does not submit, keeping the existing Shift+Enter behavior and
e.preventDefault() logic intact.
- Around line 210-212: The message list uses messages.map(... key={idx}) causing
unstable keys; change the render to use a stable message.id (MessageBubble
key={message.id}) and ensure useChatStream creates stable IDs for user messages
when they are added (e.g., generate a UUID or timestamp at creation instead of
relying on index fallback), while keeping assistant messages' existing id
assignment on message_start; update any code paths that currently set
role-${idx} as fallback to always set a real id at message creation.

In `@src/components/files/UploadFileDialog.tsx`:
- Around line 26-176: The multimodal checkbox state (multimodal) is never sent
with uploads, so update handleUpload to include multimodal in the payload passed
to uploadMutation.mutateAsync (currently called in handleUpload) and update the
IPC/upload handler and types that receive { filePath, metadata } so they accept
and propagate { multimodal: boolean } (or remove/disable the checkbox UI in the
render if you decide not to support multimodal). Ensure uploadMutation hook/type
signatures and the Electron IPC handler that processes the upload are updated to
accept and act on the new multimodal flag.

In `@src/hooks/useChatStream.ts`:
- Around line 234-255: In cancelStream (useChatStream) after marking the last
assistant message isStreaming = false, also drop that last message entirely when
its content is empty/blank to avoid leaving an empty assistant bubble: locate
cancelStream and the setMessages updater that inspects updated[lastIdx] (uses
currentStreamIdRef, unsubscribeRef, setIsStreaming), and change the updater to
remove the last element if its role === 'assistant' and its content.trim() is
empty (instead of keeping it with empty content).
- Around line 34-57: When assistantName changes, reset the conversation: inside
the useChatStream hook add an effect that listens to assistantName and, on
change, clears messages via setMessages([]), cancels any active stream using
currentStreamIdRef (call window.electronAPI.assistant.chatStream.cancel(...) and
handle errors), call unsubscribeRef.current() if present, and reset related
state such as setIsStreaming(false) and setError(null); reference the existing
symbols useChatStream, assistantName, setMessages, currentStreamIdRef,
unsubscribeRef, setIsStreaming, and setError to locate where to implement this
cleanup.
🧹 Nitpick comments (4)
src/components/files/FilesPanel.tsx (1)

168-175: Add an accessible label to the clear-search button.

The “✕” control needs a non-visual label for screen readers.

♿ Suggested tweak
           {searchTerm && (
             <button
+              aria-label="Clear search"
+              title="Clear search"
               onClick={() => setSearchTerm('')}
               className="absolute right-1 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors text-xs w-4 h-4 flex items-center justify-center"
             >
               ✕
             </button>
           )}
src/components/files/UploadFileDialog.tsx (1)

132-153: Validate dropped files against the allowed extensions (and ignore drops while uploading).
Line 137 accepts any dropped file, so drag‑and‑drop can bypass the picker filters and upload unsupported types. Also consider ignoring drops while isUploading to avoid state churn.

🔧 Suggested update
-  const handleDrop = useCallback((e: React.DragEvent) => {
+  const handleDrop = useCallback((e: React.DragEvent) => {
     e.preventDefault()
     e.stopPropagation()
     setIsDragging(false)
+    if (isUploading) return

     const files = e.dataTransfer.files
     if (files.length > 0) {
       const file = files[0]
+      const allowedExtensions = new Set(['pdf', 'txt', 'md', 'json', 'csv', 'doc', 'docx'])
+      const ext = file.name.split('.').pop()?.toLowerCase()
+      if (!ext || !allowedExtensions.has(ext)) {
+        setUploadError('Unsupported file type')
+        return
+      }
       // Note: In Electron, we need to use the file path from the dropped file
       // The path property is available in Electron's drag-and-drop
       const filePath = (file as any).path
@@
-  }, [])
+  }, [isUploading])
src/components/assistants/AssistantsPanel.tsx (1)

64-82: Resolve TODO/console logging in context-menu handlers.
Lines 64‑82 leave TODOs and unconditional console.log. Either wire the Electron context menu or guard/remove logs before release.

If you want, I can sketch the context‑menu integration using window.electronAPI.contextMenu.

src/hooks/useAssistantQueries.ts (1)

219-236: Consider adding fileDetail to the query key factory.

The inline key construction differs from the pattern used elsewhere. Adding a factory function would improve consistency and make cache invalidation easier.

 export const assistantQueryKeys = {
   all: ['assistant'] as const,
   list: (profileId: string) => [...assistantQueryKeys.all, 'list', profileId] as const,
   detail: (profileId: string, name: string) => [...assistantQueryKeys.all, 'detail', profileId, name] as const,
   files: (profileId: string, assistantName: string) => [...assistantQueryKeys.all, 'files', profileId, assistantName] as const,
+  fileDetail: (profileId: string, assistantName: string, fileId: string) => [...assistantQueryKeys.files(profileId, assistantName), 'detail', fileId] as const,
 }

Comment on lines +53 to +56
/**
* Explorer mode type - Index Explorer or Assistant Explorer
*/
export type ExplorerMode = 'index' | 'assistant'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "type ExplorerMode\s*=" --type=ts

Repository: stepandel/pinecone-explorer

Length of output: 290


🏁 Script executed:

# Check each file to understand the definitions and their context
echo "=== src/types/electron.d.ts ===" && sed -n '35,45p' src/types/electron.d.ts

echo -e "\n=== src/context/ModeContext.tsx ===" && sed -n '1,10p' src/context/ModeContext.tsx

echo -e "\n=== electron/types.ts ===" && sed -n '50,60p' electron/types.ts

Repository: stepandel/pinecone-explorer

Length of output: 1071


🏁 Script executed:

# Check if the files are importing ExplorerMode from each other or defining independently
echo "=== Checking imports in ModeContext.tsx ===" && head -20 src/context/ModeContext.tsx | grep -E "import|ExplorerMode"

echo -e "\n=== Checking imports in electron/types.ts ===" && head -20 electron/types.ts | grep -E "import|ExplorerMode"

echo -e "\n=== Checking context of electron.d.ts ===" && sed -n '1,50p' src/types/electron.d.ts | head -40

Repository: stepandel/pinecone-explorer

Length of output: 2042


ExplorerMode is defined in three places - consolidate to a single source of truth.

This type is defined in src/types/electron.d.ts (line 39), src/context/ModeContext.tsx (line 4), and electron/types.ts (line 56). Having the same type defined in multiple places can lead to inconsistency if one is updated without the others. Consider exporting from a single location (likely src/types/electron.d.ts as a global type declaration) and importing where needed.

🤖 Prompt for AI Agents
In `@electron/types.ts` around lines 53 - 56, The ExplorerMode type is duplicated
across the repo; remove the duplicate definitions and centralize the type by
exporting the single ExplorerMode from the existing global declaration and
importing it where needed (e.g., replace local type defs in ModeContext and in
the electron types file). Update references to the type to import ExplorerMode
instead of redeclaring it, ensuring ModeContext and any consumers use the
exported type to maintain a single source of truth.

Comment on lines +210 to +212
{messages.map((message, idx) => (
<MessageBubble key={idx} message={message} />
))}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd ChatView.tsx

Repository: stepandel/pinecone-explorer

Length of output: 103


🏁 Script executed:

find . -type f -name "*.tsx" -o -name "*.ts" | grep -i chat | head -20

Repository: stepandel/pinecone-explorer

Length of output: 134


🏁 Script executed:

wc -l src/components/chat/ChatView.tsx

Repository: stepandel/pinecone-explorer

Length of output: 107


🏁 Script executed:

sed -n '200,225p' src/components/chat/ChatView.tsx

Repository: stepandel/pinecone-explorer

Length of output: 745


🏁 Script executed:

cat -n src/components/chat/ChatView.tsx | sed -n '1,50p'

Repository: stepandel/pinecone-explorer

Length of output: 1920


🏁 Script executed:

cat -n src/hooks/useChatStream.ts

Repository: stepandel/pinecone-explorer

Length of output: 10423


🏁 Script executed:

rg "id.*=" src/components/chat/ChatView.tsx src/hooks/useChatStream.ts | grep -i "user\|message"

Repository: stepandel/pinecone-explorer

Length of output: 331


🏁 Script executed:

rg "uuid|generateId|nanoid|v4" src/

Repository: stepandel/pinecone-explorer

Length of output: 53


🏁 Script executed:

sed -n '18,60p' src/components/chat/ChatView.tsx

Repository: stepandel/pinecone-explorer

Length of output: 1438


🏁 Script executed:

sed -n '55,70p' src/components/chat/ChatView.tsx

Repository: stepandel/pinecone-explorer

Length of output: 618


Improve message key stability in list rendering.

Line 210 uses idx as key. When the streaming placeholder is removed on errors, React can reuse DOM nodes. While MessageBubble is stateless, this still causes unnecessary re-renders.

The suggested fix improves this for assistant messages (which receive IDs after message_start), but user messages created at line 67-71 in useChatStream never receive IDs and would still fall back to role-${idx}. For a complete solution, assign stable IDs to user messages at creation time (e.g., using a timestamp or UUID) rather than relying on indices as fallback.

🔧 Suggested update
-            {messages.map((message, idx) => (
-              <MessageBubble key={idx} message={message} />
-            ))}
+            {messages.map((message, idx) => (
+              <MessageBubble key={message.id ?? `${message.role}-${idx}`} message={message} />
+            ))}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{messages.map((message, idx) => (
<MessageBubble key={idx} message={message} />
))}
{messages.map((message, idx) => (
<MessageBubble key={message.id ?? `${message.role}-${idx}`} message={message} />
))}
🤖 Prompt for AI Agents
In `@src/components/chat/ChatView.tsx` around lines 210 - 212, The message list
uses messages.map(... key={idx}) causing unstable keys; change the render to use
a stable message.id (MessageBubble key={message.id}) and ensure useChatStream
creates stable IDs for user messages when they are added (e.g., generate a UUID
or timestamp at creation instead of relying on index fallback), while keeping
assistant messages' existing id assignment on message_start; update any code
paths that currently set role-${idx} as fallback to always set a real id at
message creation.

Comment on lines +26 to +176
const [multimodal, setMultimodal] = useState(false)
const [isDragging, setIsDragging] = useState(false)
const [uploadError, setUploadError] = useState<string | null>(null)

const uploadMutation = useUploadFileMutation(profileId, assistantName)

// Check if selected file is a PDF (for multimodal checkbox)
const isPdf = useMemo(() => {
if (!selectedFile) return false
return selectedFile.name.toLowerCase().endsWith('.pdf')
}, [selectedFile])

// Reset state when dialog opens/closes
useEffect(() => {
if (!open) {
setSelectedFile(null)
setMetadataJson('')
setMetadataError(null)
setMultimodal(false)
setUploadError(null)
}
}, [open])

// Reset multimodal when file changes and it's not a PDF
useEffect(() => {
if (!isPdf) {
setMultimodal(false)
}
}, [isPdf])

// Validate metadata JSON
const validateMetadata = useCallback((json: string): Record<string, string | number> | null => {
if (!json.trim()) return null

try {
const parsed = JSON.parse(json)
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
setMetadataError('Metadata must be a JSON object')
return null
}
// Validate values are strings or numbers
for (const [key, value] of Object.entries(parsed)) {
if (typeof value !== 'string' && typeof value !== 'number') {
setMetadataError(`Value for "${key}" must be a string or number`)
return null
}
}
setMetadataError(null)
return parsed as Record<string, string | number>
} catch {
setMetadataError('Invalid JSON syntax')
return null
}
}, [])

// Handle metadata change
const handleMetadataChange = useCallback((value: string) => {
setMetadataJson(value)
if (value.trim()) {
validateMetadata(value)
} else {
setMetadataError(null)
}
}, [validateMetadata])

// Handle file picker button
const handleFilePicker = useCallback(async () => {
try {
const result = await window.electronAPI.dialog.showOpenDialog({
properties: ['openFile'],
filters: [
{ name: 'Documents', extensions: ['pdf', 'txt', 'md', 'json', 'csv', 'doc', 'docx'] },
{ name: 'All Files', extensions: ['*'] },
],
})

if (result.canceled || !result.filePaths.length) return

const filePath = result.filePaths[0]
// Get file info - we'll extract name from path
const fileName = filePath.split(/[/\\]/).pop() || 'Unknown'

setSelectedFile({
name: fileName,
path: filePath,
size: 0, // Size not available from dialog, could use fs in preload if needed
})
setUploadError(null)
} catch (err) {
console.error('Failed to open file picker:', err)
}
}, [])

// Handle drag events
const handleDragOver = useCallback((e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(true)
}, [])

const handleDragLeave = useCallback((e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)
}, [])

const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
setIsDragging(false)

const files = e.dataTransfer.files
if (files.length > 0) {
const file = files[0]
// Note: In Electron, we need to use the file path from the dropped file
// The path property is available in Electron's drag-and-drop
const filePath = (file as any).path

if (filePath) {
setSelectedFile({
name: file.name,
path: filePath,
size: file.size,
})
setUploadError(null)
}
}
}, [])

// Handle upload
const handleUpload = useCallback(async () => {
if (!selectedFile) return

// Validate metadata if provided
let metadata: Record<string, string | number> | undefined
if (metadataJson.trim()) {
const parsed = validateMetadata(metadataJson)
if (parsed === null && metadataJson.trim()) {
// Validation failed
return
}
metadata = parsed || undefined
}

setUploadError(null)

try {
await uploadMutation.mutateAsync({
filePath: selectedFile.path,
metadata,
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Multimodal toggle has no effect on uploads.
Line 26 tracks multimodal and Line 315 renders the checkbox, but Line 173 never sends it, so the user’s choice is ignored. Either remove/disable the control or extend the upload params + IPC to carry this flag.

🚩 Proposed change (requires IPC/types update)
-      await uploadMutation.mutateAsync({
-        filePath: selectedFile.path,
-        metadata,
-      })
+      await uploadMutation.mutateAsync({
+        filePath: selectedFile.path,
+        metadata,
+        multimodal,
+      })
@@
-  }, [selectedFile, metadataJson, validateMetadata, uploadMutation, onOpenChange])
+  }, [selectedFile, metadataJson, validateMetadata, uploadMutation, onOpenChange, multimodal])

Also applies to: 315-335

🤖 Prompt for AI Agents
In `@src/components/files/UploadFileDialog.tsx` around lines 26 - 176, The
multimodal checkbox state (multimodal) is never sent with uploads, so update
handleUpload to include multimodal in the payload passed to
uploadMutation.mutateAsync (currently called in handleUpload) and update the
IPC/upload handler and types that receive { filePath, metadata } so they accept
and propagate { multimodal: boolean } (or remove/disable the checkbox UI in the
render if you decide not to support multimodal). Ensure uploadMutation hook/type
signatures and the Electron IPC handler that processes the upload are updated to
accept and act on the new multimodal flag.

Comment on lines +234 to +255
const cancelStream = useCallback(() => {
if (currentStreamIdRef.current) {
window.electronAPI.assistant.chatStream.cancel(currentStreamIdRef.current).catch(console.error)
currentStreamIdRef.current = null
}
if (unsubscribeRef.current) {
unsubscribeRef.current()
unsubscribeRef.current = null
}
setIsStreaming(false)
// Mark the last message as no longer streaming
setMessages(prev => {
const updated = [...prev]
const lastIdx = updated.length - 1
if (updated[lastIdx]?.role === 'assistant' && updated[lastIdx].isStreaming) {
updated[lastIdx] = {
...updated[lastIdx],
isStreaming: false,
}
}
return updated
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Drop empty assistant placeholder when cancelling.
Line 248 marks the placeholder as finished but keeps empty content, leaving a blank assistant bubble if canceling immediately.

🔧 Suggested update
-      if (updated[lastIdx]?.role === 'assistant' && updated[lastIdx].isStreaming) {
-        updated[lastIdx] = {
-          ...updated[lastIdx],
-          isStreaming: false,
-        }
-      }
+      if (updated[lastIdx]?.role === 'assistant' && updated[lastIdx].isStreaming) {
+        if (!updated[lastIdx].content) {
+          updated.pop()
+        } else {
+          updated[lastIdx] = {
+            ...updated[lastIdx],
+            isStreaming: false,
+          }
+        }
+      }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const cancelStream = useCallback(() => {
if (currentStreamIdRef.current) {
window.electronAPI.assistant.chatStream.cancel(currentStreamIdRef.current).catch(console.error)
currentStreamIdRef.current = null
}
if (unsubscribeRef.current) {
unsubscribeRef.current()
unsubscribeRef.current = null
}
setIsStreaming(false)
// Mark the last message as no longer streaming
setMessages(prev => {
const updated = [...prev]
const lastIdx = updated.length - 1
if (updated[lastIdx]?.role === 'assistant' && updated[lastIdx].isStreaming) {
updated[lastIdx] = {
...updated[lastIdx],
isStreaming: false,
}
}
return updated
})
const cancelStream = useCallback(() => {
if (currentStreamIdRef.current) {
window.electronAPI.assistant.chatStream.cancel(currentStreamIdRef.current).catch(console.error)
currentStreamIdRef.current = null
}
if (unsubscribeRef.current) {
unsubscribeRef.current()
unsubscribeRef.current = null
}
setIsStreaming(false)
// Mark the last message as no longer streaming
setMessages(prev => {
const updated = [...prev]
const lastIdx = updated.length - 1
if (updated[lastIdx]?.role === 'assistant' && updated[lastIdx].isStreaming) {
if (!updated[lastIdx].content) {
updated.pop()
} else {
updated[lastIdx] = {
...updated[lastIdx],
isStreaming: false,
}
}
}
return updated
})
🤖 Prompt for AI Agents
In `@src/hooks/useChatStream.ts` around lines 234 - 255, In cancelStream
(useChatStream) after marking the last assistant message isStreaming = false,
also drop that last message entirely when its content is empty/blank to avoid
leaving an empty assistant bubble: locate cancelStream and the setMessages
updater that inspects updated[lastIdx] (uses currentStreamIdRef, unsubscribeRef,
setIsStreaming), and change the updater to remove the last element if its role
=== 'assistant' and its content.trim() is empty (instead of keeping it with
empty content).

@stepandel stepandel changed the base branch from master to feat/assistant-mode February 5, 2026 18:19
- Remove @ts-expect-error, add temperature/contextOptions to chat params
- Add isDestroyed check and error handling for streaming in main.ts
- Clear assistantService on disconnect in pinecone-service.ts
- Make assistant rows keyboard-accessible (use button element)
- Add IME composition check (isComposing) to prevent accidental submit
- Use stable message keys (message.id) instead of array index
- Reset conversation state when assistantName changes
- Remove empty assistant placeholder when canceling stream
- Pass multimodal flag through file upload flow
@stepandel stepandel merged commit 58f4475 into feat/assistant-mode Feb 5, 2026
@stepandel stepandel deleted the pine-45-chat-view branch February 5, 2026 19:15
stepandel added a commit that referenced this pull request Feb 6, 2026
* feat: Add ModeContext and ModeSwitcher UI (#37)

* feat: Add ModeContext and ModeSwitcher UI

- Add ModeContext for managing index/assistant mode state
- Add ModeSwitcher segmented control with Database/Bot icons
- Persist mode preference per connection profile
- Add keyboard shortcuts Cmd+1 (Index) and Cmd+2 (Assistant)

Closes PINE-36

* fix: address review comments on PINE-36

- Update .linear.toml workspace from chroma-explorer to pinecone-explorer
- Make setPreferredMode throw on missing profile instead of silently no-op
- Change ModeSwitcher from tablist/tab to radiogroup/radio for accessibility

* fix: add packages field to pnpm-workspace.yaml for CI

* fix: add aria-label for accessible name on ModeSwitcher buttons

---------

Co-authored-by: Atlas (Engineering Lead) <atlas@openclaw.ai>
Co-authored-by: Scout (Lead Tester) <scout@openclaw.ai>

* feat: Add AssistantService with CRUD operations (#38)

* feat: Add AssistantService with CRUD operations

- Add AssistantService class wrapping Pinecone SDK assistant methods
- Add IPC handlers for assistant:list/create/describe/update/delete
- Add preload bindings for window.electronAPI.assistant
- Add TypeScript types for AssistantModel, CreateAssistantParams, UpdateAssistantParams
- Wire up service to PineconeService with getAssistantService() method

Closes PINE-37

* fix: address review comments on PINE-37

- Normalize metadata null to undefined in mapAssistantModel
- Fix stale setMode closure in keyboard shortcut handler (moved setMode before useEffect, added to deps)

---------

Co-authored-by: Atlas (Engineering Lead) <atlas@openclaw.ai>
Co-authored-by: Scout (Lead Tester) <scout@openclaw.ai>

* feat: Add AssistantsPanel component (#39)

* feat: Add AssistantsPanel component

- Add useAssistantQueries.ts with React Query hooks for assistant CRUD
- Add AssistantSelectionContext.tsx for managing selected assistant
- Add AssistantsPanel.tsx mirroring IndexesPanel pattern
- Status indicators: Ready (green), Initializing (yellow), Failed (red)
- Loading, error, and empty states handled

Closes PINE-38

* 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

* feat: Add FilesPanel component

- Add useFilesQuery hook with dynamic polling (5s while processing)
- Add FileSelectionContext for tracking selected file
- Add FilesPanel component with status indicators and upload button
- Add dialog:showOpenDialog IPC handler for native file picker
- Wire up providers in ConnectionWindow

Closes PINE-41

* feat(files): add UploadFileDialog component with drag-and-drop support

PINE-42

- Add useUploadFileMutation hook to useAssistantQueries.ts
- Create UploadFileDialog component with:
  - Drag-and-drop file zone
  - File picker button using native dialog
  - Selected file preview with name and size
  - Optional metadata JSON editor with validation
  - Multimodal checkbox (enabled only for PDF files)
  - Upload progress indicator
  - Error handling with inline message
  - Dialog closes on successful upload
- Wire up FilesPanel upload button to open UploadFileDialog

* feat(PINE-43): Create FileDetailPanel component

- Create FileDetailPanel.tsx showing file metadata when selected
- Add ID field with copy-to-clipboard button
- Display status with color indicator (Available/Processing/Failed/Deleting)
- Show processing progress bar when file is processing
- Show error message section for failed files
- Display timestamps (created/updated) with formatted dates
- Show custom metadata as JSON
- Add Download button that opens signedUrl via shell.openExternal
- Add Delete button with confirmation dialog
- Add useDeleteFileMutation and useFileDetailQuery hooks to useAssistantQueries.ts
- Export FileDetailPanel from files/index.ts

Note: File size display not implemented as AssistantFile type from
Pinecone API does not include a size field.

* feat: Add chat IPC handlers with streaming support

- Add ChatMessage, ChatParams, ChatResponse, ChatStreamChunk types
- Add chat() and chatStream() methods to AssistantService
- Add IPC handlers for assistant:chat, assistant:chat:stream:start/cancel
- Add preload bindings with onChunk event listener for streaming
- Track active streams with AbortController for cancellation

Closes PINE-44

* feat(PINE-45): Create ChatView main component

- Create src/components/chat/ChatView.tsx
  - Layout with scrollable message list and fixed input area
  - Message display with user/assistant avatars and styling
  - Model dropdown selector (gpt-4o, claude-3-5-sonnet, gemini-2.0-flash)
  - Clear conversation button
  - Auto-scroll to bottom on new messages
  - Submit on Enter, Shift+Enter for newline
  - Send button disabled while streaming
  - Stop generation button during streaming
  - Citation display for assistant messages
  - Empty state with helpful instructions

- Create src/hooks/useChatStream.ts
  - Manages streaming state and message accumulation
  - Handles chunk events (message_start, content, citation, message_end, error)
  - Returns: messages, isStreaming, sendMessage, clearMessages, cancelStream
  - Properly cleans up subscriptions on unmount

- Update src/components/layout/MainContent.tsx
  - Import ModeContext and AssistantSelectionContext
  - Render ChatView when mode === 'assistant' and an assistant is selected
  - Show empty state message when in assistant mode without selection
  - Keep existing VectorsView for mode === 'index'

* feat(PINE-46): Create ChatMessage component with streaming

- Add ChatMessage component with markdown support via react-markdown
- User messages right-aligned with blue/primary background
- Assistant messages left-aligned with muted background
- Typing indicator (animated dots) when streaming with empty content
- Live cursor animation during content streaming
- Citation numbers as clickable superscripts inline with text
- Styled code blocks and inline code
- Update ChatView to use new ChatMessage component

* feat(chat): add CitationPopover component for interactive citations

- Create CitationPopover component using Radix Popover
- Shows file name and page numbers for each reference
- View File button navigates using FileSelectionContext
- Update ChatMessage to wrap citation superscripts with popover
- Popover closes on outside click (Radix default behavior)

Closes PINE-47

* feat: Wire up Assistant mode in MainContent

- Conditionally render AssistantsPanel/IndexesPanel based on mode
- Conditionally render FilesPanel/NamespacesPanel based on mode
- Conditionally render FileDetailPanel/VectorDetailPanel based on mode
- Preserve panel resize handles and widths
- Selection state isolated between modes via separate contexts

Closes PINE-48

* feat(PINE-49): Add context menus for Assistants and Files

- Add IPC handlers in main.ts for context-menu:show-assistant and context-menu:show-file
- Add preload bindings for showAssistantMenu, onAssistantAction, showFileMenu, onFileAction
- Update AssistantsPanel.tsx with:
  - Native context menu on right-click with Edit and Delete options
  - Delete confirmation dialog with name verification
  - Hook integration with useDeleteAssistantMutation
- Update FilesPanel.tsx with:
  - Native context menu on right-click with Download and Delete options
  - Delete confirmation dialog
  - Download via signedUrl fetch and shell.openExternal
  - Hook integration with useDeleteFileMutation and useFileDetailQuery
- Update TypeScript declarations in electron.d.ts

Acceptance criteria:
- Right-click assistant shows Edit/Delete menu ✓
- Right-click file shows Download/Delete menu ✓
- Delete actions show confirmation dialog ✓
- Menu actions trigger correct operations ✓

* feat(PINE-50): Add keyboard shortcuts for Assistant mode

- Update keyboard shortcuts constants with new assistant/chat categories:
  - INDEX_MODE (Cmd+1): Switch to Index mode
  - ASSISTANT_MODE (Cmd+2): Switch to Assistant mode
  - NEW_ASSISTANT (Cmd+Shift+N): Create new assistant
  - SEND_MESSAGE (Cmd+Enter): Send chat message
  - FOCUS_CHAT_INPUT (Cmd+K): Focus chat input
  - CLEAR_CONVERSATION (Cmd+Shift+Backspace): Clear conversation

- Update electron menu.ts:
  - Replace panel toggle items with Index Mode/Assistant Mode in View menu
  - Add Assistant menu with New Assistant, Chat submenu, Edit/Delete items
  - Chat submenu includes Send, Focus Input, Clear Conversation

- Add IPC bindings in preload.ts for new menu events:
  - Mode switching: onSwitchToIndexMode, onSwitchToAssistantMode
  - Assistant: onNewAssistant, onEditAssistant, onDeleteAssistant
  - Chat: onSendMessage, onFocusChatInput, onClearConversation

- Update ModeContext.tsx to listen for menu IPC events

- Update ChatView.tsx with keyboard shortcut handlers:
  - Cmd+K focuses chat input
  - Cmd+Shift+Backspace clears conversation
  - Cmd+Enter sends message (via menu)

- Update AssistantsPanel.tsx:
  - Cmd+Shift+N creates new assistant
  - Handle menu events for edit/delete assistant

- Update TypeScript types in electron.d.ts

- Clean up obsolete toggle panel handlers from useMenuHandlers.ts

---------

Co-authored-by: Atlas (Engineering Lead) <atlas@openclaw.ai>

* feat: Add file management IPC handlers (#40)

* 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

* fix: address review comments - pnpm workspace, linear config, AssistantStatus type

* 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

* 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 <stepandel@users.noreply.github.com>

---------

Co-authored-by: Atlas (Engineering Lead) <atlas@openclaw.ai>
Co-authored-by: Scout (Lead Tester) <scout@openclaw.ai>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Stepan Arsentjev <stepandel@users.noreply.github.com>

* feat: Add FilesPanel component (#41)

* feat: Add FilesPanel component

- Add useFilesQuery hook with dynamic polling (5s while processing)
- Add FileSelectionContext for tracking selected file
- Add FilesPanel component with status indicators and upload button
- Add dialog:showOpenDialog IPC handler for native file picker
- Wire up providers in ConnectionWindow

Closes PINE-41

* fix: address review comments - pnpm workspace, linear config, AssistantStatus type

* fix: clear assistantService on disconnect, remove files from later PRs

---------

Co-authored-by: Atlas (Engineering Lead) <atlas@openclaw.ai>
Co-authored-by: Scout (Lead Tester) <scout@openclaw.ai>

* fix build: add stub FileDetailPanel

* fix: Address PR review comments

- Add registerAccelerator: false to New Index menu item for consistency
- Add cancellation guard in ModeContext to prevent stale mode updates
- Handle 'InitializationFailed' status in AssistantsPanel
- Make citation superscripts keyboard accessible (button with aria-label)
- Validate URL protocol before calling openExternal for security
- Disable delete button when file status is 'Deleting'
- Clear messages when switching assistants in useChatStream

* feat: Add AssistantConfigView for create/edit (#51)

- Add AssistantConfigView component for creating/editing assistants
- Add DraftAssistantContext for managing draft assistant state
- Wire up AssistantConfigView in MainContent
- Add DraftAssistantProvider to ConnectionWindow

Closes PINE-39

Co-authored-by: Scout (Lead Tester) <scout@openclaw.ai>

* fix: address review comments on PINE-42 (#42)

Co-authored-by: Scout (Lead Tester) <scout@openclaw.ai>

* fix: address review comments on PINE-44 (#45)

- Remove @ts-expect-error, add temperature/contextOptions to chat params
- Add isDestroyed check and error handling for streaming in main.ts
- Clear assistantService on disconnect in pinecone-service.ts
- Make assistant rows keyboard-accessible (use button element)
- Add IME composition check (isComposing) to prevent accidental submit
- Use stable message keys (message.id) instead of array index
- Reset conversation state when assistantName changes
- Remove empty assistant placeholder when canceling stream
- Pass multimodal flag through file upload flow

Co-authored-by: Scout (Lead Tester) <scout@openclaw.ai>

* fix: address review comments on PINE-39 (#47)

- Fix .linear.toml workspace (chroma-explorer → pinecone-explorer)
- Add packages field to pnpm-workspace.yaml for CI
- Add InitializationFailed to AssistantStatus type
- Reset assistantService on connect/disconnect

Co-authored-by: Scout (Lead Tester) <scout@openclaw.ai>

* fix: address review comments on PINE-49 (#49)

- Pass temperature and contextOptions to chat() and chatStream() methods
- Add multimodal param to upload file flow (types, hook, service)
- Guard handleConfirmDelete against null currentProfile
- Use stable message.id key instead of array index in ChatView
- Fix download race condition by verifying fileDetail matches fileToDownload
- Keep file detail cache consistent during delete operations
- Handle early stream chunks before stream ID is set

Co-authored-by: Scout (Lead Tester) <scout@openclaw.ai>

* fix: Address additional PR review comments (round 2)

- Add assistant and chat menu event handlers to preload.ts
- Add types for new menu handlers in electron.d.ts
- Add pending guard to prevent duplicate delete calls in AssistantsPanel
- Reset initialization state when profile changes in ModeContext
- Extract shared Markdown components to reduce duplication in ChatMessage

* fix buid errors

* feat: Add E2E test suite for Assistant feature (#53)

* feat(e2e): add data-testid attributes to assistant components

Add comprehensive data-testid attributes for E2E testing:

Mode:
- mode-switcher, mode-index, mode-assistant

Assistants:
- assistants-panel, assistant-item, assistant-status
- new-assistant-button, assistant-config-view
- assistant-name-input, assistant-instructions-input
- assistant-save-button, assistant-cancel-button

Files:
- files-panel, files-empty-state, file-item
- upload-file-button, file-detail-panel
- file-detail-empty-state, file-download-button
- file-delete-button, upload-file-dialog
- browse-files-button, upload-submit-button

Chat:
- chat-view, chat-message-list, chat-input
- chat-send-button, chat-stop-button
- chat-clear-button, chat-model-selector
- chat-message-user, chat-message-assistant

Citations:
- citation-superscript, citation-popover
- citation-reference, citation-file-name
- citation-view-file-button

Part of PINE-51

* fix(e2e): Address PR review comments - 13 actionable items

Fixes from CodeRabbit review:

1. assistant-citations.spec.ts:
   - Citation test now explicitly skips with message when no citations
   - Multiple citations test uses test.skip() when citationCount <= 1

2. assistant-crud.spec.ts:
   - Added assertion for errorMessage visibility in validation test
   - Context menu edit test now explicitly skipped (native menu limitation)

3. assistant-file-detail.spec.ts:
   - File selection test explicitly skips when fileCount === 0
   - Delete test now asserts file disappears and count decreases

4. assistant-integration.spec.ts:
   - Network failure test now uses page.route() to simulate failures

5. assistant-mode.spec.ts:
   - Cross-platform keyboard shortcuts (Meta on macOS, Control on others)
   - Mode persistence test asserts switcher visibility (no silent skip)

6. assistant-upload.spec.ts:
   - API upload test: skip until real file fixture available
   - Metadata input test: skip until file dialog mocking available
   - Processing status test: explicit skip when no files
   - Progress test: skip until upload flow is wired

---------

Co-authored-by: Atlas (Engineering Lead) <atlas@openclaw.ai>

* fix(assistant): wire chatStream API through preload (#54)

PINE-55: Fix 'Cannot read properties of undefined (reading onChunk)'

Root cause: useChatStream.ts expected assistant.chatStream.* APIs but
electron/preload.ts never exposed them. IPC handlers existed in main.ts
but weren't wired through the preload bridge.

Changes:
- Add Chat types to electron/types.ts (ChatMessage, Citation,
  CitationReference, ChatUsage, ChatParams, ChatResponse, ChatStreamChunk)
- Add chatStream namespace to assistant API in preload.ts with
  start/cancel/onChunk methods
- Add corresponding TypeScript types to src/types/electron.d.ts

The chatStream API now properly exposes:
- start(profileId, assistantName, params) -> streamId
- cancel(streamId) -> void
- onChunk(callback) -> cleanup function

Co-authored-by: Atlas (Engineering Lead) <atlas@openclaw.ai>

* fix: Phase 6 feedback - dialog API and mode labels (PINE-53, PINE-54) (#55)

* fix(assistant): wire chatStream API through preload

PINE-55: Fix 'Cannot read properties of undefined (reading onChunk)'

Root cause: useChatStream.ts expected assistant.chatStream.* APIs but
electron/preload.ts never exposed them. IPC handlers existed in main.ts
but weren't wired through the preload bridge.

Changes:
- Add Chat types to electron/types.ts (ChatMessage, Citation,
  CitationReference, ChatUsage, ChatParams, ChatResponse, ChatStreamChunk)
- Add chatStream namespace to assistant API in preload.ts with
  start/cancel/onChunk methods
- Add corresponding TypeScript types to src/types/electron.d.ts

The chatStream API now properly exposes:
- start(profileId, assistantName, params) -> streamId
- cancel(streamId) -> void
- onChunk(callback) -> cleanup function

* fix: Phase 6 feedback - dialog API and mode labels

PINE-54: Wire dialog API through preload
- Add dialog.showOpenDialog to electron/preload.ts
- Add corresponding TypeScript types

PINE-53: Update mode labels to 'Database' instead of 'Index'
- Change 'Index Explorer' → 'Database Explorer' in tooltip
- Change 'Index' → 'Database' in button text

---------

Co-authored-by: Atlas (Engineering Lead) <atlas@openclaw.ai>

* fix uploader

* fix(assistant): use SDK chatStream method instead of chat with stream flag

The Pinecone SDK has a dedicated `assistant.chatStream()` method for streaming.
Passing `stream: true` to `assistant.chat()` is invalid and causes an API error.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(assistant): update supported models list to match Pinecone Assistant

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style(chat): compact macOS-native chat UI

Remove avatars and role labels for an Apple Messages-style layout.
Tighten spacing, use pill-shaped bubbles, circular send button, and
smaller typography to match the app's TopBar and sidebar density.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(files): add right-click context menu for file actions (#56)

PINE-56: Phase 6 feedback - Part 2

Add file context menu with Download and Delete actions:
- Add showFileMenu/onFileAction to electron/preload.ts
- IPC handler already existed in main.ts (lines 656-673)
- Add onContextMenu handler to FilesPanel file buttons
- Delete action shows confirmation dialog, then deletes file
- Download action opens signed URL in browser
- Add TypeScript types to electron.d.ts

Shortcuts in Settings were already implemented (keyboard-shortcuts.ts
has 'assistant' and 'chat' categories).

Co-authored-by: Atlas (Engineering Lead) <atlas@openclaw.ai>

* feat(analytics): add tracking for Assistant events (PINE-57) (#57)

* feat(files): add right-click context menu for file actions

PINE-56: Phase 6 feedback - Part 2

Add file context menu with Download and Delete actions:
- Add showFileMenu/onFileAction to electron/preload.ts
- IPC handler already existed in main.ts (lines 656-673)
- Add onContextMenu handler to FilesPanel file buttons
- Delete action shows confirmation dialog, then deletes file
- Download action opens signed URL in browser
- Add TypeScript types to electron.d.ts

Shortcuts in Settings were already implemented (keyboard-shortcuts.ts
has 'assistant' and 'chat' categories).

* feat(analytics): add tracking for Assistant events

PINE-57: Phase 7 - Add analytics

Add track() calls to Assistant IPC handlers:
- assistant_created (with region)
- assistant_deleted
- file_uploaded (with multimodal flag)
- file_deleted
- chat_message_sent (with model, messageCount)
- chat_stream_started (with model)

Follows existing analytics pattern from index operations.

---------

Co-authored-by: Atlas (Engineering Lead) <atlas@openclaw.ai>

* fix(files): poll for Deleting status to update UI after file removal

The files query only polled while files had 'Processing' status, so files
stuck in 'Deleting' status never refreshed until a manual page reload.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(assistant): production readiness fixes from audit

Address 10 issues found during production readiness audit:

1. Fix IPC wiring for menu-driven mode switching (onIndexMode/onAssistantMode)
2. Add URL protocol validation in shell:openExternal (block non-http(s))
3. Add confirmation dialog before file deletion in FileDetailPanel
4. Surface file operation errors to users in FilesPanel (upload/delete/download)
5. Resolve keyboard shortcut collision (NEW_ASSISTANT → CmdOrCtrl+Shift+A)
6. Add synchronous ref guard to prevent double-submit in useChatStream
7. Abort active chat streams when window is destroyed
8. Add vitest framework with 17 unit tests for AssistantService and matchesShortcut
9. Add file path validation (exists check) before upload in main process
10. Remove unused dependencies (sharp, dotenv, bufferutil, utf-8-validate)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* clean up

---------

Co-authored-by: Atlas (Engineering Lead) <atlas@openclaw.ai>
Co-authored-by: Scout (Lead Tester) <scout@openclaw.ai>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Stepan Arsentjev <stepandel@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant