Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion src/main/ipc/handlers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ipcMain, BrowserWindow, webContents, dialog, shell } from 'electron';
import { ipcMain, BrowserWindow, webContents, dialog, shell, app } from 'electron';
import path from 'path';
import fs from 'fs';
import { databaseService, HistoryEntry, Bookmark, Tab } from '../services/database';
Expand Down Expand Up @@ -1187,4 +1187,36 @@ When Planning Mode is enabled, you have access to these tools:
throw error;
}
});

// User agreement handlers
ipcMain.handle('agreement:check', async () => {
try {
const accepted = databaseService.getSetting('user-agreement-accepted');
return accepted === 'true';
} catch (error: any) {
console.error('agreement:check error:', error.message);
throw error;
}
});

ipcMain.handle('agreement:accept', async () => {
try {
databaseService.setSetting('user-agreement-accepted', 'true');
return { success: true };
} catch (error: any) {
console.error('agreement:accept error:', error.message);
throw error;
}
});

// App control handlers
ipcMain.handle('app:quit', async () => {
try {
app.quit();
return { success: true };
} catch (error: any) {
console.error('app:quit error:', error.message);
throw error;
}
});
}
9 changes: 9 additions & 0 deletions src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ const ALLOWED_INVOKE_CHANNELS = [
'ollama:generate',
'ollama:chat',
'ollama:getStatus',
'ollama:restart',
'ollama:forceKill',
'ollama:stop',
'ollama:cancelPull',
'ollama:cancelChat',
'tabs:wasCrash',
'tool:search_history',
'tool:get_bookmarks',
'tool:analyze_page_content',
Expand Down Expand Up @@ -64,6 +70,9 @@ const ALLOWED_INVOKE_CHANNELS = [
'download:chooseSaveLocation',
'download:openManager',
'download:saveImage',
'agreement:check',
'agreement:accept',
'app:quit',
];

const ALLOWED_LISTEN_CHANNELS = [
Expand Down
8 changes: 1 addition & 7 deletions src/main/services/database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -616,13 +616,7 @@ class DatabaseService {
updateDownload(id: number, updates: Partial<Download>): void {
if (!this.db) throw new Error('Database not initialized');

const allowedFields = [
'received_bytes',
'state',
'end_time',
'error',
'total_bytes',
] as const;
const allowedFields = ['received_bytes', 'state', 'end_time', 'error', 'total_bytes'] as const;

const fields: string[] = [];
const values: any[] = [];
Expand Down
59 changes: 37 additions & 22 deletions src/main/services/ollama.ts
Original file line number Diff line number Diff line change
Expand Up @@ -663,20 +663,24 @@ export class OllamaService {
private async findOllamaPid(): Promise<number | null> {
return new Promise<number | null>((resolve) => {
if (process.platform === 'win32') {
exec('tasklist /FI "IMAGENAME eq ollama.exe" /FO CSV /NH', { timeout: 3000 }, (error, stdout) => {
if (error || !stdout || stdout.includes('INFO: No tasks are running')) {
resolve(null);
return;
}
exec(
'tasklist /FI "IMAGENAME eq ollama.exe" /FO CSV /NH',
{ timeout: 3000 },
(error, stdout) => {
if (error || !stdout || stdout.includes('INFO: No tasks are running')) {
resolve(null);
return;
}

// Parse the first ollama.exe process found
const match = stdout.match(/"ollama\.exe","(\d+)"/);
if (match && match[1]) {
resolve(parseInt(match[1], 10));
} else {
resolve(null);
// Parse the first ollama.exe process found
const match = stdout.match(/"ollama\.exe","(\d+)"/);
if (match && match[1]) {
resolve(parseInt(match[1], 10));
} else {
resolve(null);
}
}
});
);
} else {
exec('pgrep -f "ollama"', { timeout: 3000 }, (error, stdout) => {
if (error || !stdout.trim()) {
Expand Down Expand Up @@ -734,7 +738,10 @@ export class OllamaService {

try {
// Parse CSV output (skip header and node line)
const lines = stdout.trim().split('\n').filter(line => line.trim());
const lines = stdout
.trim()
.split('\n')
.filter((line) => line.trim());
if (lines.length < 2) {
resolve(null);
return;
Expand All @@ -747,9 +754,10 @@ export class OllamaService {
const workingSetSize = parseInt(parts[3], 10); // Memory in bytes

// Calculate uptime (0 if we don't have start time)
const uptime = this.processStartTime > 0
? Math.floor((Date.now() - this.processStartTime) / 1000)
: 0;
const uptime =
this.processStartTime > 0
? Math.floor((Date.now() - this.processStartTime) / 1000)
: 0;

resolve({
pid,
Expand Down Expand Up @@ -786,9 +794,10 @@ export class OllamaService {
const rss = parseInt(output[0], 10) * 1024; // Convert KB to bytes
const cpu = parseFloat(output[1]);

const uptime = this.processStartTime > 0
? Math.floor((Date.now() - this.processStartTime) / 1000)
: 0;
const uptime =
this.processStartTime > 0
? Math.floor((Date.now() - this.processStartTime) / 1000)
: 0;

resolve({
pid,
Expand Down Expand Up @@ -844,7 +853,7 @@ export class OllamaService {

// Wait for stop to complete (isStopping flag will be reset by stop())
// Add extra delay to ensure cleanup is complete
await new Promise(resolve => setTimeout(resolve, 1500));
await new Promise((resolve) => setTimeout(resolve, 1500));

// Verify process is fully stopped before starting
if (this.process) {
Expand Down Expand Up @@ -1330,7 +1339,9 @@ export class OllamaService {

// Log periodically to show stream is alive
if (chunkCount % 100 === 0) {
console.log(`[Ollama] Received ${chunkCount} chunks, ${tokenCount} tokens, buffer size: ${buffer.length}`);
console.log(
`[Ollama] Received ${chunkCount} chunks, ${tokenCount} tokens, buffer size: ${buffer.length}`
);
// Debug: Show buffer content if no tokens are being extracted
if (tokenCount === 0 && chunkCount >= 100) {
console.log('[Ollama] DEBUG - Buffer sample:', buffer.substring(0, 200));
Expand All @@ -1344,7 +1355,11 @@ export class OllamaService {
processedSomething = false;

// Strategy 1: Try to parse buffer as complete JSON (for small chunks)
if (buffer.length < 500 && buffer.trim().startsWith('{') && buffer.trim().endsWith('}')) {
if (
buffer.length < 500 &&
buffer.trim().startsWith('{') &&
buffer.trim().endsWith('}')
) {
try {
const data = JSON.parse(buffer);
processedSomething = true;
Expand Down
63 changes: 63 additions & 0 deletions src/renderer/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
import React, { useEffect, useState } from 'react';
import { BrowserLayout } from './components/Browser/BrowserLayout';
import { DownloadManager } from './components/Downloads/DownloadManager';
import { UserAgreement } from './components/UserAgreement/UserAgreement';

function App() {
const [route, setRoute] = useState(window.location.hash);
const [agreementAccepted, setAgreementAccepted] = useState<boolean | null>(null);
const [showAgreement, setShowAgreement] = useState(false);

// Check if user has accepted the agreement
useEffect(() => {
const checkAgreement = async () => {
try {
const accepted = await window.electron.invoke('agreement:check');
setAgreementAccepted(accepted);
setShowAgreement(!accepted);
} catch (error) {
console.error('Failed to check user agreement:', error);
// On error, assume agreement not accepted for safety
setAgreementAccepted(false);
setShowAgreement(true);
}
};

checkAgreement();
}, []);

useEffect(() => {
const handleHashChange = () => {
Expand All @@ -14,6 +35,48 @@ function App() {
return () => window.removeEventListener('hashchange', handleHashChange);
}, []);

const handleAcceptAgreement = async () => {
try {
await window.electron.invoke('agreement:accept');
setAgreementAccepted(true);
setShowAgreement(false);
} catch (error) {
console.error('Failed to save agreement acceptance:', error);
}
};

const handleDeclineAgreement = async () => {
try {
// Close the application if user declines
await window.electron.invoke('app:quit');
} catch (error) {
console.error('Failed to quit application:', error);
}
};

// Show loading state while checking agreement
if (agreementAccepted === null) {
return (
<div className="flex items-center justify-center h-screen bg-background">
<div className="text-center space-y-3">
<div className="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin mx-auto" />
<p className="text-muted-foreground">Loading...</p>
</div>
</div>
);
}

// Show agreement modal if not accepted
if (!agreementAccepted) {
return (
<UserAgreement
isOpen={showAgreement}
onAccept={handleAcceptAgreement}
onDecline={handleDeclineAgreement}
/>
);
}

// Route to download manager if hash is #downloads
if (route === '#downloads' || route === '#/downloads') {
return <DownloadManager />;
Expand Down
Loading