Merge pull request #7376 from janhq/fix/update-renderer-using-plugins

fix: update renderer using plugins
This commit is contained in:
Louis
2026-01-22 16:36:34 +07:00
committed by GitHub
25 changed files with 145 additions and 207 deletions

View File

@@ -96,7 +96,8 @@ jobs:
- name: Build app
run: |
make build
env:
env:
NODE_OPTIONS: "--max-old-space-size=4196"
APP_PATH: '.'
- name: Upload Artifact

View File

@@ -173,6 +173,7 @@ jobs:
run: |
make build
env:
NODE_OPTIONS: "--max-old-space-size=4196"
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APP_PATH: '.'
POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}

View File

@@ -39,6 +39,10 @@
"@radix-ui/react-slot": "1.2.0",
"@radix-ui/react-switch": "1.2.2",
"@radix-ui/react-tooltip": "1.2.4",
"@streamdown/cjk": "^1.0.1",
"@streamdown/code": "^1.0.1",
"@streamdown/math": "^1.0.1",
"@streamdown/mermaid": "^1.0.1",
"@tabler/icons-react": "3.34.0",
"@tailwindcss/vite": "4.1.4",
"@tanstack/react-router": "^1.121.34",
@@ -86,7 +90,7 @@
"remark-math": "6.0.0",
"shiki": "^3.19.0",
"sonner": "2.0.5",
"streamdown": "npm:@janhq/streamdown@^2.0.2",
"streamdown": "npm:@janhq/streamdown@^2.1.1",
"tailwindcss": "4.1.17",
"token.js": "npm:token.js-fork@0.7.31",
"tw-animate-css": "1.2.8",

View File

