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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18
node-version: 22
cache: 'npm'

- name: Set up Python (for native modules)
Expand Down
14 changes: 14 additions & 0 deletions build/entitlements.mac.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
</dict>
</plist>
6 changes: 6 additions & 0 deletions electron-builder.json5
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@
mac: {
target: ['dmg'],
icon: 'public/app_logo.png',
entitlements: 'build/entitlements.mac.plist',
entitlementsInherit: 'build/entitlements.mac.plist',
extendInfo: {
NSMicrophoneUsageDescription:
'Alice uses the microphone for voice activity detection and speech recognition.',
},
artifactName: 'Alice-AI-App-Mac-${version}-Installer.${ext}',
},
win: {
Expand Down
15 changes: 14 additions & 1 deletion electron/main/settingsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,20 @@ export interface AppSettings {
SUMMARIZATION_MODEL?: string
SUMMARIZATION_SYSTEM_PROMPT?: string
ttsProvider?: 'openai' | 'local'
ttsVoice?: 'alloy' | 'echo' | 'fable' | 'nova' | 'onyx' | 'shimmer'
ttsVoice?:
| 'alloy'
| 'ash'
| 'ballad'
| 'coral'
| 'echo'
| 'fable'
| 'nova'
| 'onyx'
| 'sage'
| 'shimmer'
| 'verse'
| 'marin'
| 'cedar'
localTtsVoice?: string
embeddingProvider?: 'openai' | 'local'
ragEnabled?: boolean
Expand Down
27 changes: 21 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"author": "pmbstyle",
"license": "MIT",
"private": true,
"engines": {
"node": ">=22.0.0"
},
"keywords": [
"electron",
"rollup",
Expand Down Expand Up @@ -46,7 +49,7 @@
"dotenv": "^17.4.2",
"electron": "43.2.0",
"electron-builder": "26.15.3",
"openai": "^6.33.0",
"openai": "^7.4.0",
"pinia": "^3.0.3",
"postcss": "^8.5.25",
"prettier": "^3.9.6",
Expand Down
2 changes: 1 addition & 1 deletion src/components/settings/AssistantSettingsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@
</p>
<p class="text-xs text-gray-400 mt-1">
Model used for generating conversation summaries (e.g.,
gpt-4.1-nano).
gpt-5.6-luna).
</p>
</div>

Expand Down
7 changes: 7 additions & 0 deletions src/components/settings/CoreSettingsTab.vue
Original file line number Diff line number Diff line change
Expand Up @@ -477,11 +477,18 @@
class="select select-bordered w-full focus:select-primary"
>
<option value="alloy">Alloy</option>
<option value="ash">Ash</option>
<option value="ballad">Ballad</option>
<option value="coral">Coral</option>
<option value="echo">Echo</option>
<option value="fable">Fable</option>
<option value="nova">Nova</option>
<option value="onyx">Onyx</option>
<option value="sage">Sage</option>
<option value="shimmer">Shimmer</option>
<option value="verse">Verse</option>
<option value="marin">Marin (Recommended)</option>
<option value="cedar">Cedar (Recommended)</option>
</select>
</div>
<div v-if="currentSettings.ttsProvider === 'google'">
Expand Down
2 changes: 1 addition & 1 deletion src/components/wizard/OnboardingWizard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ import { listZAIModelsForConfig } from '../../services/llmProviders/zai'
const step = ref(1)
const settingsStore = useSettingsStore()
const scrollContainer = ref<HTMLElement>()
const OPENAI_SUMMARIZATION_MODEL = 'gpt-4.1-nano'
const OPENAI_SUMMARIZATION_MODEL = 'gpt-5.6-luna'
const DEFAULT_MAIN_WINDOW_SIZE = {
width: 500,
height: 500,
Expand Down
15 changes: 15 additions & 0 deletions src/modules/conversation/__tests__/chatOrchestrator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,21 @@ describe('chatOrchestrator', () => {
expect(dependencies.setAudioState).toHaveBeenCalledWith('IDLE')
})

it.each(['AbortError', 'APIUserAbortError'])(
'silently handles %s from the OpenAI stream request',
async errorName => {
const error = Object.assign(new Error('Request was aborted.'), {
name: errorName,
})
const { dependencies, orchestrator } = setup({ throwError: error })

await orchestrator.runChat()

expect(dependencies.logError).not.toHaveBeenCalled()
expect(dependencies.handleStreamError).not.toHaveBeenCalled()
}
)

it('skips work when store is not initialized', async () => {
const ctx = setup()
ctx.dependencies.isInitialized.mockReturnValue(false)
Expand Down
33 changes: 18 additions & 15 deletions src/modules/conversation/__tests__/speechQueue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,24 @@ describe('createSpeechQueueManager', () => {
expect(deps.setAudioState).not.toHaveBeenCalled()
})

it('silently handles AbortError from TTS stream', async () => {
const abortError = Object.assign(new Error('cancel'), {
name: 'AbortError',
})
const deps = buildDependencies({
ttsStream: vi.fn(async () => {
throw abortError
}),
})
const manager = createSpeechQueueManager(deps)

await manager.enqueueSpeech('Hello')

expect(deps.logError).not.toHaveBeenCalled()
})
it.each(['AbortError', 'APIUserAbortError'])(
'silently handles %s from TTS stream',
async errorName => {
const abortError = Object.assign(new Error('cancel'), {
name: errorName,
})
const deps = buildDependencies({
ttsStream: vi.fn(async () => {
throw abortError
}),
})
const manager = createSpeechQueueManager(deps)

await manager.enqueueSpeech('Hello')

expect(deps.logError).not.toHaveBeenCalled()
}
)

it('does not enqueue a response that resolves after cancellation', async () => {
let resolveTts: ((response: Response) => void) | undefined
Expand Down
34 changes: 18 additions & 16 deletions src/modules/conversation/__tests__/streamHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,22 +170,24 @@ describe('createStreamHandler', () => {
expect(spies.handleStreamError).toHaveBeenCalledWith(error)
})

it('returns early without error when the stream is aborted', async () => {
const { deps, spies } = createDependencies()
const handler = createStreamHandler(deps)

const abortingStream: AsyncIterable<any> = {
async *[Symbol.asyncIterator]() {
const abortError = new Error('aborted')
abortError.name = 'AbortError'
throw abortError
},
}
it.each(['AbortError', 'APIUserAbortError'])(
'returns early without error for %s',
async errorName => {
const { deps, spies } = createDependencies()
const handler = createStreamHandler(deps)

const abortingStream: AsyncIterable<any> = {
async *[Symbol.asyncIterator]() {
const abortError = new Error('aborted')
abortError.name = errorName
throw abortError
},
}

const result = await handler.process({ stream: abortingStream })
const result = await handler.process({ stream: abortingStream })

expect(result.streamEndedNormally).toBe(false)
expect(spies.handleStreamError).not.toHaveBeenCalled()
})
expect(result.streamEndedNormally).toBe(false)
expect(spies.handleStreamError).not.toHaveBeenCalled()
}
)
})

3 changes: 2 additions & 1 deletion src/modules/conversation/chatOrchestrator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type OpenAI from 'openai'
import type { ChatMessage } from '../../types/chat'
import type { RagSearchResult } from '../../types/rag'
import { isExpectedAbortError } from '../../utils/isAbortError'

export interface ChatDependencies {
isInitialized(): boolean
Expand Down Expand Up @@ -453,7 +454,7 @@ export function createChatOrchestrator(
)
await dependencies.processStream(streamResult, placeholderTempId, false)
} catch (error: any) {
if (error?.name === 'AbortError') return
if (abortController.signal.aborted || isExpectedAbortError(error)) return

dependencies.logError('Error starting OpenAI response stream:', error)

Expand Down
4 changes: 2 additions & 2 deletions src/modules/conversation/reminderHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ChatMessage } from '../../types/chat'
import { isExpectedAbortError } from '../../utils/isAbortError'

export interface ReminderData {
message: string
Expand Down Expand Up @@ -46,7 +47,7 @@ export function createReminderHandler(
await dependencies.enqueueSpeech(data.message)
}
} catch (error: any) {
if (error?.name === 'AbortError') {
if (isExpectedAbortError(error)) {
return
}
dependencies.logError(
Expand All @@ -64,4 +65,3 @@ export function createReminderHandler(
},
}
}

3 changes: 2 additions & 1 deletion src/modules/conversation/speechQueue.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { AudioState } from '../../stores/generalStore'
import { isExpectedAbortError } from '../../utils/isAbortError'

export interface SpeechQueueDependencies {
createAbortController(): AbortController
Expand Down Expand Up @@ -42,7 +43,7 @@ export function createSpeechQueueManager(
dependencies.setAudioState('SPEAKING')
}
} catch (error: any) {
if (error?.name === 'AbortError') {
if (abortController.signal.aborted || isExpectedAbortError(error)) {
return
}
dependencies.logError('TTS stream creation failed:', error)
Expand Down
3 changes: 2 additions & 1 deletion src/modules/conversation/streamHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
StreamProcessingOptions,
StreamProcessingResult,
} from './types'
import { isExpectedAbortError } from '../../utils/isAbortError'

const SENTENCE_END_REGEX = /[.!?]\s*$/

Expand Down Expand Up @@ -139,7 +140,7 @@ export function createStreamHandler(

await flushSentence()
} catch (error: any) {
if (error?.name === 'AbortError') {
if (isExpectedAbortError(error)) {
return { streamEndedNormally: false }
}
streamEndedNormally = false
Expand Down
13 changes: 3 additions & 10 deletions src/modules/conversation/toolCallHandler.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import type OpenAI from 'openai'
import type { ToolCallHandler, ToolCallHandlerDependencies } from './types'
import type { AudioState } from '../../stores/generalStore'
import { isExpectedAbortError } from '../../utils/isAbortError'

const PREVIOUS_RESPONSE_NOT_FOUND = 'Previous response with id'
const NOT_FOUND_SUFFIX = 'not found'

function isAbortError(error: unknown): boolean {
return (
!!error &&
typeof error === 'object' &&
(error as { name?: string }).name === 'AbortError'
)
}

function isPreviousResponseMissingError(error: unknown): boolean {
if (!error || typeof error !== 'object') return false
const message = (error as { message?: string }).message || ''
Expand Down Expand Up @@ -87,7 +80,7 @@ export function createToolCallHandler(
await attemptContinuation(originalResponseIdForTool)
return
} catch (error) {
if (isAbortError(error)) {
if (isExpectedAbortError(error)) {
return
}

Expand All @@ -109,7 +102,7 @@ export function createToolCallHandler(
await attemptContinuation(null)
return
} catch (retryError) {
if (!isAbortError(retryError)) {
if (!isExpectedAbortError(retryError)) {
dependencies.logError(
'[Error Recovery] Retry also failed:',
retryError
Expand Down
Loading
Loading