mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-01-05 04:19:02 +08:00
Merge branch 'main' of github.com:CherryHQ/cherry-studio into feat/sora2
This commit is contained in:
commit
397a24b833
11
.github/workflows/claude-translator.yml
vendored
11
.github/workflows/claude-translator.yml
vendored
@ -16,10 +16,13 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
translate:
|
translate:
|
||||||
if: |
|
if: |
|
||||||
(github.event_name == 'issues') ||
|
(github.event_name == 'issues')
|
||||||
(github.event_name == 'issue_comment' && github.event.sender.type != 'Bot') ||
|
|| (github.event_name == 'issue_comment' && github.event.sender.type != 'Bot')
|
||||||
(github.event_name == 'pull_request_review' && github.event.sender.type != 'Bot') ||
|
|| (
|
||||||
(github.event_name == 'pull_request_review_comment' && github.event.sender.type != 'Bot')
|
(github.event_name == 'pull_request_review' || github.event_name == 'pull_request_review_comment')
|
||||||
|
&& github.event.sender.type != 'Bot'
|
||||||
|
&& github.event.pull_request.head.repo.fork == false
|
||||||
|
)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import tseslint from '@electron-toolkit/eslint-config-ts'
|
|||||||
import eslint from '@eslint/js'
|
import eslint from '@eslint/js'
|
||||||
import eslintReact from '@eslint-react/eslint-plugin'
|
import eslintReact from '@eslint-react/eslint-plugin'
|
||||||
import { defineConfig } from 'eslint/config'
|
import { defineConfig } from 'eslint/config'
|
||||||
|
import importZod from 'eslint-plugin-import-zod'
|
||||||
import oxlint from 'eslint-plugin-oxlint'
|
import oxlint from 'eslint-plugin-oxlint'
|
||||||
import reactHooks from 'eslint-plugin-react-hooks'
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
import simpleImportSort from 'eslint-plugin-simple-import-sort'
|
import simpleImportSort from 'eslint-plugin-simple-import-sort'
|
||||||
@ -15,7 +16,8 @@ export default defineConfig([
|
|||||||
{
|
{
|
||||||
plugins: {
|
plugins: {
|
||||||
'simple-import-sort': simpleImportSort,
|
'simple-import-sort': simpleImportSort,
|
||||||
'unused-imports': unusedImports
|
'unused-imports': unusedImports,
|
||||||
|
'import-zod': importZod
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||||
@ -25,6 +27,7 @@ export default defineConfig([
|
|||||||
'simple-import-sort/exports': 'error',
|
'simple-import-sort/exports': 'error',
|
||||||
'unused-imports/no-unused-imports': 'error',
|
'unused-imports/no-unused-imports': 'error',
|
||||||
'@eslint-react/no-prop-types': 'error',
|
'@eslint-react/no-prop-types': 'error',
|
||||||
|
'import-zod/prefer-zod-namespace': 'error'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Configuration for ensuring compatibility with the original ESLint(8.x) rules
|
// Configuration for ensuring compatibility with the original ESLint(8.x) rules
|
||||||
|
|||||||
@ -258,6 +258,7 @@
|
|||||||
"emoji-picker-element": "^1.22.1",
|
"emoji-picker-element": "^1.22.1",
|
||||||
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
|
"epub": "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch",
|
||||||
"eslint": "^9.22.0",
|
"eslint": "^9.22.0",
|
||||||
|
"eslint-plugin-import-zod": "^1.2.0",
|
||||||
"eslint-plugin-oxlint": "^1.15.0",
|
"eslint-plugin-oxlint": "^1.15.0",
|
||||||
"eslint-plugin-react-hooks": "^5.2.0",
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||||
@ -295,7 +296,7 @@
|
|||||||
"motion": "^12.10.5",
|
"motion": "^12.10.5",
|
||||||
"notion-helper": "^1.3.22",
|
"notion-helper": "^1.3.22",
|
||||||
"npx-scope-finder": "^1.2.0",
|
"npx-scope-finder": "^1.2.0",
|
||||||
"oxlint": "^1.15.0",
|
"oxlint": "^1.22.0",
|
||||||
"oxlint-tsgolint": "^0.2.0",
|
"oxlint-tsgolint": "^0.2.0",
|
||||||
"p-queue": "^8.1.0",
|
"p-queue": "^8.1.0",
|
||||||
"pdf-lib": "^1.17.1",
|
"pdf-lib": "^1.17.1",
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import { LanguageModelV2 } from '@ai-sdk/provider'
|
|||||||
import { createXai } from '@ai-sdk/xai'
|
import { createXai } from '@ai-sdk/xai'
|
||||||
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
||||||
import { customProvider, Provider } from 'ai'
|
import { customProvider, Provider } from 'ai'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基础 Provider IDs
|
* 基础 Provider IDs
|
||||||
|
|||||||
@ -53,6 +53,7 @@ export enum IpcChannel {
|
|||||||
|
|
||||||
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
|
Webview_SetOpenLinkExternal = 'webview:set-open-link-external',
|
||||||
Webview_SetSpellCheckEnabled = 'webview:set-spell-check-enabled',
|
Webview_SetSpellCheckEnabled = 'webview:set-spell-check-enabled',
|
||||||
|
Webview_SearchHotkey = 'webview:search-hotkey',
|
||||||
|
|
||||||
// Open
|
// Open
|
||||||
Open_Path = 'open:path',
|
Open_Path = 'open:path',
|
||||||
|
|||||||
@ -22,3 +22,12 @@ export type MCPProgressEvent = {
|
|||||||
callId: string
|
callId: string
|
||||||
progress: number // 0-1 range
|
progress: number // 0-1 range
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type WebviewKeyEvent = {
|
||||||
|
webviewId: number
|
||||||
|
key: string
|
||||||
|
control: boolean
|
||||||
|
meta: boolean
|
||||||
|
shift: boolean
|
||||||
|
alt: boolean
|
||||||
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import selectionService, { initSelectionService } from './services/SelectionServ
|
|||||||
import { registerShortcuts } from './services/ShortcutService'
|
import { registerShortcuts } from './services/ShortcutService'
|
||||||
import { TrayService } from './services/TrayService'
|
import { TrayService } from './services/TrayService'
|
||||||
import { windowService } from './services/WindowService'
|
import { windowService } from './services/WindowService'
|
||||||
|
import { initWebviewHotkeys } from './services/WebviewService'
|
||||||
|
|
||||||
const logger = loggerService.withContext('MainEntry')
|
const logger = loggerService.withContext('MainEntry')
|
||||||
|
|
||||||
@ -108,6 +109,7 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
// Some APIs can only be used after this event occurs.
|
// Some APIs can only be used after this event occurs.
|
||||||
|
|
||||||
app.whenReady().then(async () => {
|
app.whenReady().then(async () => {
|
||||||
|
initWebviewHotkeys()
|
||||||
// Set app user model id for windows
|
// Set app user model id for windows
|
||||||
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
electronApp.setAppUserModelId(import.meta.env.VITE_MAIN_BUNDLE_ID || 'com.kangfenmao.CherryStudio')
|
||||||
|
|
||||||
|
|||||||
@ -786,7 +786,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
|
|||||||
ipcMain.handle(IpcChannel.Webview_SetOpenLinkExternal, (_, webviewId: number, isExternal: boolean) =>
|
ipcMain.handle(IpcChannel.Webview_SetOpenLinkExternal, (_, webviewId: number, isExternal: boolean) =>
|
||||||
setOpenLinkExternal(webviewId, isExternal)
|
setOpenLinkExternal(webviewId, isExternal)
|
||||||
)
|
)
|
||||||
|
|
||||||
ipcMain.handle(IpcChannel.Webview_SetSpellCheckEnabled, (_, webviewId: number, isEnable: boolean) => {
|
ipcMain.handle(IpcChannel.Webview_SetSpellCheckEnabled, (_, webviewId: number, isEnable: boolean) => {
|
||||||
const webview = webContents.fromId(webviewId)
|
const webview = webContents.fromId(webviewId)
|
||||||
if (!webview) return
|
if (!webview) return
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { loggerService } from '@logger'
|
|||||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
||||||
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
|
||||||
import { net } from 'electron'
|
import { net } from 'electron'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
const logger = loggerService.withContext('DifyKnowledgeServer')
|
const logger = loggerService.withContext('DifyKnowledgeServer')
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprot
|
|||||||
import { net } from 'electron'
|
import { net } from 'electron'
|
||||||
import { JSDOM } from 'jsdom'
|
import { JSDOM } from 'jsdom'
|
||||||
import TurndownService from 'turndown'
|
import TurndownService from 'turndown'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
export const RequestPayloadSchema = z.object({
|
export const RequestPayloadSchema = z.object({
|
||||||
url: z.url(),
|
url: z.url(),
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import fs from 'fs/promises'
|
|||||||
import { minimatch } from 'minimatch'
|
import { minimatch } from 'minimatch'
|
||||||
import os from 'os'
|
import os from 'os'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
const logger = loggerService.withContext('MCP:FileSystemServer')
|
const logger = loggerService.withContext('MCP:FileSystemServer')
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { session, shell, webContents } from 'electron'
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
|
import { app, session, shell, webContents } from 'electron'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* init the useragent of the webview session
|
* init the useragent of the webview session
|
||||||
@ -36,3 +37,61 @@ export function setOpenLinkExternal(webviewId: number, isExternal: boolean) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const attachKeyboardHandler = (contents: Electron.WebContents) => {
|
||||||
|
if (contents.getType?.() !== 'webview') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBeforeInput = (event: Electron.Event, input: Electron.Input) => {
|
||||||
|
if (!input) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = input.key?.toLowerCase()
|
||||||
|
if (!key) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFindShortcut = (input.control || input.meta) && key === 'f'
|
||||||
|
const isEscape = key === 'escape'
|
||||||
|
const isEnter = key === 'enter'
|
||||||
|
|
||||||
|
if (!isFindShortcut && !isEscape && !isEnter) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Prevent default to override the guest page's native find dialog
|
||||||
|
// and keep shortcuts routed to our custom search overlay
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const host = contents.hostWebContents
|
||||||
|
if (!host || host.isDestroyed()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
host.send(IpcChannel.Webview_SearchHotkey, {
|
||||||
|
webviewId: contents.id,
|
||||||
|
key,
|
||||||
|
control: Boolean(input.control),
|
||||||
|
meta: Boolean(input.meta),
|
||||||
|
shift: Boolean(input.shift),
|
||||||
|
alt: Boolean(input.alt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
contents.on('before-input-event', handleBeforeInput)
|
||||||
|
contents.once('destroyed', () => {
|
||||||
|
contents.removeListener('before-input-event', handleBeforeInput)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initWebviewHotkeys() {
|
||||||
|
webContents.getAllWebContents().forEach((contents) => {
|
||||||
|
if (contents.isDestroyed()) return
|
||||||
|
attachKeyboardHandler(contents)
|
||||||
|
})
|
||||||
|
|
||||||
|
app.on('web-contents-created', (_, contents) => {
|
||||||
|
attachKeyboardHandler(contents)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import {
|
|||||||
OAuthTokens
|
OAuthTokens
|
||||||
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
} from '@modelcontextprotocol/sdk/shared/auth.js'
|
||||||
import EventEmitter from 'events'
|
import EventEmitter from 'events'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
export interface OAuthStorageData {
|
export interface OAuthStorageData {
|
||||||
clientInfo?: OAuthClientInformation
|
clientInfo?: OAuthClientInformation
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { loadOcrImage } from '@main/utils/ocr'
|
import { loadOcrImage } from '@main/utils/ocr'
|
||||||
import { ImageFileMetadata, isImageFileMetadata, OcrPpocrConfig, OcrResult, SupportedOcrFile } from '@types'
|
import { ImageFileMetadata, isImageFileMetadata, OcrPpocrConfig, OcrResult, SupportedOcrFile } from '@types'
|
||||||
import { net } from 'electron'
|
import { net } from 'electron'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
import { OcrBaseService } from './OcrBaseService'
|
import { OcrBaseService } from './OcrBaseService'
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { SpanEntity, TokenUsage } from '@mcp-trace/trace-core'
|
|||||||
import { SpanContext } from '@opentelemetry/api'
|
import { SpanContext } from '@opentelemetry/api'
|
||||||
import { TerminalConfig, UpgradeChannel } from '@shared/config/constant'
|
import { TerminalConfig, UpgradeChannel } from '@shared/config/constant'
|
||||||
import type { LogLevel, LogSourceWithContext } from '@shared/config/logger'
|
import type { LogLevel, LogSourceWithContext } from '@shared/config/logger'
|
||||||
import type { FileChangeEvent } from '@shared/config/types'
|
import type { FileChangeEvent, WebviewKeyEvent } from '@shared/config/types'
|
||||||
import { IpcChannel } from '@shared/IpcChannel'
|
import { IpcChannel } from '@shared/IpcChannel'
|
||||||
import type { Notification } from '@types'
|
import type { Notification } from '@types'
|
||||||
import {
|
import {
|
||||||
@ -390,7 +390,16 @@ const api = {
|
|||||||
setOpenLinkExternal: (webviewId: number, isExternal: boolean) =>
|
setOpenLinkExternal: (webviewId: number, isExternal: boolean) =>
|
||||||
ipcRenderer.invoke(IpcChannel.Webview_SetOpenLinkExternal, webviewId, isExternal),
|
ipcRenderer.invoke(IpcChannel.Webview_SetOpenLinkExternal, webviewId, isExternal),
|
||||||
setSpellCheckEnabled: (webviewId: number, isEnable: boolean) =>
|
setSpellCheckEnabled: (webviewId: number, isEnable: boolean) =>
|
||||||
ipcRenderer.invoke(IpcChannel.Webview_SetSpellCheckEnabled, webviewId, isEnable)
|
ipcRenderer.invoke(IpcChannel.Webview_SetSpellCheckEnabled, webviewId, isEnable),
|
||||||
|
onFindShortcut: (callback: (payload: WebviewKeyEvent) => void) => {
|
||||||
|
const listener = (_event: Electron.IpcRendererEvent, payload: WebviewKeyEvent) => {
|
||||||
|
callback(payload)
|
||||||
|
}
|
||||||
|
ipcRenderer.on(IpcChannel.Webview_SearchHotkey, listener)
|
||||||
|
return () => {
|
||||||
|
ipcRenderer.off(IpcChannel.Webview_SearchHotkey, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
storeSync: {
|
storeSync: {
|
||||||
subscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Subscribe),
|
subscribe: () => ipcRenderer.invoke(IpcChannel.StoreSync_Subscribe),
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import type { Assistant, KnowledgeReference } from '@renderer/types'
|
|||||||
import { ExtractResults, KnowledgeExtractResults } from '@renderer/utils/extract'
|
import { ExtractResults, KnowledgeExtractResults } from '@renderer/utils/extract'
|
||||||
import { type InferToolInput, type InferToolOutput, tool } from 'ai'
|
import { type InferToolInput, type InferToolOutput, tool } from 'ai'
|
||||||
import { isEmpty } from 'lodash'
|
import { isEmpty } from 'lodash'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 知识库搜索工具
|
* 知识库搜索工具
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import store from '@renderer/store'
|
import store from '@renderer/store'
|
||||||
import { selectCurrentUserId, selectGlobalMemoryEnabled, selectMemoryConfig } from '@renderer/store/memory'
|
import { selectCurrentUserId, selectGlobalMemoryEnabled, selectMemoryConfig } from '@renderer/store/memory'
|
||||||
import { type InferToolInput, type InferToolOutput, tool } from 'ai'
|
import { type InferToolInput, type InferToolOutput, tool } from 'ai'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
import { MemoryProcessor } from '../../services/MemoryProcessor'
|
import { MemoryProcessor } from '../../services/MemoryProcessor'
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import WebSearchService from '@renderer/services/WebSearchService'
|
|||||||
import { WebSearchProvider, WebSearchProviderResponse } from '@renderer/types'
|
import { WebSearchProvider, WebSearchProviderResponse } from '@renderer/types'
|
||||||
import { ExtractResults } from '@renderer/utils/extract'
|
import { ExtractResults } from '@renderer/utils/extract'
|
||||||
import { type InferToolInput, type InferToolOutput, tool } from 'ai'
|
import { type InferToolInput, type InferToolOutput, tool } from 'ai'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 使用预提取关键词的网络搜索工具
|
* 使用预提取关键词的网络搜索工具
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import {
|
|||||||
getThinkModelType,
|
getThinkModelType,
|
||||||
isDeepSeekHybridInferenceModel,
|
isDeepSeekHybridInferenceModel,
|
||||||
isDoubaoThinkingAutoModel,
|
isDoubaoThinkingAutoModel,
|
||||||
|
isGrok4FastReasoningModel,
|
||||||
isGrokReasoningModel,
|
isGrokReasoningModel,
|
||||||
isOpenAIReasoningModel,
|
isOpenAIReasoningModel,
|
||||||
isQwenAlwaysThinkModel,
|
isQwenAlwaysThinkModel,
|
||||||
@ -52,7 +53,12 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
// Don't disable reasoning for models that require it
|
// Don't disable reasoning for models that require it
|
||||||
if (isGrokReasoningModel(model) || isOpenAIReasoningModel(model) || model.id.includes('seed-oss')) {
|
if (
|
||||||
|
isGrokReasoningModel(model) ||
|
||||||
|
isOpenAIReasoningModel(model) ||
|
||||||
|
isQwenAlwaysThinkModel(model) ||
|
||||||
|
model.id.includes('seed-oss')
|
||||||
|
) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
return { reasoning: { enabled: false, exclude: true } }
|
return { reasoning: { enabled: false, exclude: true } }
|
||||||
@ -100,6 +106,7 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
|||||||
// reasoningEffort有效的情况
|
// reasoningEffort有效的情况
|
||||||
// DeepSeek hybrid inference models, v3.1 and maybe more in the future
|
// DeepSeek hybrid inference models, v3.1 and maybe more in the future
|
||||||
// 不同的 provider 有不同的思考控制方式,在这里统一解决
|
// 不同的 provider 有不同的思考控制方式,在这里统一解决
|
||||||
|
|
||||||
if (isDeepSeekHybridInferenceModel(model)) {
|
if (isDeepSeekHybridInferenceModel(model)) {
|
||||||
if (isSystemProvider(provider)) {
|
if (isSystemProvider(provider)) {
|
||||||
switch (provider.id) {
|
switch (provider.id) {
|
||||||
@ -142,6 +149,16 @@ export function getReasoningEffort(assistant: Assistant, model: Model): Reasonin
|
|||||||
|
|
||||||
// OpenRouter models
|
// OpenRouter models
|
||||||
if (model.provider === SystemProviderIds.openrouter) {
|
if (model.provider === SystemProviderIds.openrouter) {
|
||||||
|
// Grok 4 Fast doesn't support effort levels, always use enabled: true
|
||||||
|
if (isGrok4FastReasoningModel(model)) {
|
||||||
|
return {
|
||||||
|
reasoning: {
|
||||||
|
enabled: true // Ignore effort level, just enable reasoning
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other OpenRouter models that support effort levels
|
||||||
if (isSupportedReasoningEffortModel(model) || isSupportedThinkingTokenModel(model)) {
|
if (isSupportedReasoningEffortModel(model) || isSupportedThinkingTokenModel(model)) {
|
||||||
return {
|
return {
|
||||||
reasoning: {
|
reasoning: {
|
||||||
@ -412,6 +429,13 @@ export function getGeminiReasoningParams(assistant: Assistant, model: Model): Re
|
|||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get XAI-specific reasoning parameters
|
||||||
|
* This function should only be called for XAI provider models
|
||||||
|
* @param assistant - The assistant configuration
|
||||||
|
* @param model - The model being used
|
||||||
|
* @returns XAI-specific reasoning parameters
|
||||||
|
*/
|
||||||
export function getXAIReasoningParams(assistant: Assistant, model: Model): Record<string, any> {
|
export function getXAIReasoningParams(assistant: Assistant, model: Model): Record<string, any> {
|
||||||
if (!isSupportedReasoningEffortGrokModel(model)) {
|
if (!isSupportedReasoningEffortGrokModel(model)) {
|
||||||
return {}
|
return {}
|
||||||
@ -419,6 +443,11 @@ export function getXAIReasoningParams(assistant: Assistant, model: Model): Recor
|
|||||||
|
|
||||||
const { reasoning_effort: reasoningEffort } = getAssistantSettings(assistant)
|
const { reasoning_effort: reasoningEffort } = getAssistantSettings(assistant)
|
||||||
|
|
||||||
|
if (!reasoningEffort) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For XAI provider Grok models, use reasoningEffort parameter directly
|
||||||
return {
|
return {
|
||||||
reasoningEffort
|
reasoningEffort
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { GEMINI_FLASH_MODEL_REGEX } from './websearch'
|
|||||||
|
|
||||||
// Reasoning models
|
// Reasoning models
|
||||||
export const REASONING_REGEX =
|
export const REASONING_REGEX =
|
||||||
/^(o\d+(?:-[\w-]+)?|.*\b(?:reasoning|reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*|.*\bhunyuan-t1(?:-[\w-]+)?\b.*|.*\bglm-zero-preview\b.*|.*\bgrok-(?:3-mini|4)(?:-[\w-]+)?\b.*)$/i
|
/^(?!.*-non-reasoning\b)(o\d+(?:-[\w-]+)?|.*\b(?:reasoning|reasoner|thinking)\b.*|.*-[rR]\d+.*|.*\bqwq(?:-[\w-]+)?\b.*|.*\bhunyuan-t1(?:-[\w-]+)?\b.*|.*\bglm-zero-preview\b.*|.*\bgrok-(?:3-mini|4|4-fast)(?:-[\w-]+)?\b.*)$/i
|
||||||
|
|
||||||
// 模型类型到支持的reasoning_effort的映射表
|
// 模型类型到支持的reasoning_effort的映射表
|
||||||
// TODO: refactor this. too many identical options
|
// TODO: refactor this. too many identical options
|
||||||
@ -24,6 +24,7 @@ export const MODEL_SUPPORTED_REASONING_EFFORT: ReasoningEffortConfig = {
|
|||||||
gpt5: ['minimal', 'low', 'medium', 'high'] as const,
|
gpt5: ['minimal', 'low', 'medium', 'high'] as const,
|
||||||
gpt5_codex: ['low', 'medium', 'high'] as const,
|
gpt5_codex: ['low', 'medium', 'high'] as const,
|
||||||
grok: ['low', 'high'] as const,
|
grok: ['low', 'high'] as const,
|
||||||
|
grok4_fast: ['auto'] as const,
|
||||||
gemini: ['low', 'medium', 'high', 'auto'] as const,
|
gemini: ['low', 'medium', 'high', 'auto'] as const,
|
||||||
gemini_pro: ['low', 'medium', 'high', 'auto'] as const,
|
gemini_pro: ['low', 'medium', 'high', 'auto'] as const,
|
||||||
qwen: ['low', 'medium', 'high'] as const,
|
qwen: ['low', 'medium', 'high'] as const,
|
||||||
@ -43,6 +44,7 @@ export const MODEL_SUPPORTED_OPTIONS: ThinkingOptionConfig = {
|
|||||||
gpt5: [...MODEL_SUPPORTED_REASONING_EFFORT.gpt5] as const,
|
gpt5: [...MODEL_SUPPORTED_REASONING_EFFORT.gpt5] as const,
|
||||||
gpt5_codex: MODEL_SUPPORTED_REASONING_EFFORT.gpt5_codex,
|
gpt5_codex: MODEL_SUPPORTED_REASONING_EFFORT.gpt5_codex,
|
||||||
grok: MODEL_SUPPORTED_REASONING_EFFORT.grok,
|
grok: MODEL_SUPPORTED_REASONING_EFFORT.grok,
|
||||||
|
grok4_fast: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.grok4_fast] as const,
|
||||||
gemini: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini] as const,
|
gemini: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.gemini] as const,
|
||||||
gemini_pro: MODEL_SUPPORTED_REASONING_EFFORT.gemini_pro,
|
gemini_pro: MODEL_SUPPORTED_REASONING_EFFORT.gemini_pro,
|
||||||
qwen: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.qwen] as const,
|
qwen: ['off', ...MODEL_SUPPORTED_REASONING_EFFORT.qwen] as const,
|
||||||
@ -66,6 +68,8 @@ export const getThinkModelType = (model: Model): ThinkingModelType => {
|
|||||||
}
|
}
|
||||||
} else if (isSupportedReasoningEffortOpenAIModel(model)) {
|
} else if (isSupportedReasoningEffortOpenAIModel(model)) {
|
||||||
thinkingModelType = 'o'
|
thinkingModelType = 'o'
|
||||||
|
} else if (isGrok4FastReasoningModel(model)) {
|
||||||
|
thinkingModelType = 'grok4_fast'
|
||||||
} else if (isSupportedThinkingTokenGeminiModel(model)) {
|
} else if (isSupportedThinkingTokenGeminiModel(model)) {
|
||||||
if (GEMINI_FLASH_MODEL_REGEX.test(model.id)) {
|
if (GEMINI_FLASH_MODEL_REGEX.test(model.id)) {
|
||||||
thinkingModelType = 'gemini'
|
thinkingModelType = 'gemini'
|
||||||
@ -142,19 +146,46 @@ export function isSupportedReasoningEffortGrokModel(model?: Model): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const modelId = getLowerBaseModelName(model.id)
|
const modelId = getLowerBaseModelName(model.id)
|
||||||
|
const providerId = model.provider.toLowerCase()
|
||||||
if (modelId.includes('grok-3-mini')) {
|
if (modelId.includes('grok-3-mini')) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (providerId === 'openrouter' && modelId.includes('grok-4-fast')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the model is Grok 4 Fast reasoning version
|
||||||
|
* Explicitly excludes non-reasoning variants (models with 'non-reasoning' in their ID)
|
||||||
|
*
|
||||||
|
* Note: XAI official uses different model IDs for reasoning vs non-reasoning
|
||||||
|
* Third-party providers like OpenRouter expose a single ID with reasoning parameters, while first-party providers require separate IDs. Only the OpenRouter variant supports toggling.
|
||||||
|
*
|
||||||
|
* @param model - The model to check
|
||||||
|
* @returns true if the model is a reasoning-enabled Grok 4 Fast model
|
||||||
|
*/
|
||||||
|
export function isGrok4FastReasoningModel(model?: Model): boolean {
|
||||||
|
if (!model) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const modelId = getLowerBaseModelName(model.id)
|
||||||
|
return modelId.includes('grok-4-fast') && !modelId.includes('non-reasoning')
|
||||||
|
}
|
||||||
|
|
||||||
export function isGrokReasoningModel(model?: Model): boolean {
|
export function isGrokReasoningModel(model?: Model): boolean {
|
||||||
if (!model) {
|
if (!model) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const modelId = getLowerBaseModelName(model.id)
|
const modelId = getLowerBaseModelName(model.id)
|
||||||
if (isSupportedReasoningEffortGrokModel(model) || modelId.includes('grok-4')) {
|
if (
|
||||||
|
isSupportedReasoningEffortGrokModel(model) ||
|
||||||
|
(modelId.includes('grok-4') && !modelId.includes('non-reasoning'))
|
||||||
|
) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,7 +296,11 @@ export function isQwenAlwaysThinkModel(model?: Model): boolean {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const modelId = getLowerBaseModelName(model.id, '/')
|
const modelId = getLowerBaseModelName(model.id, '/')
|
||||||
return modelId.startsWith('qwen3') && modelId.includes('thinking')
|
// 包括 qwen3 开头的 thinking 模型和 qwen3-vl 的 thinking 模型
|
||||||
|
return (
|
||||||
|
(modelId.startsWith('qwen3') && modelId.includes('thinking')) ||
|
||||||
|
(modelId.includes('qwen3-vl') && modelId.includes('thinking'))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Doubao 支持思考模式的模型正则
|
// Doubao 支持思考模式的模型正则
|
||||||
@ -329,7 +364,10 @@ export const isPerplexityReasoningModel = (model?: Model): boolean => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const modelId = getLowerBaseModelName(model.id, '/')
|
const modelId = getLowerBaseModelName(model.id, '/')
|
||||||
return isSupportedReasoningEffortPerplexityModel(model) || modelId.includes('reasoning')
|
return (
|
||||||
|
isSupportedReasoningEffortPerplexityModel(model) ||
|
||||||
|
(modelId.includes('reasoning') && !modelId.includes('non-reasoning'))
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const isSupportedReasoningEffortPerplexityModel = (model: Model): boolean => {
|
export const isSupportedReasoningEffortPerplexityModel = (model: Model): boolean => {
|
||||||
@ -443,6 +481,8 @@ export const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> =
|
|||||||
// qwen-plus-x 系列自 qwen-plus-2025-07-28 后模型最长思维链变为 81_920, qwen-plus 模型于 2025.9.16 同步变更
|
// qwen-plus-x 系列自 qwen-plus-2025-07-28 后模型最长思维链变为 81_920, qwen-plus 模型于 2025.9.16 同步变更
|
||||||
'qwen3-235b-a22b-thinking-2507$': { min: 0, max: 81_920 },
|
'qwen3-235b-a22b-thinking-2507$': { min: 0, max: 81_920 },
|
||||||
'qwen3-30b-a3b-thinking-2507$': { min: 0, max: 81_920 },
|
'qwen3-30b-a3b-thinking-2507$': { min: 0, max: 81_920 },
|
||||||
|
'qwen3-vl-235b-a22b-thinking$': { min: 0, max: 81_920 },
|
||||||
|
'qwen3-vl-30b-a3b-thinking$': { min: 0, max: 81_920 },
|
||||||
'qwen-plus-2025-07-14$': { min: 0, max: 38_912 },
|
'qwen-plus-2025-07-14$': { min: 0, max: 38_912 },
|
||||||
'qwen-plus-2025-04-28$': { min: 0, max: 38_912 },
|
'qwen-plus-2025-04-28$': { min: 0, max: 38_912 },
|
||||||
'qwen3-1\\.7b$': { min: 0, max: 30_720 },
|
'qwen3-1\\.7b$': { min: 0, max: 30_720 },
|
||||||
|
|||||||
@ -24,7 +24,7 @@ const visionAllowedModels = [
|
|||||||
'qwen2.5-vl',
|
'qwen2.5-vl',
|
||||||
'qwen3-vl',
|
'qwen3-vl',
|
||||||
'qwen2.5-omni',
|
'qwen2.5-omni',
|
||||||
'qwen3-omni',
|
'qwen3-omni(?:-[\\w-]+)?',
|
||||||
'qvq',
|
'qvq',
|
||||||
'internvl2',
|
'internvl2',
|
||||||
'grok-vision-beta',
|
'grok-vision-beta',
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import Favicon from '@renderer/components/Icons/FallbackFavicon'
|
|||||||
import { Tooltip } from 'antd'
|
import { Tooltip } from 'antd'
|
||||||
import React, { memo, useCallback, useMemo } from 'react'
|
import React, { memo, useCallback, useMemo } from 'react'
|
||||||
import styled from 'styled-components'
|
import styled from 'styled-components'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
export const CitationSchema = z.object({
|
export const CitationSchema = z.object({
|
||||||
url: z.url(),
|
url: z.url(),
|
||||||
|
|||||||
@ -21,11 +21,11 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
const [query, setQuery] = useState('')
|
const [query, setQuery] = useState('')
|
||||||
const [matchCount, setMatchCount] = useState(0)
|
const [matchCount, setMatchCount] = useState(0)
|
||||||
const [activeIndex, setActiveIndex] = useState(0)
|
const [activeIndex, setActiveIndex] = useState(0)
|
||||||
const [currentWebview, setCurrentWebview] = useState<WebviewTag | null>(null)
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
const focusFrameRef = useRef<number | null>(null)
|
const focusFrameRef = useRef<number | null>(null)
|
||||||
const lastAppIdRef = useRef<string>(appId)
|
const lastAppIdRef = useRef<string>(appId)
|
||||||
const attachedWebviewRef = useRef<WebviewTag | null>(null)
|
const attachedWebviewRef = useRef<WebviewTag | null>(null)
|
||||||
|
const activeWebview = webviewRef.current ?? null
|
||||||
|
|
||||||
const focusInput = useCallback(() => {
|
const focusInput = useCallback(() => {
|
||||||
if (focusFrameRef.current !== null) {
|
if (focusFrameRef.current !== null) {
|
||||||
@ -118,34 +118,66 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
}, [performSearch, query])
|
}, [performSearch, query])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const nextWebview = webviewRef.current ?? null
|
attachedWebviewRef.current = activeWebview
|
||||||
if (currentWebview === nextWebview) return
|
if (!activeWebview) {
|
||||||
setCurrentWebview(nextWebview)
|
|
||||||
}, [currentWebview, webviewRef])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const target = currentWebview
|
|
||||||
if (!target) {
|
|
||||||
attachedWebviewRef.current = null
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const handle = handleFoundInPage
|
const handle = handleFoundInPage
|
||||||
attachedWebviewRef.current = target
|
activeWebview.addEventListener('found-in-page', handle)
|
||||||
target.addEventListener('found-in-page', handle)
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
target.removeEventListener('found-in-page', handle)
|
activeWebview.removeEventListener('found-in-page', handle)
|
||||||
if (attachedWebviewRef.current === target) {
|
if (attachedWebviewRef.current === activeWebview) {
|
||||||
try {
|
try {
|
||||||
target.stopFindInPage('clearSelection')
|
activeWebview.stopFindInPage('clearSelection')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('stopFindInPage failed', { error })
|
logger.error('stopFindInPage failed', { error })
|
||||||
}
|
}
|
||||||
attachedWebviewRef.current = null
|
attachedWebviewRef.current = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [currentWebview, handleFoundInPage])
|
}, [activeWebview, handleFoundInPage])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeWebview) return
|
||||||
|
const onFindShortcut = window.api?.webview?.onFindShortcut
|
||||||
|
if (!onFindShortcut) return
|
||||||
|
|
||||||
|
const webContentsId = activeWebview.getWebContentsId?.()
|
||||||
|
if (!webContentsId) {
|
||||||
|
logger.warn('WebviewSearch: missing webContentsId', { appId })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const unsubscribe = onFindShortcut(({ webviewId, key, control, meta, shift }) => {
|
||||||
|
if (webviewId !== webContentsId) return
|
||||||
|
|
||||||
|
if ((control || meta) && key === 'f') {
|
||||||
|
openSearch()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isVisible) return
|
||||||
|
|
||||||
|
if (key === 'escape') {
|
||||||
|
closeSearch()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'enter') {
|
||||||
|
if (shift) {
|
||||||
|
goToPrevious()
|
||||||
|
} else {
|
||||||
|
goToNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe?.()
|
||||||
|
}
|
||||||
|
}, [appId, activeWebview, closeSearch, goToNext, goToPrevious, isVisible, openSearch])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isVisible) return
|
if (!isVisible) return
|
||||||
@ -159,7 +191,7 @@ const WebviewSearch: FC<WebviewSearchProps> = ({ webviewRef, isWebviewReady, app
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
performSearch(query)
|
performSearch(query)
|
||||||
}, [currentWebview, isVisible, performSearch, query])
|
}, [activeWebview, isVisible, performSearch, query])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeydown = (event: KeyboardEvent) => {
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import type { WebviewKeyEvent } from '@shared/config/types'
|
||||||
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import type { WebviewTag } from 'electron'
|
import type { WebviewTag } from 'electron'
|
||||||
@ -36,6 +37,7 @@ const createWebviewMock = () => {
|
|||||||
listeners.get(type)?.delete(listener)
|
listeners.get(type)?.delete(listener)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
getWebContentsId: vi.fn(() => 1),
|
||||||
findInPage: findInPageMock as unknown as WebviewTag['findInPage'],
|
findInPage: findInPageMock as unknown as WebviewTag['findInPage'],
|
||||||
stopFindInPage: stopFindInPageMock as unknown as WebviewTag['stopFindInPage']
|
stopFindInPage: stopFindInPageMock as unknown as WebviewTag['stopFindInPage']
|
||||||
} as unknown as WebviewTag
|
} as unknown as WebviewTag
|
||||||
@ -102,13 +104,34 @@ describe('WebviewSearch', () => {
|
|||||||
info: vi.fn(),
|
info: vi.fn(),
|
||||||
addToast: vi.fn()
|
addToast: vi.fn()
|
||||||
}
|
}
|
||||||
|
let removeFindShortcutListenerMock: ReturnType<typeof vi.fn>
|
||||||
|
let onFindShortcutMock: ReturnType<typeof vi.fn>
|
||||||
|
const invokeLatestShortcut = (payload: WebviewKeyEvent) => {
|
||||||
|
const handler = onFindShortcutMock.mock.calls.at(-1)?.[0] as ((args: WebviewKeyEvent) => void) | undefined
|
||||||
|
if (!handler) {
|
||||||
|
throw new Error('Shortcut handler not registered')
|
||||||
|
}
|
||||||
|
act(() => {
|
||||||
|
handler(payload)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
removeFindShortcutListenerMock = vi.fn()
|
||||||
|
onFindShortcutMock = vi.fn(() => removeFindShortcutListenerMock)
|
||||||
|
Object.assign(window as any, {
|
||||||
|
api: {
|
||||||
|
webview: {
|
||||||
|
onFindShortcut: onFindShortcutMock
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Object.assign(window, { toast: toastMock })
|
Object.assign(window, { toast: toastMock })
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
Reflect.deleteProperty(window, 'api')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('opens the search overlay with keyboard shortcut', async () => {
|
it('opens the search overlay with keyboard shortcut', async () => {
|
||||||
@ -124,6 +147,47 @@ describe('WebviewSearch', () => {
|
|||||||
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument()
|
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('opens the search overlay when webview shortcut is forwarded', async () => {
|
||||||
|
const { webview } = createWebviewMock()
|
||||||
|
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||||
|
|
||||||
|
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
invokeLatestShortcut({ webviewId: 1, key: 'f', control: true, meta: false, shift: false, alt: false })
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('closes the search overlay when escape is forwarded from the webview', async () => {
|
||||||
|
const { webview } = createWebviewMock()
|
||||||
|
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||||
|
|
||||||
|
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
invokeLatestShortcut({ webviewId: 1, key: 'f', control: true, meta: false, shift: false, alt: false })
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onFindShortcutMock.mock.calls.length).toBeGreaterThanOrEqual(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
invokeLatestShortcut({ webviewId: 1, key: 'escape', control: false, meta: false, shift: false, alt: false })
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByPlaceholderText('Search')).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('performs searches and navigates between results', async () => {
|
it('performs searches and navigates between results', async () => {
|
||||||
const { emit, findInPageMock, webview } = createWebviewMock()
|
const { emit, findInPageMock, webview } = createWebviewMock()
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||||
@ -165,6 +229,45 @@ describe('WebviewSearch', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('navigates results when enter is forwarded from the webview', async () => {
|
||||||
|
const { findInPageMock, webview } = createWebviewMock()
|
||||||
|
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||||
|
const user = userEvent.setup()
|
||||||
|
|
||||||
|
render(<WebviewSearch webviewRef={webviewRef} isWebviewReady appId="app-1" />)
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onFindShortcutMock).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
invokeLatestShortcut({ webviewId: 1, key: 'f', control: true, meta: false, shift: false, alt: false })
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByPlaceholderText('Search')).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onFindShortcutMock.mock.calls.length).toBeGreaterThanOrEqual(2)
|
||||||
|
})
|
||||||
|
|
||||||
|
const input = screen.getByRole('textbox')
|
||||||
|
await user.type(input, 'Cherry')
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(findInPageMock).toHaveBeenCalledWith('Cherry', undefined)
|
||||||
|
})
|
||||||
|
findInPageMock.mockClear()
|
||||||
|
|
||||||
|
invokeLatestShortcut({ webviewId: 1, key: 'enter', control: false, meta: false, shift: false, alt: false })
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(findInPageMock).toHaveBeenCalledWith('Cherry', { forward: true, findNext: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
findInPageMock.mockClear()
|
||||||
|
invokeLatestShortcut({ webviewId: 1, key: 'enter', control: false, meta: false, shift: true, alt: false })
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(findInPageMock).toHaveBeenCalledWith('Cherry', { forward: false, findNext: true })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('clears search state when appId changes', async () => {
|
it('clears search state when appId changes', async () => {
|
||||||
const { findInPageMock, stopFindInPageMock, webview } = createWebviewMock()
|
const { findInPageMock, stopFindInPageMock, webview } = createWebviewMock()
|
||||||
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
const webviewRef = { current: webview } as React.RefObject<WebviewTag | null>
|
||||||
@ -219,6 +322,7 @@ describe('WebviewSearch', () => {
|
|||||||
unmount()
|
unmount()
|
||||||
|
|
||||||
expect(stopFindInPageMock).toHaveBeenCalledWith('clearSelection')
|
expect(stopFindInPageMock).toHaveBeenCalledWith('clearSelection')
|
||||||
|
expect(removeFindShortcutListenerMock).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('ignores keyboard shortcut when webview is not ready', async () => {
|
it('ignores keyboard shortcut when webview is not ready', async () => {
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
* WARNING: Any null value will be converted to undefined from api.
|
* WARNING: Any null value will be converted to undefined from api.
|
||||||
*/
|
*/
|
||||||
import { ModelMessage, TextStreamPart } from 'ai'
|
import { ModelMessage, TextStreamPart } from 'ai'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
import type { Message, MessageBlock } from './newMessage'
|
import type { Message, MessageBlock } from './newMessage'
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Model } from '@types'
|
import { Model } from '@types'
|
||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
import { ProviderTypeSchema } from './provider'
|
import { ProviderTypeSchema } from './provider'
|
||||||
|
|
||||||
|
|||||||
@ -82,6 +82,7 @@ const ThinkModelTypes = [
|
|||||||
'gpt5',
|
'gpt5',
|
||||||
'gpt5_codex',
|
'gpt5_codex',
|
||||||
'grok',
|
'grok',
|
||||||
|
'grok4_fast',
|
||||||
'gemini',
|
'gemini',
|
||||||
'gemini_pro',
|
'gemini_pro',
|
||||||
'qwen',
|
'qwen',
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { Model } from '@types'
|
import { Model } from '@types'
|
||||||
import z from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
export const ProviderTypeSchema = z.enum([
|
export const ProviderTypeSchema = z.enum([
|
||||||
'openai',
|
'openai',
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
export type ToolType = 'builtin' | 'provider' | 'mcp'
|
export type ToolType = 'builtin' | 'provider' | 'mcp'
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
export const freshnessOptions = ['oneDay', 'oneWeek', 'oneMonth', 'oneYear', 'noLimit'] as const
|
export const freshnessOptions = ['oneDay', 'oneWeek', 'oneMonth', 'oneYear', 'noLimit'] as const
|
||||||
|
|
||||||
|
|||||||
@ -11,7 +11,8 @@ import {
|
|||||||
import { InvalidToolInputError, NoSuchToolError } from 'ai'
|
import { InvalidToolInputError, NoSuchToolError } from 'ai'
|
||||||
import { AxiosError, isAxiosError } from 'axios'
|
import { AxiosError, isAxiosError } from 'axios'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
import { z, ZodError } from 'zod'
|
import * as z from 'zod'
|
||||||
|
import { ZodError } from 'zod'
|
||||||
|
|
||||||
import { parseJSON } from './json'
|
import { parseJSON } from './json'
|
||||||
import { safeSerialize } from './serialize'
|
import { safeSerialize } from './serialize'
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { z } from 'zod'
|
import * as z from 'zod'
|
||||||
|
|
||||||
// Define Zod schema for fact retrieval output
|
// Define Zod schema for fact retrieval output
|
||||||
export const FactRetrievalSchema = z.object({
|
export const FactRetrievalSchema = z.object({
|
||||||
|
|||||||
85
yarn.lock
85
yarn.lock
@ -7832,58 +7832,58 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@oxlint/darwin-arm64@npm:1.15.0":
|
"@oxlint/darwin-arm64@npm:1.22.0":
|
||||||
version: 1.15.0
|
version: 1.22.0
|
||||||
resolution: "@oxlint/darwin-arm64@npm:1.15.0"
|
resolution: "@oxlint/darwin-arm64@npm:1.22.0"
|
||||||
conditions: os=darwin & cpu=arm64
|
conditions: os=darwin & cpu=arm64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@oxlint/darwin-x64@npm:1.15.0":
|
"@oxlint/darwin-x64@npm:1.22.0":
|
||||||
version: 1.15.0
|
version: 1.22.0
|
||||||
resolution: "@oxlint/darwin-x64@npm:1.15.0"
|
resolution: "@oxlint/darwin-x64@npm:1.22.0"
|
||||||
conditions: os=darwin & cpu=x64
|
conditions: os=darwin & cpu=x64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@oxlint/linux-arm64-gnu@npm:1.15.0":
|
"@oxlint/linux-arm64-gnu@npm:1.22.0":
|
||||||
version: 1.15.0
|
version: 1.22.0
|
||||||
resolution: "@oxlint/linux-arm64-gnu@npm:1.15.0"
|
resolution: "@oxlint/linux-arm64-gnu@npm:1.22.0"
|
||||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@oxlint/linux-arm64-musl@npm:1.15.0":
|
"@oxlint/linux-arm64-musl@npm:1.22.0":
|
||||||
version: 1.15.0
|
version: 1.22.0
|
||||||
resolution: "@oxlint/linux-arm64-musl@npm:1.15.0"
|
resolution: "@oxlint/linux-arm64-musl@npm:1.22.0"
|
||||||
conditions: os=linux & cpu=arm64 & libc=musl
|
conditions: os=linux & cpu=arm64 & libc=musl
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@oxlint/linux-x64-gnu@npm:1.15.0":
|
"@oxlint/linux-x64-gnu@npm:1.22.0":
|
||||||
version: 1.15.0
|
version: 1.22.0
|
||||||
resolution: "@oxlint/linux-x64-gnu@npm:1.15.0"
|
resolution: "@oxlint/linux-x64-gnu@npm:1.22.0"
|
||||||
conditions: os=linux & cpu=x64 & libc=glibc
|
conditions: os=linux & cpu=x64 & libc=glibc
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@oxlint/linux-x64-musl@npm:1.15.0":
|
"@oxlint/linux-x64-musl@npm:1.22.0":
|
||||||
version: 1.15.0
|
version: 1.22.0
|
||||||
resolution: "@oxlint/linux-x64-musl@npm:1.15.0"
|
resolution: "@oxlint/linux-x64-musl@npm:1.22.0"
|
||||||
conditions: os=linux & cpu=x64 & libc=musl
|
conditions: os=linux & cpu=x64 & libc=musl
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@oxlint/win32-arm64@npm:1.15.0":
|
"@oxlint/win32-arm64@npm:1.22.0":
|
||||||
version: 1.15.0
|
version: 1.22.0
|
||||||
resolution: "@oxlint/win32-arm64@npm:1.15.0"
|
resolution: "@oxlint/win32-arm64@npm:1.22.0"
|
||||||
conditions: os=win32 & cpu=arm64
|
conditions: os=win32 & cpu=arm64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@oxlint/win32-x64@npm:1.15.0":
|
"@oxlint/win32-x64@npm:1.22.0":
|
||||||
version: 1.15.0
|
version: 1.22.0
|
||||||
resolution: "@oxlint/win32-x64@npm:1.15.0"
|
resolution: "@oxlint/win32-x64@npm:1.22.0"
|
||||||
conditions: os=win32 & cpu=x64
|
conditions: os=win32 & cpu=x64
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
@ -14441,6 +14441,7 @@ __metadata:
|
|||||||
emoji-picker-element: "npm:^1.22.1"
|
emoji-picker-element: "npm:^1.22.1"
|
||||||
epub: "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch"
|
epub: "patch:epub@npm%3A1.3.0#~/.yarn/patches/epub-npm-1.3.0-8325494ffe.patch"
|
||||||
eslint: "npm:^9.22.0"
|
eslint: "npm:^9.22.0"
|
||||||
|
eslint-plugin-import-zod: "npm:^1.2.0"
|
||||||
eslint-plugin-oxlint: "npm:^1.15.0"
|
eslint-plugin-oxlint: "npm:^1.15.0"
|
||||||
eslint-plugin-react-hooks: "npm:^5.2.0"
|
eslint-plugin-react-hooks: "npm:^5.2.0"
|
||||||
eslint-plugin-simple-import-sort: "npm:^12.1.1"
|
eslint-plugin-simple-import-sort: "npm:^12.1.1"
|
||||||
@ -14484,7 +14485,7 @@ __metadata:
|
|||||||
npx-scope-finder: "npm:^1.2.0"
|
npx-scope-finder: "npm:^1.2.0"
|
||||||
officeparser: "npm:^4.2.0"
|
officeparser: "npm:^4.2.0"
|
||||||
os-proxy-config: "npm:^1.1.2"
|
os-proxy-config: "npm:^1.1.2"
|
||||||
oxlint: "npm:^1.15.0"
|
oxlint: "npm:^1.22.0"
|
||||||
oxlint-tsgolint: "npm:^0.2.0"
|
oxlint-tsgolint: "npm:^0.2.0"
|
||||||
p-queue: "npm:^8.1.0"
|
p-queue: "npm:^8.1.0"
|
||||||
pdf-lib: "npm:^1.17.1"
|
pdf-lib: "npm:^1.17.1"
|
||||||
@ -18607,6 +18608,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"eslint-plugin-import-zod@npm:^1.2.0":
|
||||||
|
version: 1.2.0
|
||||||
|
resolution: "eslint-plugin-import-zod@npm:1.2.0"
|
||||||
|
peerDependencies:
|
||||||
|
"@typescript-eslint/utils": ^8.35.1
|
||||||
|
eslint: ">=9"
|
||||||
|
checksum: 10c0/c03f3059c4e55fa50ad43da6db989bb4c51c6466ab17bd7ec7d096421d060a20488ee1ef0df80fa61706ad0b6a4534622540ceab0638d3b6887df7d810ef1d22
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"eslint-plugin-oxlint@npm:^1.15.0":
|
"eslint-plugin-oxlint@npm:^1.15.0":
|
||||||
version: 1.15.0
|
version: 1.15.0
|
||||||
resolution: "eslint-plugin-oxlint@npm:1.15.0"
|
resolution: "eslint-plugin-oxlint@npm:1.15.0"
|
||||||
@ -24597,18 +24608,18 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"oxlint@npm:^1.15.0":
|
"oxlint@npm:^1.22.0":
|
||||||
version: 1.15.0
|
version: 1.22.0
|
||||||
resolution: "oxlint@npm:1.15.0"
|
resolution: "oxlint@npm:1.22.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@oxlint/darwin-arm64": "npm:1.15.0"
|
"@oxlint/darwin-arm64": "npm:1.22.0"
|
||||||
"@oxlint/darwin-x64": "npm:1.15.0"
|
"@oxlint/darwin-x64": "npm:1.22.0"
|
||||||
"@oxlint/linux-arm64-gnu": "npm:1.15.0"
|
"@oxlint/linux-arm64-gnu": "npm:1.22.0"
|
||||||
"@oxlint/linux-arm64-musl": "npm:1.15.0"
|
"@oxlint/linux-arm64-musl": "npm:1.22.0"
|
||||||
"@oxlint/linux-x64-gnu": "npm:1.15.0"
|
"@oxlint/linux-x64-gnu": "npm:1.22.0"
|
||||||
"@oxlint/linux-x64-musl": "npm:1.15.0"
|
"@oxlint/linux-x64-musl": "npm:1.22.0"
|
||||||
"@oxlint/win32-arm64": "npm:1.15.0"
|
"@oxlint/win32-arm64": "npm:1.22.0"
|
||||||
"@oxlint/win32-x64": "npm:1.15.0"
|
"@oxlint/win32-x64": "npm:1.22.0"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
oxlint-tsgolint: ">=0.2.0"
|
oxlint-tsgolint: ">=0.2.0"
|
||||||
dependenciesMeta:
|
dependenciesMeta:
|
||||||
@ -24634,7 +24645,7 @@ __metadata:
|
|||||||
bin:
|
bin:
|
||||||
oxc_language_server: bin/oxc_language_server
|
oxc_language_server: bin/oxc_language_server
|
||||||
oxlint: bin/oxlint
|
oxlint: bin/oxlint
|
||||||
checksum: 10c0/3eb2a27b972f2a02200b068345ab6a3a17f7bc29c4546c6b3478727388d8d59b94a554f9b6bb1320b71a75cc598b728de0ffee5e4e70ac27457104b8efebb257
|
checksum: 10c0/652c93b9360ea66c7ee87f649a56ba2b8eddc5e32494a53a61ca86749d87ce2be354960e135a60ab7054105e6b187e9d4ec56959cdb02d517423c23d6a523894
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user