@@ -154,12 +154,6 @@ const ChatInput = ({
const [isDragOver, setIsDragOver] = useState(false)
const [hasMmproj, setHasMmproj] = useState(false)
const activeModels = useAppState(useShallow((state) => state.activeModels))
const hasActiveModels = useMemo(
() =>
activeModels.length > 0 &&
activeModels.some((e) => e === selectedModel?.id),
[activeModels, selectedModel?.id]
)
// Jan Browser Extension hook
const {
@@ -1694,7 +1688,6 @@ const ChatInput = ({
<div className="flex items-center gap-2">
{selectedProvider === 'llamacpp' &&
hasActiveModels &&
tokenCounterCompact &&
!initialMessage &&
(threadMessages?.length > 0 || prompt.trim().length > 0) && (
@@ -1762,7 +1755,6 @@ const ChatInput = ({
)}
{selectedProvider === 'llamacpp' &&
hasActiveModels &&
!tokenCounterCompact &&
!initialMessage &&
(threadMessages?.length > 0 || prompt.trim().length > 0) && (

View File

@@ -357,7 +357,7 @@ export const MessageItem = memo(
{/* Message actions for assistant messages (non-tool) */}
{message.role === 'assistant' &&
message.parts.some((p) => p.type === 'text') && (
message.parts.some((p) => p.type === 'text' && p.text.length > 0) && (
<div className="flex items-center gap-2 text-main-view-fg/60 text-xs">
<div
className={cn(

View File

@@ -4,6 +4,11 @@ import { memo, useMemo } from 'react'
import { cn } from '@/lib/utils'
// import 'katex/dist/katex.min.css'
import { defaultRehypePlugins, Streamdown } from 'streamdown'
import { cjk } from '@streamdown/cjk'
import { code } from '@streamdown/code'
import { math } from '@streamdown/math'
import { mermaid } from '@streamdown/mermaid'
import remarkGfm from 'remark-gfm'
import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'
@@ -107,6 +112,12 @@ function RenderMarkdownComponent({
defaultRehypePlugins.harden,
]}
components={components}
plugins={{
code: code,
mermaid: mermaid,
math: math,
cjk: cjk,
}}
mermaid={
messageId
? {

View File

@@ -578,118 +578,4 @@ describe('useMCPServers', () => {
expect(result.current.deletedServerKeys).toContain('lifecycle-server')
})
})
describe('Proactive Mode Settings', () => {
it('should have proactiveMode in default settings', () => {
expect(DEFAULT_MCP_SETTINGS.proactiveMode).toBeDefined()
expect(DEFAULT_MCP_SETTINGS.proactiveMode).toBe(false)
})
it('should initialize proactiveMode as false', () => {
const { result } = renderHook(() => useMCPServers())
expect(result.current.settings.proactiveMode).toBe(false)
})
it('should update proactiveMode using updateSettings', () => {
const { result } = renderHook(() => useMCPServers())
act(() => {
result.current.updateSettings({ proactiveMode: true })
})
expect(result.current.settings.proactiveMode).toBe(true)
})
it('should toggle proactiveMode on and off', () => {
const { result } = renderHook(() => useMCPServers())
// Initially false
expect(result.current.settings.proactiveMode).toBe(false)
// Toggle to true
act(() => {
result.current.updateSettings({ proactiveMode: true })
})
expect(result.current.settings.proactiveMode).toBe(true)
// Toggle back to false
act(() => {
result.current.updateSettings({ proactiveMode: false })
})
expect(result.current.settings.proactiveMode).toBe(false)
})
it('should not affect other settings when updating proactiveMode', () => {
const { result } = renderHook(() => useMCPServers())
const originalSettings = { ...result.current.settings }
act(() => {
result.current.updateSettings({ proactiveMode: true })
})
expect(result.current.settings.toolCallTimeoutSeconds).toBe(
originalSettings.toolCallTimeoutSeconds
)
expect(result.current.settings.baseRestartDelayMs).toBe(
originalSettings.baseRestartDelayMs
)
expect(result.current.settings.maxRestartDelayMs).toBe(
originalSettings.maxRestartDelayMs
)
expect(result.current.settings.backoffMultiplier).toBe(
originalSettings.backoffMultiplier
)
})
it('should update proactiveMode along with other settings', () => {
const { result } = renderHook(() => useMCPServers())
act(() => {
result.current.updateSettings({
proactiveMode: true,
toolCallTimeoutSeconds: 60,
})
})
expect(result.current.settings.proactiveMode).toBe(true)
expect(result.current.settings.toolCallTimeoutSeconds).toBe(60)
})
it('should call syncServers with proactiveMode included in settings', async () => {
const { result } = renderHook(() => useMCPServers())
act(() => {
result.current.updateSettings({ proactiveMode: true })
})
await act(async () => {
await result.current.syncServers()
})
expect(mockUpdateMCPConfig).toHaveBeenCalledWith(
expect.stringContaining('proactiveMode')
)
})
it('should persist proactiveMode setting through setSettings', () => {
const { result } = renderHook(() => useMCPServers())
const newSettings = {
...DEFAULT_MCP_SETTINGS,
proactiveMode: true,
toolCallTimeoutSeconds: 45,
}
act(() => {
result.current.setSettings(newSettings)
})
expect(result.current.settings.proactiveMode).toBe(true)
expect(result.current.settings.toolCallTimeoutSeconds).toBe(45)
})
})
})

View File

@@ -176,10 +176,6 @@ export function useJanBrowserExtension() {
} catch (error) {
// Don't show error if cancelled
if (cancelledRef.current) return
toast.error('Failed to toggle Jan Browser MCP', {
description: error instanceof Error ? error.message : String(error),
})
console.error('Error toggling Jan Browser MCP:', error)
setDialogOpen(false)
setDialogState('closed')

View File

@@ -24,7 +24,6 @@ export type MCPSettings = {
baseRestartDelayMs: number
maxRestartDelayMs: number
backoffMultiplier: number
proactiveMode: boolean
}
export const DEFAULT_MCP_SETTINGS: MCPSettings = {
@@ -32,7 +31,6 @@ export const DEFAULT_MCP_SETTINGS: MCPSettings = {
baseRestartDelayMs: 1000,
maxRestartDelayMs: 30000,
backoffMultiplier: 2,
proactiveMode: false,
}
type MCPServerStoreState = {

View File

@@ -41,9 +41,6 @@ export type ServiceHub = {
Array<{ name: string; description: string; inputSchema: unknown }>
>
}
models(): {
startModel(provider: ProviderObject, model: string): Promise<unknown>
}
}
export class CustomChatTransport implements ChatTransport<UIMessage> {
@@ -225,20 +222,17 @@ export class CustomChatTransport implements ChatTransport<UIMessage> {
const updatedProvider = useModelProvider
.getState()
.getProviderByName(this.provider.provider)
// Start the model (this will be a no-op for remote providers)
await this.serviceHub
.models()
.startModel(updatedProvider ?? this.provider, this.modelId)
// Create the model using the factory
// For llamacpp provider, startModel is called internally in ModelFactory.createLlamaCppModel
this.model = await ModelFactory.createModel(
this.modelId,
updatedProvider ?? this.provider
)
} catch (error) {
console.error('Failed to start model:', error)
console.error('Failed to create model:', error)
throw new Error(
`Failed to start model: ${error instanceof Error ? error.message : String(error)}`
`Failed to create model: ${error instanceof Error ? error.message : JSON.stringify(error)}`
)
}
} else {

View File

@@ -106,7 +106,7 @@ export class ModelFactory {
switch (providerName) {
case 'llamacpp':
return this.createLlamaCppModel(modelId)
return this.createLlamaCppModel(modelId, provider)
case 'anthropic':
return this.createAnthropicModel(modelId, provider)
@@ -131,11 +131,29 @@ export class ModelFactory {
}
/**
* Create a llamacpp model by finding the running session
* Create a llamacpp model by starting the model and finding the running session
*/
private static async createLlamaCppModel(
modelId: string
modelId: string,
provider?: ProviderObject
): Promise<LanguageModel> {
// Start the model first if provider is available
if (provider) {
try {
const { useServiceStore } = await import('@/hooks/useServiceHub')
const serviceHub = useServiceStore.getState().serviceHub
if (serviceHub) {
await serviceHub.models().startModel(provider, modelId)
}
} catch (error) {
console.error('Failed to start llamacpp model:', error)
throw new Error(
`Failed to start model: ${error instanceof Error ? error.message : JSON.stringify(error)}`
)
}
}
// Get session info which includes port and api_key
const sessionInfo = await invoke<SessionInfo | null>(
'plugin:llamacpp|find_session_by_model',

View File

@@ -59,7 +59,5 @@
"description": "Nakonfigurujte, jak Jan spravuje a opakuje pokusy se servery MCP.",
"toolCallTimeout": "Časový limit volání nástroje (sekundy)",
"toolCallTimeoutDesc": "Maximální doba čekání na odpověď nástroje MCP před vypršením časového limitu."
},
"proactiveMode": "Proaktivní režim používání prohlížeče",
"proactiveModeDesc": "Pokud je povoleno, automaticky zachytává snímky obrazovky prohlížeče, aby poskytl další kontext pro úlohy automatizace prohlížeče. Vyžaduje Jan Browser MCP s modelem pro vidění a použití nástrojů."
}
}

View File

@@ -59,7 +59,5 @@
"description": "Konfiguriere, wie Jan MCP-Server verwaltet und erneut versucht.",
"toolCallTimeout": "Zeitlimit für Tool-Aufruf (Sekunden)",
"toolCallTimeoutDesc": "Maximale Wartezeit auf eine Antwort eines MCP-Tools, bevor abgebrochen wird."
},
"proactiveMode": "Browser Use Proaktiv-Modus",
"proactiveModeDesc": "Wenn aktiviert, werden automatisch Browser-Screenshots erfasst, um zusätzlichen Kontext für Browser-Automatisierungsaufgaben bereitzustellen. Erfordert Jan Browser MCP mit Vision- und Tool-Nutzungsmodell."
}
}

View File

@@ -59,7 +59,5 @@
"description": "Configure how Jan manages and retries MCP servers.",
"toolCallTimeout": "Tool call timeout (seconds)",
"toolCallTimeoutDesc": "Maximum time to wait for an MCP tool response before timing out."
},
"proactiveMode": "Browser Use Proactive Mode",
"proactiveModeDesc": "When enabled, automatically captures browser screenshots to provide additional context for browser automation tasks. Requires Jan Browser MCP with vision and tool use model."
}
}

View File

@@ -59,7 +59,5 @@
"description": "Configurez comment Jan gère et réessaye les serveurs MCP.",
"toolCallTimeout": "Délai d'appel d'outil (secondes)",
"toolCallTimeoutDesc": "Temps maximum d'attente pour une réponse d'outil MCP avant l'expiration du délai."
},
"proactiveMode": "Mode Proactif d'Utilisation du Navigateur",
"proactiveModeDesc": "Lorsqu'activé, capture automatiquement des captures d'écran du navigateur pour fournir un contexte supplémentaire pour les tâches d'automatisation du navigateur. Nécessite Jan Browser MCP avec un modèle de vision et d'utilisation d'outils."
}
}

View File

@@ -59,7 +59,5 @@
"description": "Atur bagaimana Jan mengelola dan mencoba ulang server MCP.",
"toolCallTimeout": "Batas waktu pemanggilan alat (detik)",
"toolCallTimeoutDesc": "Waktu maksimum menunggu respons alat MCP sebelum waktu habis."
},
"proactiveMode": "Mode Proaktif Penggunaan Browser",
"proactiveModeDesc": "Jika diaktifkan, secara otomatis menangkap tangkapan layar browser untuk memberikan konteks tambahan untuk tugas otomasi browser. Memerlukan Jan Browser MCP dengan model vision dan penggunaan tool."
}
}

View File

@@ -59,7 +59,5 @@
"description": "JanがMCPサーバーを管理し再試行する方法を設定します。",
"toolCallTimeout": "ツール呼び出しタイムアウト(秒)",
"toolCallTimeoutDesc": "MCPツールの応答を待機する最大時間タイムアウトまで。"
},
"proactiveMode": "ブラウザ使用プロアクティブモード",
"proactiveModeDesc": "有効にすると、ブラウザの自動化タスクに追加のコンテキストを提供するために、ブラウザのスクリーンショットを自動的にキャプチャします。VisionとToolを使用するモデルを備えたJan Browser MCPが必要です。"
}
}

View File

@@ -59,7 +59,5 @@
"description": "Skonfiguruj, jak Jan zarządza i ponawia próby połączenia z serwerami MCP.",
"toolCallTimeout": "Limit czasu wywołania narzędzia (sekundy)",
"toolCallTimeoutDesc": "Maksymalny czas oczekiwania na odpowiedź narzędzia MCP przed przekroczeniem limitu czasu."
},
"proactiveMode": "Tryb Proaktywnego Użycia Przeglądarki",
"proactiveModeDesc": "Po włączeniu automatycznie przechwytuje zrzuty ekranu przeglądarki, aby zapewnić dodatkowy kontekst dla zadań automatyzacji przeglądarki. Wymaga Jan Browser MCP z modelem vision i narzędzi."
}
}

