diff --git a/bin/cli.js b/bin/cli.js index ce819a6f..92e02ef7 100644 --- a/bin/cli.js +++ b/bin/cli.js @@ -43,6 +43,7 @@ const child = spawn(electron, [mainPath, ...args], { }) // Forward signals to child process +// eslint-disable-next-line @typescript-eslint/explicit-function-return-type function forwardSignal(signal) { if (child.pid) { process.kill(child.pid, signal) diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 033ffded..c8b17f1e 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -7,10 +7,10 @@ import tailwindcss from '@tailwindcss/vite' const pkg = JSON.parse(readFileSync('./package.json', 'utf-8')) // Plugin to copy resources to output -function copyResources() { +function copyResources(): { name: string; closeBundle: () => void } { return { name: 'copy-resources', - closeBundle() { + closeBundle(): void { const srcIcon = resolve('resources/icon.png') const destDir = resolve('out/resources') const destIcon = resolve('out/resources/icon.png') diff --git a/src/main/ipc/threads.ts b/src/main/ipc/threads.ts index d744da41..6d9fb0d2 100644 --- a/src/main/ipc/threads.ts +++ b/src/main/ipc/threads.ts @@ -12,7 +12,7 @@ import { deleteThreadCheckpoint } from '../storage' import { generateTitle } from '../services/title-generator' import type { Thread } from '../types' -export function registerThreadHandlers(ipcMain: IpcMain) { +export function registerThreadHandlers(ipcMain: IpcMain): void { // List all threads ipcMain.handle('threads:list', async () => { const threads = getAllThreads() @@ -90,7 +90,7 @@ export function registerThreadHandlers(ipcMain: IpcMain) { // Delete a thread ipcMain.handle('threads:delete', async (_event, threadId: string) => { console.log('[Threads] Deleting thread:', threadId) - + // Delete from our metadata store dbDeleteThread(threadId) console.log('[Threads] Deleted from metadata store') diff --git a/src/main/storage.ts b/src/main/storage.ts index d3f37f7a..50278140 100644 --- a/src/main/storage.ts +++ b/src/main/storage.ts @@ -75,7 +75,7 @@ function parseEnvFile(): Record { function writeEnvFile(env: Record): void { getOpenworkDir() // ensure dir exists const lines = Object.entries(env) - .filter(([_, v]) => v) + .filter((entry) => entry[1]) .map(([k, v]) => `${k}=${v}`) writeFileSync(getEnvFilePath(), lines.join('\n') + '\n') } diff --git a/src/renderer/src/components/chat/ApiKeyDialog.tsx b/src/renderer/src/components/chat/ApiKeyDialog.tsx index d89ffed0..4faa981c 100644 --- a/src/renderer/src/components/chat/ApiKeyDialog.tsx +++ b/src/renderer/src/components/chat/ApiKeyDialog.tsx @@ -24,13 +24,13 @@ const PROVIDER_INFO: Record = { google: { placeholder: 'AIza...', envVar: 'GOOGLE_API_KEY' } } -export function ApiKeyDialog({ open, onOpenChange, provider }: ApiKeyDialogProps) { +export function ApiKeyDialog({ open, onOpenChange, provider }: ApiKeyDialogProps): React.JSX.Element | null { const [apiKey, setApiKey] = useState('') const [showKey, setShowKey] = useState(false) const [saving, setSaving] = useState(false) const [deleting, setDeleting] = useState(false) const [hasExistingKey, setHasExistingKey] = useState(false) - + const { setApiKey: saveApiKey, deleteApiKey } = useAppStore() // Check if there's an existing key when dialog opens @@ -46,10 +46,10 @@ export function ApiKeyDialog({ open, onOpenChange, provider }: ApiKeyDialogProps const info = PROVIDER_INFO[provider.id] || { placeholder: '...', envVar: '' } - async function handleSave() { + async function handleSave(): Promise { if (!apiKey.trim()) return if (!provider) return - + console.log('[ApiKeyDialog] Saving API key for provider:', provider.id) setSaving(true) try { @@ -63,7 +63,7 @@ export function ApiKeyDialog({ open, onOpenChange, provider }: ApiKeyDialogProps } } - async function handleDelete() { + async function handleDelete(): Promise { if (!provider) return setDeleting(true) try { @@ -84,7 +84,7 @@ export function ApiKeyDialog({ open, onOpenChange, provider }: ApiKeyDialogProps {hasExistingKey ? `Update ${provider.name} API Key` : `Add ${provider.name} API Key`} - {hasExistingKey + {hasExistingKey ? 'Enter a new API key to replace the existing one, or remove it.' : `Enter your ${provider.name} API key to use their models.` } diff --git a/src/renderer/src/components/chat/ChatContainer.tsx b/src/renderer/src/components/chat/ChatContainer.tsx index 1b9b293e..ed31ce42 100644 --- a/src/renderer/src/components/chat/ChatContainer.tsx +++ b/src/renderer/src/components/chat/ChatContainer.tsx @@ -7,7 +7,8 @@ import { useCurrentThread, useThreadStream } from '@/lib/thread-context' import { MessageBubble } from './MessageBubble' import { ModelSwitcher } from './ModelSwitcher' import { Folder } from 'lucide-react' -import { WorkspacePicker, selectWorkspaceFolder } from './WorkspacePicker' +import { WorkspacePicker } from './WorkspacePicker' +import { selectWorkspaceFolder } from '@/lib/workspace-utils' import { ChatTodos } from './ChatTodos' import { ContextUsageIndicator } from './ContextUsageIndicator' import type { Message } from '@/types' @@ -61,7 +62,7 @@ export function ChatContainer({ threadId }: ChatContainerProps): React.JSX.Eleme const isLoading = streamData.isLoading const handleApprovalDecision = useCallback( - async (decision: 'approve' | 'reject' | 'edit') => { + async (decision: 'approve' | 'reject' | 'edit'): Promise => { if (!pendingApproval || !stream) return setPendingApproval(null) @@ -163,14 +164,14 @@ export function ChatContainer({ threadId }: ChatContainerProps): React.JSX.Eleme }, [displayMessages]) // Get the actual scrollable viewport element from Radix ScrollArea - const getViewport = useCallback(() => { + const getViewport = useCallback((): HTMLDivElement | null => { return scrollRef.current?.querySelector( '[data-radix-scroll-area-viewport]' ) as HTMLDivElement | null }, []) // Track scroll position to determine if user is at bottom - const handleScroll = useCallback(() => { + const handleScroll = useCallback((): void => { const viewport = getViewport() if (!viewport) return @@ -286,7 +287,7 @@ export function ChatContainer({ threadId }: ChatContainerProps): React.JSX.Eleme } const handleSelectWorkspaceFromEmptyState = async (): Promise => { - await selectWorkspaceFolder(threadId, setWorkspacePath, setWorkspaceFiles, () => {}, undefined) + await selectWorkspaceFolder(threadId, setWorkspacePath, setWorkspaceFiles, () => { }, undefined) } return ( @@ -322,9 +323,9 @@ export function ChatContainer({ threadId }: ChatContainerProps): React.JSX.Eleme )} {displayMessages.map((message) => ( - = { 'o3': 200_000, 'o3-mini': 200_000, // Google models + 'gemini-3-pro-preview': 2_000_000, + 'gemini-3-flash-preview': 1_000_000, + 'gemini-2.5-pro': 2_000_000, + 'gemini-2.5-flash': 1_000_000, + 'gemini-2.5-flash-lite': 1_000_000, 'gemini-2.0-flash': 1_000_000, 'gemini-1.5-pro': 2_000_000, 'gemini-1.5-flash': 1_000_000 @@ -131,8 +136,8 @@ export function ContextUsageIndicator({ - @@ -148,7 +153,7 @@ export function ContextUsageIndicator({ {/* Progress bar */}
-
@@ -164,7 +169,7 @@ export function ContextUsageIndicator({
Token Breakdown
- +
{/* Input tokens */}
@@ -201,7 +206,7 @@ export function ContextUsageIndicator({
Cache
- +
{tokenUsage.cacheReadTokens !== undefined && tokenUsage.cacheReadTokens > 0 && (
diff --git a/src/renderer/src/components/chat/MessageBubble.tsx b/src/renderer/src/components/chat/MessageBubble.tsx index e4dbf79c..9a71ec81 100644 --- a/src/renderer/src/components/chat/MessageBubble.tsx +++ b/src/renderer/src/components/chat/MessageBubble.tsx @@ -17,7 +17,7 @@ interface MessageBubbleProps { onApprovalDecision?: (decision: 'approve' | 'reject' | 'edit') => void } -export function MessageBubble({ message, isStreaming, toolResults, pendingApproval, onApprovalDecision }: MessageBubbleProps) { +export function MessageBubble({ message, isStreaming, toolResults, pendingApproval, onApprovalDecision }: MessageBubbleProps): React.JSX.Element | null { const isUser = message.role === 'user' const isTool = message.role === 'tool' @@ -26,17 +26,17 @@ export function MessageBubble({ message, isStreaming, toolResults, pendingApprov return null } - const getIcon = () => { + const getIcon = (): React.JSX.Element => { if (isUser) return return } - const getLabel = () => { + const getLabel = (): string => { if (isUser) return 'YOU' return 'AGENT' } - const renderContent = () => { + const renderContent = (): React.ReactNode => { if (typeof message.content === 'string') { // Empty content if (!message.content.trim()) { diff --git a/src/renderer/src/components/chat/ModelSwitcher.tsx b/src/renderer/src/components/chat/ModelSwitcher.tsx index 26eb180d..9f6e5bdf 100644 --- a/src/renderer/src/components/chat/ModelSwitcher.tsx +++ b/src/renderer/src/components/chat/ModelSwitcher.tsx @@ -9,26 +9,26 @@ import { ApiKeyDialog } from './ApiKeyDialog' import type { Provider, ProviderId } from '@/types' // Provider icons as simple SVG components -function AnthropicIcon({ className }: { className?: string }) { +function AnthropicIcon({ className }: { className?: string }): React.JSX.Element { return ( - + ) } -function OpenAIIcon({ className }: { className?: string }) { +function OpenAIIcon({ className }: { className?: string }): React.JSX.Element { return ( - + ) } -function GoogleIcon({ className }: { className?: string }) { +function GoogleIcon({ className }: { className?: string }): React.JSX.Element { return ( - + ) } @@ -51,12 +51,12 @@ interface ModelSwitcherProps { threadId: string } -export function ModelSwitcher({ threadId }: ModelSwitcherProps) { +export function ModelSwitcher({ threadId }: ModelSwitcherProps): React.JSX.Element { const [open, setOpen] = useState(false) const [selectedProviderId, setSelectedProviderId] = useState(null) const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false) const [apiKeyProvider, setApiKeyProvider] = useState(null) - + const { models, providers, loadModels, loadProviders } = useAppStore() const { currentModel, setCurrentModel } = useCurrentThread(threadId) @@ -69,41 +69,32 @@ export function ModelSwitcher({ threadId }: ModelSwitcherProps) { // Use fallback providers if none loaded const displayProviders = providers.length > 0 ? providers : FALLBACK_PROVIDERS - // Set initial selected provider based on current model - useEffect(() => { - if (!selectedProviderId && currentModel) { - const model = models.find(m => m.id === currentModel) - if (model) { - setSelectedProviderId(model.provider) - } - } - // Default to first provider if none selected - if (!selectedProviderId && displayProviders.length > 0) { - setSelectedProviderId(displayProviders[0].id) - } - }, [currentModel, models, selectedProviderId, displayProviders]) + // Determine effective provider ID (manual selection > current model > default) + const effectiveProviderId = selectedProviderId || + (currentModel ? models.find(m => m.id === currentModel)?.provider : null) || + (displayProviders.length > 0 ? displayProviders[0].id : null) const selectedModel = models.find(m => m.id === currentModel) - const filteredModels = selectedProviderId - ? models.filter(m => m.provider === selectedProviderId) + const filteredModels = effectiveProviderId + ? models.filter(m => m.provider === effectiveProviderId) : [] - const selectedProvider = displayProviders.find(p => p.id === selectedProviderId) + const selectedProvider = displayProviders.find(p => p.id === effectiveProviderId) - function handleProviderClick(provider: Provider) { + function handleProviderClick(provider: Provider): void { setSelectedProviderId(provider.id) } - function handleModelSelect(modelId: string) { + function handleModelSelect(modelId: string): void { setCurrentModel(modelId) setOpen(false) } - function handleConfigureApiKey(provider: Provider) { + function handleConfigureApiKey(provider: Provider): void { setApiKeyProvider(provider) setApiKeyDialogOpen(true) } - function handleApiKeyDialogClose(isOpen: boolean) { + function handleApiKeyDialogClose(isOpen: boolean): void { setApiKeyDialogOpen(isOpen) if (!isOpen) { // Refresh providers after dialog closes @@ -132,8 +123,8 @@ export function ModelSwitcher({ threadId }: ModelSwitcherProps) { - @@ -152,7 +143,7 @@ export function ModelSwitcher({ threadId }: ModelSwitcherProps) { onClick={() => handleProviderClick(provider)} className={cn( "w-full flex items-center gap-1.5 px-2 py-1 rounded-sm text-xs transition-colors text-left", - selectedProviderId === provider.id + effectiveProviderId === provider.id ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground hover:bg-muted/50" )} @@ -173,7 +164,7 @@ export function ModelSwitcher({ threadId }: ModelSwitcherProps) {
Model
- + {selectedProvider && !selectedProvider.hasApiKey ? ( // No API key configured
@@ -209,14 +200,14 @@ export function ModelSwitcher({ threadId }: ModelSwitcherProps) { )} ))} - + {filteredModels.length === 0 && (

No models available

)}
- + {/* Configure API key link for providers that have a key */} {selectedProvider?.hasApiKey && ( - - + {zoom}% - + - + - +