View File

@@ -59,7 +59,5 @@
"description": "Configure como o Jan gerencia e tenta novamente os servidores MCP.",
"toolCallTimeout": "Tempo limite de chamada da ferramenta (segundos)",
"toolCallTimeoutDesc": "Tempo máximo de espera por uma resposta da ferramenta MCP antes de atingir o tempo limite."
},
"proactiveMode": "Modo Proativo de Uso do Navegador",
"proactiveModeDesc": "Quando habilitado, captura automaticamente capturas de tela do navegador para fornecer contexto adicional para tarefas de automação do navegador. Requer Jan Browser MCP com modelo de visão e uso de ferramentas."
}
}

View File

@@ -59,7 +59,5 @@
"description": "Настройте, как Jan управляет и перезапускает серверы MCP.",
"toolCallTimeout": "Время ожидания вызова инструмента (секунды)",
"toolCallTimeoutDesc": "Максимальное время ожидания ответа от инструмента MCP."
},
"proactiveMode": "Режим проактивного использования браузера",
"proactiveModeDesc": "При включении автоматически создаёт снимки экрана браузера для предоставления дополнительного контекста задачам автоматизации браузера. Требуется браузер Jan MCP и модель с поддержкой зрения и использования инструментов."
}
}

View File

@@ -59,7 +59,5 @@
"description": "Cấu hình cách Jan quản lý và thử lại các máy chủ MCP.",
"toolCallTimeout": "Thời gian chờ lệnh (giây)",
"toolCallTimeoutDesc": "Thời gian tối đa chờ phản hồi từ công cụ MCP trước khi hết thời gian."
},
"proactiveMode": "Chế độ Chủ động Sử dụng Trình duyệt",
"proactiveModeDesc": "Khi được bật, tự động chụp ảnh màn hình trình duyệt để cung cấp ngữ cảnh bổ sung cho các tác vụ tự động hóa trình duyệt. Yêu cầu Jan Browser MCP với mô hình vision và sử dụng công cụ."
}
}

View File

@@ -59,7 +59,5 @@
"description": "配置 Jan 如何管理和重试 MCP 服务器。",
"toolCallTimeout": "工具调用超时时间(秒)",
"toolCallTimeoutDesc": "在超时前等待 MCP 工具响应的最长时间。"
},
"proactiveMode": "浏览器使用主动模式",
"proactiveModeDesc": "启用后,会自动捕获浏览器截图,为浏览器自动化任务提供额外的上下文信息。需要具备视觉和工具使用模型的 Jan Browser MCP。"
}
}

View File

@@ -59,7 +59,5 @@
"description": "設定 Jan 如何管理及重試 MCP 伺服器。",
"toolCallTimeout": "工具呼叫逾時(秒)",
"toolCallTimeoutDesc": "等待 MCP 工具回應的最長時間,超過即逾時。"
},
"proactiveMode": "瀏覽器使用主動模式",
"proactiveModeDesc": "啟用後,會自動擷取瀏覽器螢幕截圖,為瀏覽器自動化工作提供額外的情境資訊。需要具備視覺與工具使用模型的 Jan Browser MCP。"
}
}

View File

@@ -456,28 +456,6 @@ function MCPServersDesktop() {
/>
}
/>
<CardItem
title={
<div className="flex items-center gap-2">
<span>{t('mcp-servers:proactiveMode')}</span>
<div className="text-xs bg-main-view-fg/10 border border-main-view-fg/20 text-main-view-fg/70 rounded-full py-0.5 px-2">
<span>{t('mcp-servers:experimental')}</span>
</div>
</div>
}
description={t('mcp-servers:proactiveModeDesc')}
actions={
<div className="flex-shrink-0 ml-4">
<Switch
checked={settings.proactiveMode}
onCheckedChange={(checked) => {
updateSettings({ proactiveMode: checked })
void syncServers()
}}
/>
</div>
}
/>
</Card>
{Object.keys(mcpServers).length === 0 ? (

View File

@@ -45,6 +45,9 @@ import { processAttachmentsForSend } from '@/lib/attachmentProcessing'
import { useAttachments } from '@/hooks/useAttachments'
import { PromptProgress } from '@/components/PromptProgress'
import { useToolAvailable } from '@/hooks/useToolAvailable'
import { OUT_OF_CONTEXT_SIZE } from '@/utils/error'
import { Button } from '@/components/ui/button'
import { IconAlertCircle } from '@tabler/icons-react'
const CHAT_STATUS = {
STREAMING: 'streaming',
@@ -151,6 +154,7 @@ function ThreadDetail() {
const {
messages: chatMessages,
status,
error,
sendMessage,
regenerate,
setMessages: setChatMessages,
@@ -533,12 +537,7 @@ function ThreadDetail() {
}
})()
}
}, [
threadId,
languageModelId,
languageModelProvider,
processAndSendMessage,
])
}, [threadId, languageModelId, languageModelProvider, processAndSendMessage])
// Handle submit from ChatInput
const handleSubmit = useCallback(
@@ -600,6 +599,60 @@ function ThreadDetail() {
// and generating a new response from the selected message
regenerate(messageId ? { messageId } : undefined)
}
// Handler for increasing context size
const handleContextSizeIncrease = useCallback(async () => {
if (!selectedModel) return
const updateProvider = useModelProvider.getState().updateProvider
const provider = getProviderByName(selectedProvider)
if (!provider) return
const modelIndex = provider.models.findIndex(
(m) => m.id === selectedModel.id
)
if (modelIndex === -1) return
const model = provider.models[modelIndex]
// Increase context length by 50%
const currentCtxLen =
(model.settings?.ctx_len?.controller_props?.value as number) ?? 8192
const newCtxLen = Math.round(Math.max(8192, currentCtxLen) * 1.5)
const updatedModel = {
...model,
settings: {
...model.settings,
ctx_len: {
...(model.settings?.ctx_len ?? {}),
controller_props: {
...(model.settings?.ctx_len?.controller_props ?? {}),
value: newCtxLen,
},
},
},
}
const updatedModels = [...provider.models]
updatedModels[modelIndex] = updatedModel as Model
updateProvider(provider.provider, {
models: updatedModels,
})
await serviceHub.models().stopModel(selectedModel.id)
setTimeout(() => {
handleRegenerate()
}, 1000)
}, [
selectedModel,
selectedProvider,
getProviderByName,
serviceHub,
])
const threadModel = useMemo(() => thread?.model, [thread])
if (!threadModel) return null
@@ -657,6 +710,36 @@ function ThreadDetail() {
)
})}
{status === CHAT_STATUS.SUBMITTED && <PromptProgress />}
{error && (
<div className="px-4 py-3 mx-4 my-2 rounded-lg border border-destructive/50 bg-destructive/10">
<div className="flex items-start gap-3">
<IconAlertCircle className="size-5 text-destructive flex-shrink-0 mt-0.5" />
<div className="flex-1">
<p className="text-sm font-medium text-destructive mb-1">
Error generating response
</p>
<p className="text-sm text-main-view-fg/70">
{error.message}
</p>
{(error.message.toLowerCase().includes('context') &&
(error.message.toLowerCase().includes('size') ||
error.message.toLowerCase().includes('length') ||
error.message.toLowerCase().includes('limit'))) ||
error.message === OUT_OF_CONTEXT_SIZE ? (
<Button
variant="outline"
size="sm"
className="mt-3"
onClick={handleContextSizeIncrease}
>
<IconAlertCircle className="size-4 mr-2" />
Increase Context Size
</Button>
) : null}
</div>
</div>
</div>
)}
</ConversationContent>
<ConversationScrollButton />
</Conversation>