diff --git a/.github/workflows/template-tauri-build-windows-x64.yml b/.github/workflows/template-tauri-build-windows-x64.yml index 643fef5ac..ed00ef90f 100644 --- a/.github/workflows/template-tauri-build-windows-x64.yml +++ b/.github/workflows/template-tauri-build-windows-x64.yml @@ -54,6 +54,8 @@ on: value: ${{ jobs.build-windows-x64.outputs.WIN_SIG }} FILE_NAME: value: ${{ jobs.build-windows-x64.outputs.FILE_NAME }} + MSI_FILE_NAME: + value: ${{ jobs.build-windows-x64.outputs.MSI_FILE_NAME }} jobs: build-windows-x64: @@ -61,6 +63,7 @@ jobs: outputs: WIN_SIG: ${{ steps.metadata.outputs.WIN_SIG }} FILE_NAME: ${{ steps.metadata.outputs.FILE_NAME }} + MSI_FILE_NAME: ${{ steps.metadata.outputs.MSI_FILE_NAME }} permissions: contents: write steps: @@ -189,9 +192,15 @@ jobs: - name: Upload Artifact uses: actions/upload-artifact@v4 with: - name: jan-windows-${{ inputs.new_version }} + name: jan-windows-exe-${{ inputs.new_version }} path: | ./src-tauri/target/release/bundle/nsis/*.exe + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: jan-windows-msi-${{ inputs.new_version }} + path: | + ./src-tauri/target/release/bundle/msi/*.msi ## Set output filename for windows - name: Set output filename for windows @@ -201,13 +210,18 @@ jobs: if [ "${{ inputs.channel }}" != "stable" ]; then FILE_NAME=Jan-${{ inputs.channel }}_${{ inputs.new_version }}_x64-setup.exe WIN_SIG=$(cat Jan-${{ inputs.channel }}_${{ inputs.new_version }}_x64-setup.exe.sig) + + MSI_FILE="Jan-${{ inputs.channel }}_${{ inputs.new_version }}_x64_en-US.msi" else FILE_NAME=Jan_${{ inputs.new_version }}_x64-setup.exe WIN_SIG=$(cat Jan_${{ inputs.new_version }}_x64-setup.exe.sig) + + MSI_FILE="Jan_${{ inputs.new_version }}_x64_en-US.msi" fi echo "::set-output name=WIN_SIG::$WIN_SIG" echo "::set-output name=FILE_NAME::$FILE_NAME" + echo "::set-output name=MSI_FILE_NAME::$MSI_FILE" id: metadata ## Upload to s3 for nightly and beta @@ -220,6 +234,8 @@ jobs: # Upload for tauri updater aws s3 cp ./${{ steps.metadata.outputs.FILE_NAME }} s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/${{ steps.metadata.outputs.FILE_NAME }} aws s3 cp ./${{ steps.metadata.outputs.FILE_NAME }}.sig s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/${{ steps.metadata.outputs.FILE_NAME }}.sig + + aws s3 cp ./src-tauri/target/release/bundle/msi/${{ steps.metadata.outputs.MSI_FILE_NAME }} s3://${{ secrets.DELTA_AWS_S3_BUCKET_NAME }}/temp-${{ inputs.channel }}/${{ steps.metadata.outputs.MSI_FILE_NAME }} env: AWS_ACCESS_KEY_ID: ${{ secrets.DELTA_AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.DELTA_AWS_SECRET_ACCESS_KEY }} @@ -236,3 +252,13 @@ jobs: asset_path: ./src-tauri/target/release/bundle/nsis/${{ steps.metadata.outputs.FILE_NAME }} asset_name: ${{ steps.metadata.outputs.FILE_NAME }} asset_content_type: application/octet-stream + - name: Upload release assert if public provider is github + if: inputs.public_provider == 'github' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: actions/upload-release-asset@v1.0.1 + with: + upload_url: ${{ inputs.upload_url }} + asset_path: ./src-tauri/target/release/bundle/msi/${{ steps.metadata.outputs.MSI_FILE_NAME }} + asset_name: ${{ steps.metadata.outputs.MSI_FILE_NAME }} + asset_content_type: application/octet-stream diff --git a/core/package.json b/core/package.json index eec56a733..203eaf293 100644 --- a/core/package.json +++ b/core/package.json @@ -27,11 +27,13 @@ "devDependencies": { "@npmcli/arborist": "^7.1.0", "@types/node": "^22.10.0", + "@types/react": "19.1.2", "@vitest/coverage-v8": "^2.1.8", "@vitest/ui": "^2.1.8", "eslint": "8.57.0", "happy-dom": "^15.11.6", "pacote": "^21.0.0", + "react": "19.0.0", "request": "^2.88.2", "request-progress": "^3.0.0", "rimraf": "^6.0.1", @@ -44,5 +46,8 @@ "rxjs": "^7.8.1", "ulidx": "^2.3.0" }, + "peerDependencies": { + "react": "19.0.0" + }, "packageManager": "yarn@4.5.3" } diff --git a/core/rolldown.config.mjs b/core/rolldown.config.mjs index fd3329ee0..fbb2bd351 100644 --- a/core/rolldown.config.mjs +++ b/core/rolldown.config.mjs @@ -10,7 +10,7 @@ export default defineConfig([ sourcemap: true, }, platform: 'browser', - external: ['path'], + external: ['path', 'react', 'react-dom', 'react/jsx-runtime'], define: { NODE: JSON.stringify(`${pkgJson.name}/${pkgJson.node}`), VERSION: JSON.stringify(pkgJson.version), diff --git a/core/src/browser/extensions/conversational.test.ts b/core/src/browser/extensions/conversational.test.ts index c08468905..44d1e9b4f 100644 --- a/core/src/browser/extensions/conversational.test.ts +++ b/core/src/browser/extensions/conversational.test.ts @@ -250,4 +250,4 @@ describe('ConversationalExtension', () => { expect(retrievedAssistant.modelId).toBe('') }) -}) \ No newline at end of file +}) diff --git a/core/src/browser/extensions/engines/LocalOAIEngine.test.ts b/core/src/browser/extensions/engines/LocalOAIEngine.test.ts index 5f2563d56..3523c3ce6 100644 --- a/core/src/browser/extensions/engines/LocalOAIEngine.test.ts +++ b/core/src/browser/extensions/engines/LocalOAIEngine.test.ts @@ -131,4 +131,4 @@ describe('LocalOAIEngine', () => { expect(engine.loadedModel).toBeUndefined() }) }) -}) \ No newline at end of file +}) diff --git a/core/src/browser/extensions/mcp.test.ts b/core/src/browser/extensions/mcp.test.ts index ece971809..8ba3f200a 100644 --- a/core/src/browser/extensions/mcp.test.ts +++ b/core/src/browser/extensions/mcp.test.ts @@ -96,4 +96,4 @@ describe('MCPExtension', () => { expect(healthy).toBe(true) }) }) -}) \ No newline at end of file +}) diff --git a/core/src/browser/extensions/mcp.ts b/core/src/browser/extensions/mcp.ts index 7f30a5428..74a008d40 100644 --- a/core/src/browser/extensions/mcp.ts +++ b/core/src/browser/extensions/mcp.ts @@ -1,5 +1,6 @@ -import { MCPInterface, MCPTool, MCPToolCallResult } from '../../types' +import { MCPInterface, MCPTool, MCPToolCallResult, MCPToolComponentProps } from '../../types' import { BaseExtension, ExtensionTypeEnum } from '../extension' +import type { ComponentType } from 'react' /** * MCP (Model Context Protocol) extension for managing tools and server communication. @@ -18,4 +19,16 @@ export abstract class MCPExtension extends BaseExtension implements MCPInterface abstract getConnectedServers(): Promise abstract refreshTools(): Promise abstract isHealthy(): Promise -} \ No newline at end of file + + /** + * Optional method to provide a custom UI component for tools + * @returns A React component or null if no custom component is provided + */ + getToolComponent?(): ComponentType | null + + /** + * Optional method to get the list of tool names that should be disabled by default + * @returns Array of tool names that should be disabled by default for new users + */ + getDefaultDisabledTools?(): Promise +} diff --git a/core/src/browser/models/manager.test.ts b/core/src/browser/models/manager.test.ts index 90626b22e..0e4e728cf 100644 --- a/core/src/browser/models/manager.test.ts +++ b/core/src/browser/models/manager.test.ts @@ -131,4 +131,4 @@ describe('ModelManager', () => { expect(modelManager.models.get('model-2')).toEqual(model2) }) }) -}) \ No newline at end of file +}) diff --git a/core/src/test/setup.ts b/core/src/test/setup.ts index c597a3748..6f3d766bf 100644 --- a/core/src/test/setup.ts +++ b/core/src/test/setup.ts @@ -16,4 +16,4 @@ if (!window.core) { }) } -// Add any other global mocks needed for core tests \ No newline at end of file +// Add any other global mocks needed for core tests diff --git a/core/src/types/mcp/index.ts b/core/src/types/mcp/index.ts index 4ffd501fc..bd809d790 100644 --- a/core/src/types/mcp/index.ts +++ b/core/src/types/mcp/index.ts @@ -1,2 +1,2 @@ export * from './mcpEntity' -export * from './mcpInterface' \ No newline at end of file +export * from './mcpInterface' diff --git a/core/src/types/mcp/mcpEntity.ts b/core/src/types/mcp/mcpEntity.ts index a2259e52e..6c7e0c598 100644 --- a/core/src/types/mcp/mcpEntity.ts +++ b/core/src/types/mcp/mcpEntity.ts @@ -21,4 +21,18 @@ export interface MCPServerInfo { name: string connected: boolean tools?: MCPTool[] -} \ No newline at end of file +} + +/** + * Props for MCP tool UI components + */ +export interface MCPToolComponentProps { + /** List of available MCP tools */ + tools: MCPTool[] + + /** Function to check if a specific tool is currently enabled */ + isToolEnabled: (toolName: string) => boolean + + /** Function to toggle a tool's enabled/disabled state */ + onToolToggle: (toolName: string, enabled: boolean) => void +} diff --git a/core/src/types/mcp/mcpInterface.ts b/core/src/types/mcp/mcpInterface.ts index 15152a83b..a656e9f66 100644 --- a/core/src/types/mcp/mcpInterface.ts +++ b/core/src/types/mcp/mcpInterface.ts @@ -29,4 +29,4 @@ export interface MCPInterface { * Check if MCP service is healthy */ isHealthy(): Promise -} \ No newline at end of file +} diff --git a/docs/_redirects b/docs/_redirects index ef126ea63..b72c65400 100644 --- a/docs/_redirects +++ b/docs/_redirects @@ -115,6 +115,9 @@ /docs/built-in/tensorrt-llm /docs/desktop/llama-cpp 302 /docs/desktop/docs/desktop/linux /docs/desktop/install/linux 302 /windows /docs/desktop/install/windows 302 +/docs/quickstart /docs/ 302 +/docs/desktop/mac /docs/desktop/install/mac 302 +/handbook/open-superintelligence /handbook/why/open-superintelligence 302 /guides/integrations/continue/ /docs/desktop/server-examples/continue-dev 302 /continue-dev /docs/desktop/server-examples/continue-dev 302 @@ -133,4 +136,4 @@ /local-server/troubleshooting /docs/desktop/troubleshooting 302 /mcp /docs/desktop/mcp 302 /quickstart /docs/desktop/quickstart 302 -/server-examples/continue-dev /docs/desktop/server-examples/continue-dev 302 \ No newline at end of file +/server-examples/continue-dev /docs/desktop/server-examples/continue-dev 302 diff --git a/docs/plopfile.js b/docs/plopfile.js index a31caa889..0c384af85 100644 --- a/docs/plopfile.js +++ b/docs/plopfile.js @@ -6,7 +6,7 @@ const camelCase = (str) => { return str.replace(/[-_](\w)/g, (_, c) => c.toUpperCase()) } -const categories = ['building-jan', 'research'] +const categories = ['building-jan', 'research', 'guides'] /** * @param {import("plop").NodePlopAPI} plop diff --git a/docs/public/assets/images/general/ai-for-teacher.jpeg b/docs/public/assets/images/general/ai-for-teacher.jpeg new file mode 100644 index 000000000..c600389b3 Binary files /dev/null and b/docs/public/assets/images/general/ai-for-teacher.jpeg differ diff --git a/docs/public/assets/images/general/assistants-ai-for-teachers.jpeg b/docs/public/assets/images/general/assistants-ai-for-teachers.jpeg new file mode 100644 index 000000000..b9aedf135 Binary files /dev/null and b/docs/public/assets/images/general/assistants-ai-for-teachers.jpeg differ diff --git a/docs/public/assets/images/general/jan-ai-for-teacher.mp4 b/docs/public/assets/images/general/jan-ai-for-teacher.mp4 new file mode 100644 index 000000000..3930e6e26 Binary files /dev/null and b/docs/public/assets/images/general/jan-ai-for-teacher.mp4 differ diff --git a/docs/public/assets/images/general/jan-assistants-ai-for-legal.jpeg b/docs/public/assets/images/general/jan-assistants-ai-for-legal.jpeg new file mode 100644 index 000000000..b49de6380 Binary files /dev/null and b/docs/public/assets/images/general/jan-assistants-ai-for-legal.jpeg differ diff --git a/docs/public/assets/images/general/jan-for-ai-law-assistant-chat.jpeg b/docs/public/assets/images/general/jan-for-ai-law-assistant-chat.jpeg new file mode 100644 index 000000000..5501b5e70 Binary files /dev/null and b/docs/public/assets/images/general/jan-for-ai-law-assistant-chat.jpeg differ diff --git a/docs/src/components/Blog/index.tsx b/docs/src/components/Blog/index.tsx index ca10cf408..16899df2c 100644 --- a/docs/src/components/Blog/index.tsx +++ b/docs/src/components/Blog/index.tsx @@ -19,6 +19,10 @@ const Blog = () => { name: 'Research', id: 'research', }, + { + name: 'Guides', + id: 'guides', + }, ] return ( diff --git a/docs/src/pages/post/_assets/create-assistant-1.jpeg b/docs/src/pages/post/_assets/create-assistant-1.jpeg new file mode 100644 index 000000000..4c005b6ed Binary files /dev/null and b/docs/src/pages/post/_assets/create-assistant-1.jpeg differ diff --git a/docs/src/pages/post/_assets/jan-assistant-for-law.png b/docs/src/pages/post/_assets/jan-assistant-for-law.png new file mode 100644 index 000000000..298d92f0f Binary files /dev/null and b/docs/src/pages/post/_assets/jan-assistant-for-law.png differ diff --git a/docs/src/pages/post/_assets/jan-for-ai-law-assistant-chat.jpeg b/docs/src/pages/post/_assets/jan-for-ai-law-assistant-chat.jpeg new file mode 100644 index 000000000..5501b5e70 Binary files /dev/null and b/docs/src/pages/post/_assets/jan-for-ai-law-assistant-chat.jpeg differ diff --git a/docs/src/pages/post/_meta.json b/docs/src/pages/post/_meta.json index e5472dd61..1962ffa9e 100644 --- a/docs/src/pages/post/_meta.json +++ b/docs/src/pages/post/_meta.json @@ -20,5 +20,10 @@ "title": "Research", "display": "normal", "href": "/blog?category=research" + }, + "guides-cat": { + "title": "Guides", + "display": "normal", + "href": "/blog?category=guides" } } diff --git a/docs/src/pages/post/ai-for-law.mdx b/docs/src/pages/post/ai-for-law.mdx new file mode 100644 index 000000000..a42b70ced --- /dev/null +++ b/docs/src/pages/post/ai-for-law.mdx @@ -0,0 +1,123 @@ +--- +title: "Private AI for legal professionals who need confidentiality" +description: "It's possible to use AI without risking client data. Jan helps lawyers save time while keeping clients safe." +tags: AI, ai for law, ai for lawyers, ChatGPT alternative, Jan, local AI, offline AI +categories: guides +date: 2025-09-30 +ogImage: assets/images/general/jan-for-ai-law-assistant-chat.jpeg +twitter: + card: summary_large_image + site: "@jandotai" + title: "Private AI for legal professionals who need confidentiality" + description: "It's possible to use AI without risking client data. Jan helps lawyers save time while keeping clients safe." + image: assets/images/general/jan-assistants-ai-for-legal.jpeg +--- +import { Callout } from 'nextra/components' +import CTABlog from '@/components/Blog/CTA' +import { OpenAIStatusChecker } from '@/components/OpenAIStatusChecker' + +# Private AI for legal professionals who need confidentiality + +![AI for Law](/assets/images/general/jan-for-ai-law-assistant-chat.jpeg) + +Yes, it's possible to use AI in legal work without risking client data. + + +Client trust depends on privacy. Sending documents into public AI tools risks compliance and reputation. + + +Start by [downloading Jan](/download) and installing the **Jan v1 model**. Once installed, you can create assistants tailored to your practice and keep contracts, case notes, and client files under your control. + + +**Why use Jan for legal tasks** +- Runs locally on your hardware, no cloud uploads +- Keeps chats and interactions private +- Works offline once installed +- Lets you build assistants for your own workflows + + +--- + +## Create your assistant + +Once Jan is installed with the **Jan v1 model**, onboarding will guide you through downloading and setup. + +Click **Create assistant** to start: +![Create your first AI assistant in Jan](./_assets/create-assistant-1.jpeg) +*Create your first assistant in Jan* + +Add an assistant name and prompt: +![Jan assistant for contract review](./_assets/jan-assistant-for-law.png) +*Example of a Jan assistant for contract review* + +You can create assistants using specific prompts. Below are examples for common legal workflows. + +--- + +## Contract review assistant + +AI can help lawyers move faster through long contracts by pointing out what matters most. + +**Prompt for Jan:** +> You are a contract review assistant. +> When I paste a contract: +> - Highlight risky or unusual clauses +> - Flag ambiguous or missing terms +> - Summarize the agreement in plain English for a non-lawyer client +> Format your response with sections: **Risks**, **Ambiguities/Missing**, **Summary**. +> Do not provide legal advice. + +--- + +## Drafting assistant + +Use AI to create first drafts of NDAs, service agreements, or client letters. You still refine the output, but AI saves time on boilerplate. + +**Prompt for Jan:** +> You are a drafting assistant. +> When asked to draft a legal agreement or client letter: +> - Produce a professional first version +> - Use clear, concise language +> - Leave placeholders like [Party Name], [Date], [Amount] for details +> - Structure output with headings, numbered clauses, and consistent formatting +> Do not provide legal advice. + +--- + +## Case preparation assistant + +Case prep often means reading hundreds of pages. AI can turn depositions, discovery files, or judgments into concise notes. + +![Jan legal case preparation assistant](./_assets/jan-for-ai-law-assistant-chat.jpeg) +*Jan chat interface for case preparation — process documents and extract key information* + +**Prompt for Jan:** +> You are a case preparation assistant. +> When I provide case materials: +> - Extract key facts, issues, and arguments +> - Present them as bullet points under headings: **Facts**, **Issues**, **Arguments** +> - Keep summaries concise (under 500 words unless I request more) +> Use plain English, no speculation or legal conclusions. + +--- + +## Knowledge management assistant + +Law firms accumulate memos, policies, and precedents. AI can help organize and retrieve them quickly. + +**Prompt for Jan:** +> You are a knowledge management assistant. +> When I ask questions about internal documents: +> - Return concise summaries or direct excerpts +> - Always cite the source (e.g., “Policy Manual, Section 4”) +> - If not found in provided material, reply “Not found in documents.” +> Do not invent information. + +--- + +## Final note + +AI in legal practice is not about replacing lawyers. It’s about handling repetitive tasks safely so you can focus on real decisions. +With private AI, you gain efficiency without compromising client confidentiality. + + diff --git a/docs/src/pages/post/ai-for-teachers.mdx b/docs/src/pages/post/ai-for-teachers.mdx new file mode 100644 index 000000000..cd2c2b60b --- /dev/null +++ b/docs/src/pages/post/ai-for-teachers.mdx @@ -0,0 +1,134 @@ +--- +title: "AI for teachers who care about student privacy" +description: "Use AI in teaching without risking student data. Jan helps teachers plan lessons, grade faster, and communicate with parents." +tags: AI, ai for teachers, ChatGPT alternative, Jan, local AI, offline AI, education +categories: guides +date: 2025-10-01 +ogImage: assets/images/general/ai-for-teacher.jpeg +twitter: + card: summary_large_image + site: "@jandotai" + title: "AI for teachers who care about student privacy" + description: "Use AI in teaching without risking student data. Jan helps teachers plan lessons, grade faster, and communicate with parents." + image: assets/images/general/ai-for-teacher.jpeg +--- +import { Callout } from 'nextra/components' +import CTABlog from '@/components/Blog/CTA' + +# AI for teachers who care about student privacy + +![AI for teachers](/assets/images/general/ai-for-teacher.jpeg) + +AI can help teachers handle the work that piles up outside class. It can draft a lesson outline, suggest feedback on essays, or turn notes into a polite parent email. These are the tasks that usually stretch into evenings and weekends. + + +Most AI tools like ChatGPT run in the cloud. Sharing lesson plans, student writing, or parent details there risks compliance and trust. + + +That's where Jan comes in: +- [Download Jan](/download) +- You get the same time-saving help +- Your data never leaves your device. + + +*See how teachers use Jan for AI-powered lesson planning and grading* + + +**Why use Jan for teaching** +- Runs locally, no cloud servers +- Keeps lesson plans and student data private +- Works offline once installed +- Lets you build assistants for your daily teaching tasks + + +--- + +## Create your assistant + +Once Jan is installed, click **Create assistant** and add one of the prompts below. Each assistant is for a specific classroom task. + +![Create your first AI assistant in Jan](/assets/images/general/assistants-ai-for-teachers.jpeg) + +--- + +## Lesson planning assistant + +AI can draft lesson outlines in minutes. You adapt and refine them for your students. + +**Prompt for Jan:** +> You are a lesson planning assistant. +> When I give you a topic or subject: +> - Suggest a lesson outline with objectives, activities, and discussion questions +> - Adjust for different grade levels if I specify +> - Keep plans practical and realistic for a classroom setting + +Example ask: For Grade 6 science on ecosystems. Objectives: define food chains, explain producer/consumer roles. Activity: group poster on an ecosystem. Questions: How would removing one species affect the whole system? + +--- + +## Grading support assistant + +AI won't replace your judgment, but it can make feedback faster and more consistent. + +**Prompt for Jan:** +> You are a grading support assistant. +> When I paste student writing or answers: +> - Highlight strengths and areas for improvement +> - Suggest short, constructive feedback I can reuse +> - Keep tone supportive and professional +> Do not assign final grades. + +Example: For a history essay. Strength: clear thesis. Improvement: weak evidence. Feedback: "Great thesis and structure. Next time, support your points with specific historical examples." + +--- + +## Parent communication assistant + +Writing parent emails is important but time-consuming. + +**Prompt for Jan:** +> You are a parent communication assistant. +> When I give you key points about a student: +> - Draft a polite and empathetic email to parents +> - Use clear and professional language +> - Keep tone supportive, not overly formal +> Only include details I provide. + +Example: Notes: “Student is falling behind on homework, otherwise engaged in class.” - Output: a short, encouraging message suggesting a check-in at home. + +--- + +## Classroom resources assistant + +Generate quizzes, worksheets, or practice activities at short notice. + +**Prompt for Jan:** +> You are a classroom resource assistant. +> When I provide a topic or subject: +> - Generate sample quiz questions (multiple choice and short answer) +> - Suggest short practice activities +> - Provide answer keys separately +> Keep material age-appropriate for the level I specify. + +Example: For Grade 4 fractions. 5 multiple-choice questions with answer key, plus a quick worksheet with 3 practice problems. + +--- + +## Getting started + +1. [Download Jan](/download). +2. Install the Jan model (guided in-app) +3. Create your first assistant using one of the prompts above +4. Test with non-sensitive examples first +5. Use it in real classroom tasks once you're comfortable + +--- + +## Final note + +AI isn't here to replace teachers. It's here to take repetitive tasks off your plate so you can focus on teaching. With Jan, you can use AI confidently without risking student privacy. + + diff --git a/docs/src/pages/post/is-chatgpt-down-use-jan.mdx b/docs/src/pages/post/is-chatgpt-down-use-jan.mdx index f7e82cef8..dfd9843fd 100644 --- a/docs/src/pages/post/is-chatgpt-down-use-jan.mdx +++ b/docs/src/pages/post/is-chatgpt-down-use-jan.mdx @@ -18,7 +18,7 @@ import { OpenAIStatusChecker } from '@/components/OpenAIStatusChecker' # If ChatGPT is down, switch to AI that never goes down -If you're seeing ChatGPT is down, it could a good signal to switch to [Jan](https://www.jan.ai/), AI that never goes down. +If you're seeing ChatGPT is down, it could be a good signal to switch to [Jan](https://www.jan.ai/), AI that never goes down. ## 🔴 Realtime Status: Is ChatGPT down? @@ -108,17 +108,17 @@ When ChatGPT experiences issues, you might see these error messages: ## Quick answers about ChatGPT status -### Is ChatGPT down today? -Check the ChatGPT realtime status above. If ChatGPT is down, you'll see it here. +### Is ChatGPT down? +Check the ChatGPT realtime status above. [See if ChatGPT is down right now.](http://localhost:3000/post/is-chatgpt-down-use-jan#-realtime-status-is-chatgpt-down) ### Why is ChatGPT down? Usually server overload, maintenance, or outages at OpenAI. -### What does “ChatGPT is at capacity” mean? +### What does "ChatGPT is at capacity" mean? Too many users are online at the same time. You’ll need to wait or switch to Jan instead. ### Is ChatGPT shutting down? -No, ChatGPT isn’t shutting down. Outages are temporary. +No, ChatGPT isn't shutting down. Outages are temporary. ### Can I use ChatGPT offline? No. ChatGPT always requires internet. For [offline AI](https://www.jan.ai/post/offline-chatgpt-alternative), use [Jan](https://jan.ai). \ No newline at end of file diff --git a/extensions-web/package.json b/extensions-web/package.json index 232ba13fa..aa536e9fe 100644 --- a/extensions-web/package.json +++ b/extensions-web/package.json @@ -22,6 +22,9 @@ }, "devDependencies": { "@janhq/core": "workspace:*", + "@tabler/icons-react": "^3.34.0", + "@types/react": "19.1.2", + "react": "19.0.0", "typescript": "5.9.2", "vite": "5.4.20", "vitest": "2.1.9", @@ -29,6 +32,8 @@ }, "peerDependencies": { "@janhq/core": "*", + "@tabler/icons-react": "*", + "react": "19.0.0", "zustand": "5.0.3" }, "dependencies": { diff --git a/extensions-web/src/conversational-web/const.ts b/extensions-web/src/conversational-web/const.ts index 0ad7e9049..4a860c500 100644 --- a/extensions-web/src/conversational-web/const.ts +++ b/extensions-web/src/conversational-web/const.ts @@ -14,4 +14,4 @@ export const DEFAULT_ASSISTANT = { name: 'Jan', avatar: '👋', created_at: 1747029866.542, -} \ No newline at end of file +} diff --git a/extensions-web/src/jan-provider-web/api.ts b/extensions-web/src/jan-provider-web/api.ts index 4c7bf1af6..97a9608f2 100644 --- a/extensions-web/src/jan-provider-web/api.ts +++ b/extensions-web/src/jan-provider-web/api.ts @@ -268,4 +268,4 @@ export class JanApiClient { } } -export const janApiClient = JanApiClient.getInstance() \ No newline at end of file +export const janApiClient = JanApiClient.getInstance() diff --git a/extensions-web/src/jan-provider-web/index.ts b/extensions-web/src/jan-provider-web/index.ts index 70cbf7770..4d3a4008a 100644 --- a/extensions-web/src/jan-provider-web/index.ts +++ b/extensions-web/src/jan-provider-web/index.ts @@ -1 +1 @@ -export { default } from './provider' \ No newline at end of file +export { default } from './provider' diff --git a/extensions-web/src/jan-provider-web/store.ts b/extensions-web/src/jan-provider-web/store.ts index 02cc70686..2ff341147 100644 --- a/extensions-web/src/jan-provider-web/store.ts +++ b/extensions-web/src/jan-provider-web/store.ts @@ -92,4 +92,4 @@ export const janProviderStore = { useJanProviderStore.getState().clearError(), reset: () => useJanProviderStore.getState().reset(), -} \ No newline at end of file +} diff --git a/extensions-web/src/mcp-web/components/WebSearchButton.tsx b/extensions-web/src/mcp-web/components/WebSearchButton.tsx new file mode 100644 index 000000000..86fa08906 --- /dev/null +++ b/extensions-web/src/mcp-web/components/WebSearchButton.tsx @@ -0,0 +1,54 @@ +import { useMemo, useCallback } from 'react' +import { IconWorld } from '@tabler/icons-react' +import { MCPToolComponentProps } from '@janhq/core' + +// List of tool names considered as web search tools +const WEB_SEARCH_TOOL_NAMES = ['google_search', 'scrape']; + +export const WebSearchButton = ({ + tools, + isToolEnabled, + onToolToggle, +}: MCPToolComponentProps) => { + const webSearchTools = useMemo( + () => tools.filter((tool) => WEB_SEARCH_TOOL_NAMES.includes(tool.name)), + [tools] + ) + + // Early return if no web search tools available + if (webSearchTools.length === 0) { + return null + } + + // Check if all web search tools are enabled + const isEnabled = useMemo( + () => webSearchTools.every((tool) => isToolEnabled(tool.name)), + [webSearchTools, isToolEnabled] + ) + + const handleToggle = useCallback(() => { + // Toggle all web search tools at once + const newState = !isEnabled + webSearchTools.forEach((tool) => { + onToolToggle(tool.name, newState) + }) + }, [isEnabled, webSearchTools, onToolToggle]) + + return ( + + ) +} diff --git a/extensions-web/src/mcp-web/components/index.ts b/extensions-web/src/mcp-web/components/index.ts new file mode 100644 index 000000000..7f9bc47da --- /dev/null +++ b/extensions-web/src/mcp-web/components/index.ts @@ -0,0 +1 @@ +export { WebSearchButton } from './WebSearchButton' diff --git a/extensions-web/src/mcp-web/index.ts b/extensions-web/src/mcp-web/index.ts index 5e13846a7..3d588753f 100644 --- a/extensions-web/src/mcp-web/index.ts +++ b/extensions-web/src/mcp-web/index.ts @@ -4,11 +4,13 @@ * Uses official MCP TypeScript SDK with proper session handling */ -import { MCPExtension, MCPTool, MCPToolCallResult } from '@janhq/core' +import { MCPExtension, MCPTool, MCPToolCallResult, MCPToolComponentProps } from '@janhq/core' import { getSharedAuthService, JanAuthService } from '../shared' import { Client } from '@modelcontextprotocol/sdk/client/index.js' import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js' import { JanMCPOAuthProvider } from './oauth-provider' +import { WebSearchButton } from './components' +import type { ComponentType } from 'react' // JAN_API_BASE is defined in vite.config.ts (defaults to 'https://api-dev.jan.ai/jan/v1') declare const JAN_API_BASE: string @@ -232,4 +234,27 @@ export default class MCPExtensionWeb extends MCPExtension { throw error } } -} \ No newline at end of file + + /** + * Provides a custom UI component for web search tools + * @returns The WebSearchButton component + */ + getToolComponent(): ComponentType | null { + return WebSearchButton + } + + /** + * Returns the list of tool names that should be disabled by default for new users + * All MCP web tools are disabled by default to prevent accidental API usage + * @returns Array of tool names to disable by default + */ + async getDefaultDisabledTools(): Promise { + try { + const tools = await this.getTools() + return tools.map(tool => tool.name) + } catch (error) { + console.error('Failed to get default disabled tools:', error) + return [] + } + } +} diff --git a/extensions-web/src/mcp-web/oauth-provider.ts b/extensions-web/src/mcp-web/oauth-provider.ts index fd37c3ece..7d14264d6 100644 --- a/extensions-web/src/mcp-web/oauth-provider.ts +++ b/extensions-web/src/mcp-web/oauth-provider.ts @@ -57,4 +57,4 @@ export class JanMCPOAuthProvider implements OAuthClientProvider { async codeVerifier(): Promise { throw new Error('Code verifier not supported') } -} \ No newline at end of file +} diff --git a/extensions-web/src/shared/types/errors.ts b/extensions-web/src/shared/types/errors.ts index 7e6917faa..650507a7b 100644 --- a/extensions-web/src/shared/types/errors.ts +++ b/extensions-web/src/shared/types/errors.ts @@ -47,4 +47,4 @@ export class ApiError extends Error { isServerError(): boolean { return this.status >= 500 && this.status < 600 } -} \ No newline at end of file +} diff --git a/extensions-web/src/types.ts b/extensions-web/src/types.ts index 47ef0be71..4d52032c9 100644 --- a/extensions-web/src/types.ts +++ b/extensions-web/src/types.ts @@ -38,4 +38,4 @@ export interface IndexedDBConfig { keyPath: string indexes?: { name: string; keyPath: string | string[]; unique?: boolean }[] }[] -} \ No newline at end of file +} diff --git a/extensions-web/src/types/global.d.ts b/extensions-web/src/types/global.d.ts index a6e82d759..8d70d398b 100644 --- a/extensions-web/src/types/global.d.ts +++ b/extensions-web/src/types/global.d.ts @@ -2,4 +2,4 @@ export {} declare global { declare const JAN_API_BASE: string -} \ No newline at end of file +} diff --git a/extensions-web/src/vite-env.d.ts b/extensions-web/src/vite-env.d.ts index 151aa6856..11f02fe2a 100644 --- a/extensions-web/src/vite-env.d.ts +++ b/extensions-web/src/vite-env.d.ts @@ -1 +1 @@ -/// \ No newline at end of file +/// diff --git a/extensions-web/tsconfig.json b/extensions-web/tsconfig.json index e90dd4997..b39b50ee5 100644 --- a/extensions-web/tsconfig.json +++ b/extensions-web/tsconfig.json @@ -3,6 +3,7 @@ "target": "ES2020", "module": "ESNext", "moduleResolution": "bundler", + "jsx": "react-jsx", "allowSyntheticDefaultImports": true, "esModuleInterop": true, "strict": true, diff --git a/extensions-web/vite.config.ts b/extensions-web/vite.config.ts index 89cfb7d0e..8c144b0ab 100644 --- a/extensions-web/vite.config.ts +++ b/extensions-web/vite.config.ts @@ -9,11 +9,11 @@ export default defineConfig({ fileName: 'index' }, rollupOptions: { - external: ['@janhq/core', 'zustand'] + external: ['@janhq/core', 'zustand', 'react', 'react-dom', 'react/jsx-runtime', '@tabler/icons-react'] }, emptyOutDir: false // Don't clean the output directory }, define: { JAN_API_BASE: JSON.stringify(process.env.JAN_API_BASE || 'https://api-dev.jan.ai/v1'), } -}) \ No newline at end of file +}) diff --git a/scripts/download-bin.mjs b/scripts/download-bin.mjs index 39eb9ae87..68f09bf5f 100644 --- a/scripts/download-bin.mjs +++ b/scripts/download-bin.mjs @@ -106,13 +106,11 @@ async function main() { } // Adjust these URLs based on latest releases - const bunVersion = '1.2.10' // Example Bun version - const bunUrl = `https://github.com/oven-sh/bun/releases/download/bun-v${bunVersion}/bun-${bunPlatform}.zip` + const bunUrl = `https://github.com/oven-sh/bun/releases/latest/download/bun-${bunPlatform}.zip` - const uvVersion = '0.6.17' // Example UV version - let uvUrl = `https://github.com/astral-sh/uv/releases/download/${uvVersion}/uv-${uvPlatform}.tar.gz` + let uvUrl = `https://github.com/astral-sh/uv/releases/latest/download/uv-${uvPlatform}.tar.gz` if (platform === 'win32') { - uvUrl = `https://github.com/astral-sh/uv/releases/download/${uvVersion}/uv-${uvPlatform}.zip` + uvUrl = `https://github.com/astral-sh/uv/releases/latest/download/uv-${uvPlatform}.zip` } console.log(`Downloading Bun for ${bunPlatform}...`) diff --git a/src-tauri/plugins/tauri-plugin-llamacpp/src/gguf/utils.rs b/src-tauri/plugins/tauri-plugin-llamacpp/src/gguf/utils.rs index 50e3f4a14..cdbbf92d5 100644 --- a/src-tauri/plugins/tauri-plugin-llamacpp/src/gguf/utils.rs +++ b/src-tauri/plugins/tauri-plugin-llamacpp/src/gguf/utils.rs @@ -62,6 +62,7 @@ pub async fn estimate_kv_cache_internal( ctx_size: Option, ) -> Result { log::info!("Received ctx_size parameter: {:?}", ctx_size); + log::info!("Received model metadata:\n{:?}", &meta); let arch = meta .get("general.architecture") .ok_or(KVCacheError::ArchitectureNotFound)?; @@ -94,15 +95,43 @@ pub async fn estimate_kv_cache_internal( let key_len_key = format!("{}.attention.key_length", arch); let val_len_key = format!("{}.attention.value_length", arch); - let key_len = meta + let mut key_len = meta .get(&key_len_key) .and_then(|s| s.parse::().ok()) .unwrap_or(0); - let val_len = meta + let mut val_len = meta .get(&val_len_key) .and_then(|s| s.parse::().ok()) .unwrap_or(0); + // Fallback: calculate from embedding_length if key/val lengths not found + if key_len == 0 || val_len == 0 { + let emb_len_key = format!("{}.embedding_length", arch); + let emb_len = meta + .get(&emb_len_key) + .and_then(|s| s.parse::().ok()) + .unwrap_or(0); + + if emb_len > 0 && n_head > 0 { + // For most transformers: head_dim = embedding_length / total_heads + let total_heads = meta + .get(&n_head_key) + .and_then(|s| s.parse::().ok()) + .unwrap_or(n_head); + + let head_dim = emb_len / total_heads; + key_len = head_dim; + val_len = head_dim; + + log::info!( + "Calculated key_len and val_len from embedding_length: {} / {} heads = {} per head", + emb_len, + total_heads, + head_dim + ); + } + } + if key_len == 0 || val_len == 0 { return Err(KVCacheError::EmbeddingLengthInvalid); } diff --git a/src-tauri/src/core/mcp/helpers.rs b/src-tauri/src/core/mcp/helpers.rs index 7f8565e39..48c92ba2c 100644 --- a/src-tauri/src/core/mcp/helpers.rs +++ b/src-tauri/src/core/mcp/helpers.rs @@ -627,17 +627,28 @@ async fn schedule_mcp_start_task( } } else { let mut cmd = Command::new(config_params.command.clone()); - let bun_x_path = format!("{}/bun", bin_path.display()); - if config_params.command.clone() == "npx" && can_override_npx(bun_x_path.clone()) { + let bun_x_path = if cfg!(windows) { + bin_path.join("bun.exe") + } else { + bin_path.join("bun") + }; + if config_params.command.clone() == "npx" + && can_override_npx(bun_x_path.display().to_string()) + { let mut cache_dir = app_path.clone(); cache_dir.push(".npx"); - cmd = Command::new(bun_x_path); + cmd = Command::new(bun_x_path.display().to_string()); cmd.arg("x"); cmd.env("BUN_INSTALL", cache_dir.to_str().unwrap().to_string()); } - let uv_path = format!("{}/uv", bin_path.display()); - if config_params.command.clone() == "uvx" && can_override_uvx(uv_path.clone()) { + let uv_path = if cfg!(windows) { + bin_path.join("uv.exe") + } else { + bin_path.join("uv") + }; + if config_params.command.clone() == "uvx" && can_override_uvx(uv_path.display().to_string()) + { let mut cache_dir = app_path.clone(); cache_dir.push(".uvx"); cmd = Command::new(uv_path); @@ -935,3 +946,47 @@ pub async fn should_restart_server( } } } + +// Add a new server configuration to the MCP config file +pub fn add_server_config( + app_handle: tauri::AppHandle, + server_key: String, + server_value: Value, +) -> Result<(), String> { + add_server_config_with_path(app_handle, server_key, server_value, None) +} + +// Add a new server configuration to the MCP config file with custom path support +pub fn add_server_config_with_path( + app_handle: tauri::AppHandle, + server_key: String, + server_value: Value, + config_filename: Option<&str>, +) -> Result<(), String> { + let config_filename = config_filename.unwrap_or("mcp_config.json"); + let config_path = get_jan_data_folder_path(app_handle).join(config_filename); + + let mut config: Value = serde_json::from_str( + &std::fs::read_to_string(&config_path) + .map_err(|e| format!("Failed to read config file: {e}"))?, + ) + .map_err(|e| format!("Failed to parse config: {e}"))?; + + config + .as_object_mut() + .ok_or("Config root is not an object")? + .entry("mcpServers") + .or_insert_with(|| Value::Object(serde_json::Map::new())) + .as_object_mut() + .ok_or("mcpServers is not an object")? + .insert(server_key, server_value); + + std::fs::write( + &config_path, + serde_json::to_string_pretty(&config) + .map_err(|e| format!("Failed to serialize config: {e}"))?, + ) + .map_err(|e| format!("Failed to write config file: {e}"))?; + + Ok(()) +} diff --git a/src-tauri/src/core/mcp/tests.rs b/src-tauri/src/core/mcp/tests.rs index 081a188e8..d973ce647 100644 --- a/src-tauri/src/core/mcp/tests.rs +++ b/src-tauri/src/core/mcp/tests.rs @@ -1,9 +1,10 @@ -use super::helpers::run_mcp_commands; +use super::helpers::{add_server_config, add_server_config_with_path, run_mcp_commands}; use crate::core::app::commands::get_jan_data_folder_path; use crate::core::state::SharedMcpServers; use std::collections::HashMap; use std::fs::File; use std::io::Write; +use std::path::PathBuf; use std::sync::Arc; use tauri::test::mock_app; use tokio::sync::Mutex; @@ -27,8 +28,7 @@ async fn test_run_mcp_commands() { .expect("Failed to write to config file"); // Call the run_mcp_commands function - let servers_state: SharedMcpServers = - Arc::new(Mutex::new(HashMap::new())); + let servers_state: SharedMcpServers = Arc::new(Mutex::new(HashMap::new())); let result = run_mcp_commands(app.handle(), servers_state).await; // Assert that the function returns Ok(()) @@ -37,3 +37,188 @@ async fn test_run_mcp_commands() { // Clean up the mock config file std::fs::remove_file(&config_path).expect("Failed to remove config file"); } + +#[test] +fn test_add_server_config_new_file() { + let app = mock_app(); + let app_path = get_jan_data_folder_path(app.handle().clone()); + let config_path = app_path.join("mcp_config_test_new.json"); + + // Ensure the directory exists + if let Some(parent) = config_path.parent() { + std::fs::create_dir_all(parent).expect("Failed to create parent directory"); + } + + // Create initial config file with empty mcpServers + let mut file = File::create(&config_path).expect("Failed to create config file"); + file.write_all(b"{\"mcpServers\":{}}") + .expect("Failed to write to config file"); + drop(file); + + // Test adding a new server config + let server_value = serde_json::json!({ + "command": "npx", + "args": ["-y", "test-server"], + "env": { "TEST_API_KEY": "test_key" }, + "active": false + }); + + let result = add_server_config_with_path( + app.handle().clone(), + "test_server".to_string(), + server_value.clone(), + Some("mcp_config_test_new.json"), + ); + + assert!(result.is_ok(), "Failed to add server config: {:?}", result); + + // Verify the config was added correctly + let config_content = std::fs::read_to_string(&config_path) + .expect("Failed to read config file"); + let config: serde_json::Value = serde_json::from_str(&config_content) + .expect("Failed to parse config"); + + assert!(config["mcpServers"]["test_server"].is_object()); + assert_eq!(config["mcpServers"]["test_server"]["command"], "npx"); + assert_eq!(config["mcpServers"]["test_server"]["args"][0], "-y"); + assert_eq!(config["mcpServers"]["test_server"]["args"][1], "test-server"); + + // Clean up + std::fs::remove_file(&config_path).expect("Failed to remove config file"); +} + +#[test] +fn test_add_server_config_existing_servers() { + let app = mock_app(); + let app_path = get_jan_data_folder_path(app.handle().clone()); + let config_path = app_path.join("mcp_config_test_existing.json"); + + // Ensure the directory exists + if let Some(parent) = config_path.parent() { + std::fs::create_dir_all(parent).expect("Failed to create parent directory"); + } + + // Create config file with existing server + let initial_config = serde_json::json!({ + "mcpServers": { + "existing_server": { + "command": "existing_command", + "args": ["arg1"], + "active": true + } + } + }); + + let mut file = File::create(&config_path).expect("Failed to create config file"); + file.write_all(serde_json::to_string_pretty(&initial_config).unwrap().as_bytes()) + .expect("Failed to write to config file"); + drop(file); + + // Add new server + let new_server_value = serde_json::json!({ + "command": "new_command", + "args": ["new_arg"], + "active": false + }); + + let result = add_server_config_with_path( + app.handle().clone(), + "new_server".to_string(), + new_server_value, + Some("mcp_config_test_existing.json"), + ); + + assert!(result.is_ok(), "Failed to add server config: {:?}", result); + + // Verify both servers exist + let config_content = std::fs::read_to_string(&config_path) + .expect("Failed to read config file"); + let config: serde_json::Value = serde_json::from_str(&config_content) + .expect("Failed to parse config"); + + // Check existing server is still there + assert!(config["mcpServers"]["existing_server"].is_object()); + assert_eq!(config["mcpServers"]["existing_server"]["command"], "existing_command"); + + // Check new server was added + assert!(config["mcpServers"]["new_server"].is_object()); + assert_eq!(config["mcpServers"]["new_server"]["command"], "new_command"); + + // Clean up + std::fs::remove_file(&config_path).expect("Failed to remove config file"); +} + +#[test] +fn test_add_server_config_missing_config_file() { + let app = mock_app(); + let app_path = get_jan_data_folder_path(app.handle().clone()); + + // Ensure the directory exists + if let Some(parent) = app_path.parent() { + std::fs::create_dir_all(parent).ok(); + } + std::fs::create_dir_all(&app_path).ok(); + + let config_path = app_path.join("mcp_config.json"); + + // Ensure the file doesn't exist + if config_path.exists() { + std::fs::remove_file(&config_path).ok(); + } + + let server_value = serde_json::json!({ + "command": "test", + "args": [], + "active": false + }); + + let result = add_server_config( + app.handle().clone(), + "test".to_string(), + server_value, + ); + + assert!(result.is_err(), "Expected error when config file doesn't exist"); + assert!(result.unwrap_err().contains("Failed to read config file")); +} + +#[cfg(not(target_os = "windows"))] +#[test] +fn test_bin_path_construction_with_join() { + // Test that PathBuf::join properly constructs paths + let bin_path = PathBuf::from("/usr/local/bin"); + let bun_path = bin_path.join("bun"); + + assert_eq!(bun_path.to_string_lossy(), "/usr/local/bin/bun"); + + // Test conversion to String via display() + let bun_path_str = bun_path.display().to_string(); + assert_eq!(bun_path_str, "/usr/local/bin/bun"); +} + +#[cfg(not(target_os = "windows"))] +#[test] +fn test_uv_path_construction_with_join() { + // Test that PathBuf::join properly constructs paths for uv + let bin_path = PathBuf::from("/usr/local/bin"); + let uv_path = bin_path.join("uv"); + + assert_eq!(uv_path.to_string_lossy(), "/usr/local/bin/uv"); + + // Test conversion to String via display() + let uv_path_str = uv_path.display().to_string(); + assert_eq!(uv_path_str, "/usr/local/bin/uv"); +} + +#[cfg(target_os = "windows")] +#[test] +fn test_bin_path_construction_windows() { + // Test Windows-style paths + let bin_path = PathBuf::from(r"C:\Program Files\bin"); + let bun_path = bin_path.join("bun.exe"); + + assert_eq!(bun_path.to_string_lossy(), r"C:\Program Files\bin\bun.exe"); + + let bun_path_str = bun_path.display().to_string(); + assert_eq!(bun_path_str, r"C:\Program Files\bin\bun.exe"); +} diff --git a/src-tauri/src/core/setup.rs b/src-tauri/src/core/setup.rs index 301378695..38eca440e 100644 --- a/src-tauri/src/core/setup.rs +++ b/src-tauri/src/core/setup.rs @@ -3,10 +3,11 @@ use std::{ fs::{self, File}, io::Read, path::PathBuf, + sync::Arc, }; use tar::Archive; use tauri::{ - App, Emitter, Manager, Runtime, + App, Emitter, Manager, Runtime, Wry }; #[cfg(desktop)] @@ -14,32 +15,15 @@ use tauri::{ menu::{Menu, MenuItem, PredefinedMenuItem}, tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent}, }; -use tauri_plugin_store::StoreExt; -// use tokio::sync::Mutex; -// use tokio::time::{sleep, Duration}; // Using tokio::sync::Mutex -// // MCP +use tauri_plugin_store::Store; + +use crate::core::mcp::helpers::add_server_config; -// MCP use super::{ - app::commands::get_jan_data_folder_path, extensions::commands::get_jan_extensions_path, - mcp::helpers::run_mcp_commands, state::AppState, + extensions::commands::get_jan_extensions_path, mcp::helpers::run_mcp_commands, state::AppState, }; pub fn install_extensions(app: tauri::AppHandle, force: bool) -> Result<(), String> { - let mut store_path = get_jan_data_folder_path(app.clone()); - store_path.push("store.json"); - let store = app.store(store_path).expect("Store not initialized"); - let stored_version = store - .get("version") - .and_then(|v| v.as_str().map(String::from)) - .unwrap_or_default(); - - let app_version = app - .config() - .version - .clone() - .unwrap_or_else(|| "".to_string()); - let extensions_path = get_jan_extensions_path(app.clone()); let pre_install_path = app .path() @@ -54,13 +38,8 @@ pub fn install_extensions(app: tauri::AppHandle, force: bool) -> if std::env::var("IS_CLEAN").is_ok() { clean_up = true; } - log::info!( - "Installing extensions. Clean up: {}, Stored version: {}, App version: {}", - clean_up, - stored_version, - app_version - ); - if !clean_up && stored_version == app_version && extensions_path.exists() { + log::info!("Installing extensions. Clean up: {}", clean_up); + if !clean_up && extensions_path.exists() { return Ok(()); } @@ -164,10 +143,36 @@ pub fn install_extensions(app: tauri::AppHandle, force: bool) -> ) .map_err(|e| e.to_string())?; - // Store the new app version - store.set("version", serde_json::json!(app_version)); - store.save().expect("Failed to save store"); + Ok(()) +} +// Migrate MCP servers configuration +pub fn migrate_mcp_servers( + app_handle: tauri::AppHandle, + store: Arc>, +) -> Result<(), String> { + let mcp_version = store + .get("mcp_version") + .and_then(|v| v.as_i64()) + .unwrap_or_else(|| 0); + if mcp_version < 1 { + log::info!("Migrating MCP schema version 1"); + let result = add_server_config( + app_handle, + "exa".to_string(), + serde_json::json!({ + "command": "npx", + "args": ["-y", "exa-mcp-server"], + "env": { "EXA_API_KEY": "YOUR_EXA_API_KEY_HERE" }, + "active": false + }), + ); + if let Err(e) = result { + log::error!("Failed to add server config: {}", e); + } + } + store.set("mcp_version", 1); + store.save().expect("Failed to save store"); Ok(()) } diff --git a/src-tauri/src/core/threads/commands.rs b/src-tauri/src/core/threads/commands.rs index 905c8c6b8..44ac1964d 100644 --- a/src-tauri/src/core/threads/commands.rs +++ b/src-tauri/src/core/threads/commands.rs @@ -150,6 +150,9 @@ pub async fn create_message( let data = serde_json::to_string(&message).map_err(|e| e.to_string())?; writeln!(file, "{}", data).map_err(|e| e.to_string())?; + + // Explicitly flush to ensure data is written before returning + file.flush().map_err(|e| e.to_string())?; } Ok(message) diff --git a/src-tauri/src/core/threads/mod.rs b/src-tauri/src/core/threads/mod.rs index fb76bee8c..25225d538 100644 --- a/src-tauri/src/core/threads/mod.rs +++ b/src-tauri/src/core/threads/mod.rs @@ -13,7 +13,6 @@ pub mod commands; mod constants; pub mod helpers; -pub mod models; pub mod utils; #[cfg(test)] diff --git a/src-tauri/src/core/threads/models.rs b/src-tauri/src/core/threads/models.rs deleted file mode 100644 index 5038c6def..000000000 --- a/src-tauri/src/core/threads/models.rs +++ /dev/null @@ -1,103 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Thread { - pub id: String, - pub object: String, - pub title: String, - pub assistants: Vec, - pub created: i64, - pub updated: i64, - pub metadata: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct ThreadMessage { - pub id: String, - pub object: String, - pub thread_id: String, - pub assistant_id: Option, - pub attachments: Option>, - pub role: String, - pub content: Vec, - pub status: String, - pub created_at: i64, - pub completed_at: i64, - pub metadata: Option, - pub type_: Option, - pub error_code: Option, - pub tool_call_id: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Attachment { - pub file_id: Option, - pub tools: Option>, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(tag = "type")] -pub enum Tool { - #[serde(rename = "file_search")] - FileSearch, - #[serde(rename = "code_interpreter")] - CodeInterpreter, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct ThreadContent { - pub type_: String, - pub text: Option, - pub image_url: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct ContentValue { - pub value: String, - pub annotations: Vec, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct ImageContentValue { - pub detail: Option, - pub url: Option, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct ThreadAssistantInfo { - pub id: String, - pub name: String, - pub model: ModelInfo, - pub instructions: Option, - pub tools: Option>, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct ModelInfo { - pub id: String, - pub name: String, - pub settings: serde_json::Value, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -#[serde(tag = "type")] -pub enum AssistantTool { - #[serde(rename = "code_interpreter")] - CodeInterpreter, - #[serde(rename = "retrieval")] - Retrieval, - #[serde(rename = "function")] - Function { - name: String, - description: Option, - parameters: Option, - }, -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct ThreadState { - pub has_more: bool, - pub waiting_for_response: bool, - pub error: Option, - pub last_message: Option, -} diff --git a/src-tauri/src/core/threads/tests.rs b/src-tauri/src/core/threads/tests.rs index e9f4cbdad..8d3524d06 100644 --- a/src-tauri/src/core/threads/tests.rs +++ b/src-tauri/src/core/threads/tests.rs @@ -88,7 +88,7 @@ async fn test_create_and_list_messages() { let messages = list_messages(app.handle().clone(), thread_id.clone()) .await .unwrap(); - assert!(messages.len() > 0); + assert!(messages.len() > 0, "Expected at least one message, but got none. Thread ID: {}", thread_id); assert_eq!(messages[0]["role"], "user"); // Clean up diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index d7ff7693c..abd12ddb7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -10,6 +10,7 @@ use jan_utils::generate_app_token; use std::{collections::HashMap, sync::Arc}; use tauri::{Emitter, Manager, RunEvent}; use tauri_plugin_llamacpp::cleanup_llama_processes; +use tauri_plugin_store::StoreExt; use tokio::sync::Mutex; #[cfg_attr(all(mobile, any(target_os = "android", target_os = "ios")), tauri::mobile_entry_point)] @@ -134,11 +135,46 @@ pub fn run() { )?; #[cfg(not(any(target_os = "ios", target_os = "android")))] app.handle().plugin(tauri_plugin_updater::Builder::new().build())?; - // Install extensions - if let Err(e) = setup::install_extensions(app.handle().clone(), false) { + + // Start migration + let mut store_path = get_jan_data_folder_path(app.handle().clone()); + store_path.push("store.json"); + let store = app + .handle() + .store(store_path) + .expect("Store not initialized"); + let stored_version = store + .get("version") + .and_then(|v| v.as_str().map(String::from)) + .unwrap_or_default(); + let app_version = app + .config() + .version + .clone() + .unwrap_or_else(|| "".to_string()); + // Migrate extensions + if let Err(e) = + setup::install_extensions(app.handle().clone(), stored_version != app_version) + { log::error!("Failed to install extensions: {}", e); } + // Migrate MCP servers + if let Err(e) = setup::migrate_mcp_servers(app.handle().clone(), store.clone()) { + log::error!("Failed to migrate MCP servers: {}", e); + } + + // Store the new app version + store.set("version", serde_json::json!(app_version)); + store.save().expect("Failed to save store"); + // Migration completed + + #[cfg(desktop)] + if option_env!("ENABLE_SYSTEM_TRAY_ICON").unwrap_or("false") == "true" { + log::info!("Enabling system tray icon"); + let _ = setup::setup_tray(app); + } + #[cfg(all(feature = "deep-link", any(windows, target_os = "linux")))] { use tauri_plugin_deep_link::DeepLinkExt; diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f559d79cc..b0df3fc2f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -76,6 +76,7 @@ } }, "bundle": { + "publisher": "Menlo Research Pte. Ltd.", "active": true, "createUpdaterArtifacts": false, "icon": [ diff --git a/web-app/src/__tests__/i18n.test.ts b/web-app/src/__tests__/i18n.test.ts index 644bc019d..262d93194 100644 --- a/web-app/src/__tests__/i18n.test.ts +++ b/web-app/src/__tests__/i18n.test.ts @@ -49,4 +49,4 @@ describe('i18n module', () => { expect(i18nModule[exportName]).toBeDefined() }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/__tests__/main.test.tsx b/web-app/src/__tests__/main.test.tsx index c105482bf..aec753d56 100644 --- a/web-app/src/__tests__/main.test.tsx +++ b/web-app/src/__tests__/main.test.tsx @@ -76,4 +76,4 @@ describe('main.tsx', () => { await import('../main') }).rejects.toThrow() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/dialog.test.tsx b/web-app/src/components/ui/__tests__/dialog.test.tsx index b4c1f5aab..aeb0cbf52 100644 --- a/web-app/src/components/ui/__tests__/dialog.test.tsx +++ b/web-app/src/components/ui/__tests__/dialog.test.tsx @@ -416,4 +416,4 @@ describe('Dialog Components', () => { expect(screen.getByText('Dialog description')).toHaveAttribute('data-slot', 'dialog-description') expect(screen.getByText('Footer button').closest('div')).toHaveAttribute('data-slot', 'dialog-footer') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/dropdown-menu.test.tsx b/web-app/src/components/ui/__tests__/dropdown-menu.test.tsx index 7b0da6f76..541ae0f93 100644 --- a/web-app/src/components/ui/__tests__/dropdown-menu.test.tsx +++ b/web-app/src/components/ui/__tests__/dropdown-menu.test.tsx @@ -853,4 +853,4 @@ describe('DropdownMenu Components', () => { expect(handleItemClick).toHaveBeenCalledTimes(1) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/dropdrawer.test.tsx b/web-app/src/components/ui/__tests__/dropdrawer.test.tsx index 6203d9f4e..cef88b9d9 100644 --- a/web-app/src/components/ui/__tests__/dropdrawer.test.tsx +++ b/web-app/src/components/ui/__tests__/dropdrawer.test.tsx @@ -530,4 +530,4 @@ describe('DropDrawer Component', () => { expect(trigger).toHaveAttribute('aria-haspopup', 'dialog') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/hover-card.test.tsx b/web-app/src/components/ui/__tests__/hover-card.test.tsx index 71e78cb7f..5e0a39628 100644 --- a/web-app/src/components/ui/__tests__/hover-card.test.tsx +++ b/web-app/src/components/ui/__tests__/hover-card.test.tsx @@ -165,4 +165,4 @@ describe('HoverCard Components', () => { expect(screen.getByText('Hover content')).toBeDefined() }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/input.test.tsx b/web-app/src/components/ui/__tests__/input.test.tsx index 2ae18adad..ddf2fa7db 100644 --- a/web-app/src/components/ui/__tests__/input.test.tsx +++ b/web-app/src/components/ui/__tests__/input.test.tsx @@ -93,4 +93,4 @@ describe('Input', () => { fireEvent.blur(input) expect(handleBlur).toHaveBeenCalledTimes(1) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/popover.test.tsx b/web-app/src/components/ui/__tests__/popover.test.tsx index cec809bb7..b76d1ce04 100644 --- a/web-app/src/components/ui/__tests__/popover.test.tsx +++ b/web-app/src/components/ui/__tests__/popover.test.tsx @@ -436,4 +436,4 @@ describe('Popover Components', () => { }) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/progress.test.tsx b/web-app/src/components/ui/__tests__/progress.test.tsx index daa4b5c05..90a7bc70f 100644 --- a/web-app/src/components/ui/__tests__/progress.test.tsx +++ b/web-app/src/components/ui/__tests__/progress.test.tsx @@ -84,4 +84,4 @@ describe('Progress', () => { // For values over 100, the transform should be positive expect(indicator?.style.transform).toContain('translateX(--50%)') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/radio-group.test.tsx b/web-app/src/components/ui/__tests__/radio-group.test.tsx index a788931d8..1cb85e7c6 100644 --- a/web-app/src/components/ui/__tests__/radio-group.test.tsx +++ b/web-app/src/components/ui/__tests__/radio-group.test.tsx @@ -59,4 +59,4 @@ describe('RadioGroup', () => { expect(screen.getByLabelText('HTTP')).toBeChecked() expect(screen.getByLabelText('SSE')).not.toBeChecked() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/sheet.test.tsx b/web-app/src/components/ui/__tests__/sheet.test.tsx index dc21bbe66..988e512c0 100644 --- a/web-app/src/components/ui/__tests__/sheet.test.tsx +++ b/web-app/src/components/ui/__tests__/sheet.test.tsx @@ -260,4 +260,4 @@ describe('Sheet Components', () => { expect(screen.getByText('Main Content')).toBeInTheDocument() expect(screen.getByText('Close')).toBeInTheDocument() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/skeleton.test.tsx b/web-app/src/components/ui/__tests__/skeleton.test.tsx index 273be182e..39d9535a7 100644 --- a/web-app/src/components/ui/__tests__/skeleton.test.tsx +++ b/web-app/src/components/ui/__tests__/skeleton.test.tsx @@ -61,4 +61,4 @@ describe('Skeleton', () => { expect(skeleton).toHaveClass('w-full') expect(skeleton).toHaveClass('bg-red-500') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/slider.test.tsx b/web-app/src/components/ui/__tests__/slider.test.tsx index 5fd72f766..2a15441b2 100644 --- a/web-app/src/components/ui/__tests__/slider.test.tsx +++ b/web-app/src/components/ui/__tests__/slider.test.tsx @@ -190,4 +190,4 @@ describe('Slider', () => { expect(thumb).toHaveClass('border-accent', 'bg-main-view', 'rounded-full') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/sonner.test.tsx b/web-app/src/components/ui/__tests__/sonner.test.tsx index 72aca5526..8b2fc762d 100644 --- a/web-app/src/components/ui/__tests__/sonner.test.tsx +++ b/web-app/src/components/ui/__tests__/sonner.test.tsx @@ -90,4 +90,4 @@ describe('Toaster Component', () => { expect(toaster).toHaveAttribute('data-rich-colors', 'true') expect(toaster).toHaveAttribute('data-close-button', 'true') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/switch.test.tsx b/web-app/src/components/ui/__tests__/switch.test.tsx index d872dbc11..0db35d716 100644 --- a/web-app/src/components/ui/__tests__/switch.test.tsx +++ b/web-app/src/components/ui/__tests__/switch.test.tsx @@ -189,4 +189,4 @@ describe('Switch', () => { const switchElement = document.querySelector('[data-slot="switch"]') expect(switchElement).toHaveClass('data-[state=unchecked]:bg-main-view-fg/20') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/textarea.test.tsx b/web-app/src/components/ui/__tests__/textarea.test.tsx index 6daf09e4d..806cf9515 100644 --- a/web-app/src/components/ui/__tests__/textarea.test.tsx +++ b/web-app/src/components/ui/__tests__/textarea.test.tsx @@ -113,4 +113,4 @@ describe('Textarea', () => { const textarea = screen.getByRole('textbox') expect(textarea).toHaveAttribute('cols', '50') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/__tests__/tooltip.test.tsx b/web-app/src/components/ui/__tests__/tooltip.test.tsx index 4221751d4..46c68e0b8 100644 --- a/web-app/src/components/ui/__tests__/tooltip.test.tsx +++ b/web-app/src/components/ui/__tests__/tooltip.test.tsx @@ -111,4 +111,4 @@ describe('Tooltip Components', () => { expect(screen.getByText('First')).toBeInTheDocument() expect(screen.getByText('Second')).toBeInTheDocument() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/components/ui/radio-group.tsx b/web-app/src/components/ui/radio-group.tsx index b5fa0593a..e604ac901 100644 --- a/web-app/src/components/ui/radio-group.tsx +++ b/web-app/src/components/ui/radio-group.tsx @@ -39,4 +39,4 @@ const RadioGroupItem = React.forwardRef< }) RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName -export { RadioGroup, RadioGroupItem } \ No newline at end of file +export { RadioGroup, RadioGroupItem } diff --git a/web-app/src/constants/__tests__/windows.test.ts b/web-app/src/constants/__tests__/windows.test.ts index f9d388f98..3f55184e1 100644 --- a/web-app/src/constants/__tests__/windows.test.ts +++ b/web-app/src/constants/__tests__/windows.test.ts @@ -33,4 +33,4 @@ describe('windows constants', () => { expect(value.length).toBeGreaterThan(0) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/constants/chat.ts b/web-app/src/constants/chat.ts index d147ca9e1..e1649049f 100644 --- a/web-app/src/constants/chat.ts +++ b/web-app/src/constants/chat.ts @@ -3,4 +3,4 @@ */ export const TEMPORARY_CHAT_ID = 'temporary-chat' -export const TEMPORARY_CHAT_QUERY_ID = 'temporary-chat' \ No newline at end of file +export const TEMPORARY_CHAT_QUERY_ID = 'temporary-chat' diff --git a/web-app/src/containers/ChatInput.tsx b/web-app/src/containers/ChatInput.tsx index cba580ebd..6e314a0f5 100644 --- a/web-app/src/containers/ChatInput.tsx +++ b/web-app/src/containers/ChatInput.tsx @@ -38,6 +38,9 @@ import { useTools } from '@/hooks/useTools' import { TokenCounter } from '@/components/TokenCounter' import { useMessages } from '@/hooks/useMessages' import { useShallow } from 'zustand/react/shallow' +import { McpExtensionToolLoader } from './McpExtensionToolLoader' +import { ExtensionTypeEnum, MCPExtension } from '@janhq/core' +import { ExtensionManager } from '@/lib/extension' type ChatInputProps = { className?: string @@ -171,6 +174,12 @@ const ChatInput = ({ // Check if there are active MCP servers const hasActiveMCPServers = connectedServers.length > 0 || tools.length > 0 + // Get MCP extension and its custom component + const extensionManager = ExtensionManager.getInstance() + const mcpExtension = extensionManager.get(ExtensionTypeEnum.MCP) + const MCPToolComponent = mcpExtension?.getToolComponent?.() + + const handleSendMesage = (prompt: string) => { if (!selectedModel) { setMessage('Please select a model to start chatting.') @@ -719,60 +728,72 @@ const ChatInput = ({ {selectedModel?.capabilities?.includes('tools') && hasActiveMCPServers && ( - - - + ) : ( + // Use default tools dropdown + + -
{ - setDropdownToolsAvailable(false) - e.stopPropagation() - }} + - { - setDropdownToolsAvailable(isOpen) - if (isOpen) { - setTooltipToolsAvailable(false) - } +
{ + setDropdownToolsAvailable(false) + e.stopPropagation() }} > - {(isOpen, toolsCount) => { - return ( -
- - {toolsCount > 0 && ( -
- - {toolsCount > 99 ? '99+' : toolsCount} - -
- )} -
- ) - }} - -
-
- -

{t('tools')}

-
- - + { + setDropdownToolsAvailable(isOpen) + if (isOpen) { + setTooltipToolsAvailable(false) + } + }} + > + {(isOpen, toolsCount) => { + return ( +
+ + {toolsCount > 0 && ( +
+ + {toolsCount > 99 ? '99+' : toolsCount} + +
+ )} +
+ ) + }} +
+
+
+ +

{t('tools')}

+
+
+
+ ) )} {selectedModel?.capabilities?.includes('web_search') && ( diff --git a/web-app/src/containers/LeftPanel.tsx b/web-app/src/containers/LeftPanel.tsx index 73c98dc09..45532d76c 100644 --- a/web-app/src/containers/LeftPanel.tsx +++ b/web-app/src/containers/LeftPanel.tsx @@ -1,4 +1,4 @@ -import { Link, useRouterState } from '@tanstack/react-router' +import { Link, useRouterState, useNavigate } from '@tanstack/react-router' import { useLeftPanel } from '@/hooks/useLeftPanel' import { cn } from '@/lib/utils' import { @@ -58,6 +58,9 @@ const mainMenus = [ route: route.project, isEnabled: true, }, +] + +const secondaryMenus = [ { title: 'common:assistants', icon: IconClipboardSmile, @@ -82,6 +85,7 @@ const LeftPanel = () => { const open = useLeftPanel((state) => state.open) const setLeftPanel = useLeftPanel((state) => state.setLeftPanel) const { t } = useTranslation() + const navigate = useNavigate() const [searchTerm, setSearchTerm] = useState('') const { isAuthenticated } = useAuth() @@ -213,7 +217,12 @@ const LeftPanel = () => { if (editingProjectKey) { updateFolder(editingProjectKey, name) } else { - addFolder(name) + const newProject = addFolder(name) + // Navigate to the newly created project + navigate({ + to: '/project/$projectId', + params: { projectId: newProject.id }, + }) } setProjectDialogOpen(false) setEditingProjectKey(null) @@ -488,7 +497,7 @@ const LeftPanel = () => { )}
-
+
{favoritedThreads.length > 0 && ( <> @@ -608,6 +617,44 @@ const LeftPanel = () => {
+ + {secondaryMenus.map((menu) => { + if (!menu.isEnabled) { + return null + } + + // Regular menu items must have route and icon + if (!menu.route || !menu.icon) return null + + const isActive = (() => { + // Settings routes + if (menu.route.includes(route.settings.index)) { + return currentPath.includes(route.settings.index) + } + + // Default exact match for other routes + return currentPath === menu.route + })() + return ( + isSmallScreen && setLeftPanel(false)} + data-test-id={`menu-${menu.title}`} + activeOptions={{ exact: true }} + className={cn( + 'flex items-center gap-1.5 cursor-pointer hover:bg-left-panel-fg/10 py-1 px-1 rounded', + isActive && 'bg-left-panel-fg/10' + )} + > + + + {t(menu.title)} + + + ) + })} + {PlatformFeatures[PlatformFeature.AUTHENTICATION] && (
diff --git a/web-app/src/containers/McpExtensionToolLoader.tsx b/web-app/src/containers/McpExtensionToolLoader.tsx new file mode 100644 index 000000000..ccadada6c --- /dev/null +++ b/web-app/src/containers/McpExtensionToolLoader.tsx @@ -0,0 +1,61 @@ +import { ComponentType } from 'react' +import { MCPTool, MCPToolComponentProps } from '@janhq/core' +import { useToolAvailable } from '@/hooks/useToolAvailable' +import { useThreads } from '@/hooks/useThreads' + +interface McpExtensionToolLoaderProps { + tools: MCPTool[] + hasActiveMCPServers: boolean + selectedModelHasTools: boolean + initialMessage?: boolean + MCPToolComponent?: ComponentType | null +} + +export const McpExtensionToolLoader = ({ + tools, + hasActiveMCPServers, + selectedModelHasTools, + initialMessage, + MCPToolComponent, +}: McpExtensionToolLoaderProps) => { + // Get tool management hooks + const { isToolDisabled, setToolDisabledForThread, setDefaultDisabledTools, getDefaultDisabledTools } = useToolAvailable() + const { getCurrentThread } = useThreads() + const currentThread = getCurrentThread() + + // Handle tool toggle for custom component + const handleToolToggle = (toolName: string, enabled: boolean) => { + if (initialMessage) { + const currentDefaults = getDefaultDisabledTools() + if (enabled) { + setDefaultDisabledTools(currentDefaults.filter((name) => name !== toolName)) + } else { + setDefaultDisabledTools([...currentDefaults, toolName]) + } + } else if (currentThread?.id) { + setToolDisabledForThread(currentThread.id, toolName, enabled) + } + } + + const isToolEnabled = (toolName: string): boolean => { + if (initialMessage) { + return !getDefaultDisabledTools().includes(toolName) + } else if (currentThread?.id) { + return !isToolDisabled(currentThread.id, toolName) + } + return false + } + + // Only render if we have the custom MCP component and conditions are met + if (!selectedModelHasTools || !hasActiveMCPServers || !MCPToolComponent) { + return null + } + + return ( + + ) +} diff --git a/web-app/src/containers/RenderMarkdown.tsx b/web-app/src/containers/RenderMarkdown.tsx index 31d08cf10..c941b512d 100644 --- a/web-app/src/containers/RenderMarkdown.tsx +++ b/web-app/src/containers/RenderMarkdown.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react-hooks/exhaustive-deps */ import ReactMarkdown, { Components } from 'react-markdown' import remarkGfm from 'remark-gfm' import remarkEmoji from 'remark-emoji' diff --git a/web-app/src/containers/ThinkingBlock.tsx b/web-app/src/containers/ThinkingBlock.tsx index 68ab8644f..211fda9ff 100644 --- a/web-app/src/containers/ThinkingBlock.tsx +++ b/web-app/src/containers/ThinkingBlock.tsx @@ -3,6 +3,7 @@ import { create } from 'zustand' import { RenderMarkdown } from './RenderMarkdown' import { useAppState } from '@/hooks/useAppState' import { useTranslation } from '@/i18n/react-i18next-compat' +import { extractThinkingContent } from '@/lib/utils' interface Props { text: string @@ -43,19 +44,6 @@ const ThinkingBlock = ({ id, text }: Props) => { setThinkingState(id, newExpandedState) } - // Extract thinking content from either format - const extractThinkingContent = (text: string) => { - return text - .replace(/<\/?think>/g, '') - .replace(/<\|channel\|>analysis<\|message\|>/g, '') - .replace(/<\|start\|>assistant<\|channel\|>final<\|message\|>/g, '') - .replace(/assistant<\|channel\|>final<\|message\|>/g, '') - .replace(/<\|channel\|>/g, '') // remove any remaining channel markers - .replace(/<\|message\|>/g, '') // remove any remaining message markers - .replace(/<\|start\|>/g, '') // remove any remaining start markers - .trim() - } - const thinkingContent = extractThinkingContent(text) if (!thinkingContent) return null diff --git a/web-app/src/containers/ThreadList.tsx b/web-app/src/containers/ThreadList.tsx index b58d1872a..d971064b5 100644 --- a/web-app/src/containers/ThreadList.tsx +++ b/web-app/src/containers/ThreadList.tsx @@ -23,7 +23,7 @@ import { useThreads } from '@/hooks/useThreads' import { useThreadManagement } from '@/hooks/useThreadManagement' import { useLeftPanel } from '@/hooks/useLeftPanel' import { useMessages } from '@/hooks/useMessages' -import { cn } from '@/lib/utils' +import { cn, extractThinkingContent } from '@/lib/utils' import { useSmallScreen } from '@/hooks/useMediaQuery' import { @@ -167,14 +167,10 @@ const SortableItem = memo( )} > {thread.title || t('common:newThread')} - {variant === 'project' && ( - <> - {variant === 'project' && getLastMessageInfo?.content && ( -
- {getLastMessageInfo.content} -
- )} - + {variant === 'project' && getLastMessageInfo?.content && ( + + {extractThinkingContent(getLastMessageInfo.content)} + )}
@@ -185,7 +181,10 @@ const SortableItem = memo( { e.preventDefault() e.stopPropagation() diff --git a/web-app/src/containers/ThreadPadding.tsx b/web-app/src/containers/ThreadPadding.tsx new file mode 100644 index 000000000..3f4c725c3 --- /dev/null +++ b/web-app/src/containers/ThreadPadding.tsx @@ -0,0 +1,19 @@ +import { useThreadScrolling } from '@/hooks/useThreadScrolling' + +export const ThreadPadding = ({ + threadId, + scrollContainerRef, +}: { + threadId: string + scrollContainerRef: React.RefObject +}) => { + // Get padding height for ChatGPT-style message positioning + const { paddingHeight } = useThreadScrolling(threadId, scrollContainerRef) + return ( +
+ ) +} diff --git a/web-app/src/containers/__tests__/AvatarEmoji.test.tsx b/web-app/src/containers/__tests__/AvatarEmoji.test.tsx index ea44b95a0..e0ec9488a 100644 --- a/web-app/src/containers/__tests__/AvatarEmoji.test.tsx +++ b/web-app/src/containers/__tests__/AvatarEmoji.test.tsx @@ -121,4 +121,4 @@ describe('AvatarEmoji Component', () => { const img = screen.getByRole('img') expect(img).toHaveAttribute('alt', 'Custom avatar') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/containers/__tests__/ChatInput.simple.test.tsx b/web-app/src/containers/__tests__/ChatInput.simple.test.tsx index a1c71baa8..8992edbc0 100644 --- a/web-app/src/containers/__tests__/ChatInput.simple.test.tsx +++ b/web-app/src/containers/__tests__/ChatInput.simple.test.tsx @@ -36,4 +36,4 @@ describe('ChatInput Simple Tests', () => { const sendButton = screen.getByTestId('send-message-button') expect(sendButton).toHaveTextContent('Send') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/containers/__tests__/ChatInput.test.tsx b/web-app/src/containers/__tests__/ChatInput.test.tsx index 5038672d9..1d296e15d 100644 --- a/web-app/src/containers/__tests__/ChatInput.test.tsx +++ b/web-app/src/containers/__tests__/ChatInput.test.tsx @@ -446,4 +446,4 @@ describe('ChatInput', () => { expect(() => renderWithRouter()).not.toThrow() }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/containers/__tests__/DropdownModelProvider.displayName.test.tsx b/web-app/src/containers/__tests__/DropdownModelProvider.displayName.test.tsx index 38783dfab..5f5fba96a 100644 --- a/web-app/src/containers/__tests__/DropdownModelProvider.displayName.test.tsx +++ b/web-app/src/containers/__tests__/DropdownModelProvider.displayName.test.tsx @@ -274,4 +274,4 @@ describe('DropdownModelProvider - Display Name Integration', () => { // Both models are still visible in the dropdown, so we can't test for absence expect(screen.getAllByText('Short Name')).toHaveLength(2) // trigger + dropdown }) -}) \ No newline at end of file +}) diff --git a/web-app/src/containers/__tests__/EditModel.test.tsx b/web-app/src/containers/__tests__/EditModel.test.tsx index a02e72476..9f4eafc84 100644 --- a/web-app/src/containers/__tests__/EditModel.test.tsx +++ b/web-app/src/containers/__tests__/EditModel.test.tsx @@ -181,4 +181,4 @@ describe('DialogEditModel - Basic Component Tests', () => { expect(mockUpdateProvider).toBeDefined() expect(mockSetProviders).toBeDefined() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/containers/__tests__/LeftPanel.test.tsx b/web-app/src/containers/__tests__/LeftPanel.test.tsx index e5b316e34..d8fcccc33 100644 --- a/web-app/src/containers/__tests__/LeftPanel.test.tsx +++ b/web-app/src/containers/__tests__/LeftPanel.test.tsx @@ -266,4 +266,4 @@ describe('LeftPanel', () => { const toggleButton = document.querySelector('svg.tabler-icon-layout-sidebar') expect(toggleButton).not.toBeNull() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/containers/__tests__/SetupScreen.test.tsx b/web-app/src/containers/__tests__/SetupScreen.test.tsx index c3c98f5b4..17b16228f 100644 --- a/web-app/src/containers/__tests__/SetupScreen.test.tsx +++ b/web-app/src/containers/__tests__/SetupScreen.test.tsx @@ -168,4 +168,4 @@ describe('SetupScreen', () => { // Component should handle model installation process expect(screen.getByTestId('setup-screen')).toBeInTheDocument() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/containers/dialogs/AddModel.tsx b/web-app/src/containers/dialogs/AddModel.tsx index 2b87fb222..e8fd4e0fd 100644 --- a/web-app/src/containers/dialogs/AddModel.tsx +++ b/web-app/src/containers/dialogs/AddModel.tsx @@ -17,6 +17,7 @@ import { getProviderTitle } from '@/lib/utils' import { useTranslation } from '@/i18n/react-i18next-compat' import { ModelCapabilities } from '@/types/models' import { models as providerModels } from 'token.js' +import { toast } from 'sonner' type DialogAddModelProps = { provider: ModelProvider @@ -37,8 +38,13 @@ export const DialogAddModel = ({ provider, trigger }: DialogAddModelProps) => { // Handle form submission const handleSubmit = () => { - if (!modelId.trim()) { - return // Don't submit if model ID is empty + if (!modelId.trim()) return // Don't submit if model ID is empty + + if (provider.models.some((e) => e.id === modelId)) { + toast.error(t('providers:addModel.modelExists'), { + description: t('providers:addModel.modelExistsDesc'), + }) + return // Don't submit if model ID already exists } // Create the new model diff --git a/web-app/src/containers/dialogs/DeleteAssistantDialog.tsx b/web-app/src/containers/dialogs/DeleteAssistantDialog.tsx index 34d8b4380..2af8117af 100644 --- a/web-app/src/containers/dialogs/DeleteAssistantDialog.tsx +++ b/web-app/src/containers/dialogs/DeleteAssistantDialog.tsx @@ -75,4 +75,4 @@ export function DeleteAssistantDialog({ ) -} \ No newline at end of file +} diff --git a/web-app/src/containers/dialogs/FactoryResetDialog.tsx b/web-app/src/containers/dialogs/FactoryResetDialog.tsx index 927676799..644460b2f 100644 --- a/web-app/src/containers/dialogs/FactoryResetDialog.tsx +++ b/web-app/src/containers/dialogs/FactoryResetDialog.tsx @@ -77,4 +77,4 @@ export function FactoryResetDialog({ ) -} \ No newline at end of file +} diff --git a/web-app/src/hooks/__tests__/useAnalytic.test.ts b/web-app/src/hooks/__tests__/useAnalytic.test.ts index 8ecf4c18c..fa26ae7b9 100644 --- a/web-app/src/hooks/__tests__/useAnalytic.test.ts +++ b/web-app/src/hooks/__tests__/useAnalytic.test.ts @@ -142,4 +142,4 @@ describe('useAnalytic', () => { expect(result.current.productAnalytic).toBe(false) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useAppState.test.ts b/web-app/src/hooks/__tests__/useAppState.test.ts index a677b6564..462096eac 100644 --- a/web-app/src/hooks/__tests__/useAppState.test.ts +++ b/web-app/src/hooks/__tests__/useAppState.test.ts @@ -201,4 +201,4 @@ describe('useAppState', () => { expect(result.current.tokenSpeed).toBeUndefined() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useAppUpdater.test.ts b/web-app/src/hooks/__tests__/useAppUpdater.test.ts index 1cbd96afe..fd1a41482 100644 --- a/web-app/src/hooks/__tests__/useAppUpdater.test.ts +++ b/web-app/src/hooks/__tests__/useAppUpdater.test.ts @@ -406,4 +406,4 @@ describe('useAppUpdater', () => { expect(mockEvents.emit).toHaveBeenCalledWith('onAppUpdateDownloadSuccess', {}) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useAppearance.test.ts b/web-app/src/hooks/__tests__/useAppearance.test.ts index 74be4d3d2..8a1f3437c 100644 --- a/web-app/src/hooks/__tests__/useAppearance.test.ts +++ b/web-app/src/hooks/__tests__/useAppearance.test.ts @@ -285,4 +285,4 @@ describe('useAppearance', () => { expect(result.current.chatWidth).toBe('compact') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useChat.test.ts b/web-app/src/hooks/__tests__/useChat.test.ts index 45d46eb53..e87191fb6 100644 --- a/web-app/src/hooks/__tests__/useChat.test.ts +++ b/web-app/src/hooks/__tests__/useChat.test.ts @@ -225,4 +225,4 @@ describe('useChat', () => { expect(result.current).toBeDefined() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useClickOutside.test.ts b/web-app/src/hooks/__tests__/useClickOutside.test.ts index 0752c9396..405040761 100644 --- a/web-app/src/hooks/__tests__/useClickOutside.test.ts +++ b/web-app/src/hooks/__tests__/useClickOutside.test.ts @@ -171,4 +171,4 @@ describe('useClickOutside', () => { addEventListenerSpy.mockRestore() removeEventListenerSpy.mockRestore() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useCodeblock.test.ts b/web-app/src/hooks/__tests__/useCodeblock.test.ts index 9a71fd381..187af34b2 100644 --- a/web-app/src/hooks/__tests__/useCodeblock.test.ts +++ b/web-app/src/hooks/__tests__/useCodeblock.test.ts @@ -147,4 +147,4 @@ describe('useCodeblock', () => { expect(result.current.codeBlockStyle).toBe('preserved-theme') expect(result.current.showLineNumbers).toBe(false) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useDownloadStore.test.ts b/web-app/src/hooks/__tests__/useDownloadStore.test.ts index 66c5c97de..e68116489 100644 --- a/web-app/src/hooks/__tests__/useDownloadStore.test.ts +++ b/web-app/src/hooks/__tests__/useDownloadStore.test.ts @@ -259,4 +259,4 @@ describe('useDownloadStore', () => { expect(result.current.localDownloadingModels.has('model-1')).toBe(true) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useHotkeys.test.ts b/web-app/src/hooks/__tests__/useHotkeys.test.ts index 9af15ab8a..621718eb5 100644 --- a/web-app/src/hooks/__tests__/useHotkeys.test.ts +++ b/web-app/src/hooks/__tests__/useHotkeys.test.ts @@ -468,4 +468,4 @@ describe('useKeyboardShortcut', () => { expect(mockCallback).toHaveBeenCalledTimes(1) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useLeftPanel.test.ts b/web-app/src/hooks/__tests__/useLeftPanel.test.ts index 964ef3dd8..9406ac599 100644 --- a/web-app/src/hooks/__tests__/useLeftPanel.test.ts +++ b/web-app/src/hooks/__tests__/useLeftPanel.test.ts @@ -143,4 +143,4 @@ describe('useLeftPanel', () => { expect(result.current.open).toBe(true) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useLlamacppDevices.test.ts b/web-app/src/hooks/__tests__/useLlamacppDevices.test.ts index b8a5acdcf..6be0622e3 100644 --- a/web-app/src/hooks/__tests__/useLlamacppDevices.test.ts +++ b/web-app/src/hooks/__tests__/useLlamacppDevices.test.ts @@ -154,4 +154,4 @@ describe('useLlamacppDevices', () => { expect(result.current.devices[1].activated).toBe(true) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useLocalApiServer.test.ts b/web-app/src/hooks/__tests__/useLocalApiServer.test.ts index 388f438b9..877355651 100644 --- a/web-app/src/hooks/__tests__/useLocalApiServer.test.ts +++ b/web-app/src/hooks/__tests__/useLocalApiServer.test.ts @@ -663,4 +663,4 @@ describe('useLocalApiServer', () => { expect(result.current.serverPort).toBe(65535) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useMCPServers.test.ts b/web-app/src/hooks/__tests__/useMCPServers.test.ts index e5256a549..2e7810be1 100644 --- a/web-app/src/hooks/__tests__/useMCPServers.test.ts +++ b/web-app/src/hooks/__tests__/useMCPServers.test.ts @@ -474,4 +474,4 @@ describe('useMCPServers', () => { expect(result.current.deletedServerKeys).toContain('lifecycle-server') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useMediaQuery.test.ts b/web-app/src/hooks/__tests__/useMediaQuery.test.ts index 653511657..315fd9593 100644 --- a/web-app/src/hooks/__tests__/useMediaQuery.test.ts +++ b/web-app/src/hooks/__tests__/useMediaQuery.test.ts @@ -316,4 +316,4 @@ describe('useSmallScreen', () => { expect(mockMatchMedia).toHaveBeenCalledWith('(max-width: 768px)') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useMessages.test.ts b/web-app/src/hooks/__tests__/useMessages.test.ts index 89c0c4e85..503806e38 100644 --- a/web-app/src/hooks/__tests__/useMessages.test.ts +++ b/web-app/src/hooks/__tests__/useMessages.test.ts @@ -392,4 +392,4 @@ describe('useMessages', () => { expect(result2.current.getMessages('thread1')).toEqual([testMessage]) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useModelContextApproval.test.ts b/web-app/src/hooks/__tests__/useModelContextApproval.test.ts index 1c5e83dcd..8ed52b56b 100644 --- a/web-app/src/hooks/__tests__/useModelContextApproval.test.ts +++ b/web-app/src/hooks/__tests__/useModelContextApproval.test.ts @@ -311,4 +311,4 @@ describe('useContextSizeApproval', () => { expect(secondResult).toBe('context_shift') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useModelProvider.test.ts b/web-app/src/hooks/__tests__/useModelProvider.test.ts index 88272cf57..e1fc4a8d2 100644 --- a/web-app/src/hooks/__tests__/useModelProvider.test.ts +++ b/web-app/src/hooks/__tests__/useModelProvider.test.ts @@ -179,4 +179,4 @@ describe('useModelProvider - displayName functionality', () => { expect(provider?.models[0].displayName).toBe('Custom Model Name') expect(provider?.models[0].id).toBe('test-model.gguf') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/usePrompt.test.ts b/web-app/src/hooks/__tests__/usePrompt.test.ts index b7997cf1b..4a56cd38b 100644 --- a/web-app/src/hooks/__tests__/usePrompt.test.ts +++ b/web-app/src/hooks/__tests__/usePrompt.test.ts @@ -98,4 +98,4 @@ describe('usePrompt', () => { expect(result.current.prompt).toBe(longText) expect(result.current.prompt.length).toBe(10000) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useProviderModels.test.ts b/web-app/src/hooks/__tests__/useProviderModels.test.ts index da9b60e07..e19bcb102 100644 --- a/web-app/src/hooks/__tests__/useProviderModels.test.ts +++ b/web-app/src/hooks/__tests__/useProviderModels.test.ts @@ -99,4 +99,4 @@ describe('useProviderModels', () => { expect(fetchModelsSpy).not.toHaveBeenCalled() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useProxyConfig.test.ts b/web-app/src/hooks/__tests__/useProxyConfig.test.ts index e411ad226..9aca90821 100644 --- a/web-app/src/hooks/__tests__/useProxyConfig.test.ts +++ b/web-app/src/hooks/__tests__/useProxyConfig.test.ts @@ -320,4 +320,4 @@ describe('useProxyConfig', () => { expect(typeof result.current.setNoProxy).toBe('function') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useReleaseNotes.test.ts b/web-app/src/hooks/__tests__/useReleaseNotes.test.ts index 1b84924ff..824ae0dfa 100644 --- a/web-app/src/hooks/__tests__/useReleaseNotes.test.ts +++ b/web-app/src/hooks/__tests__/useReleaseNotes.test.ts @@ -359,4 +359,4 @@ describe('useReleaseNotes', () => { expect(result.current.error).toBe(null) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useTheme.test.ts b/web-app/src/hooks/__tests__/useTheme.test.ts index a2416eb03..f0c294385 100644 --- a/web-app/src/hooks/__tests__/useTheme.test.ts +++ b/web-app/src/hooks/__tests__/useTheme.test.ts @@ -186,4 +186,4 @@ describe('useTheme', () => { expect(result.current.isDark).toBe(false) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useToolApproval.test.ts b/web-app/src/hooks/__tests__/useToolApproval.test.ts index e73e193a3..2db153543 100644 --- a/web-app/src/hooks/__tests__/useToolApproval.test.ts +++ b/web-app/src/hooks/__tests__/useToolApproval.test.ts @@ -440,4 +440,4 @@ describe('useToolApproval', () => { expect(result.current.isToolApproved('thread-1', 'tool-a')).toBe(true) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useToolAvailable.test.ts b/web-app/src/hooks/__tests__/useToolAvailable.test.ts index f9387a4b3..b88762212 100644 --- a/web-app/src/hooks/__tests__/useToolAvailable.test.ts +++ b/web-app/src/hooks/__tests__/useToolAvailable.test.ts @@ -417,4 +417,4 @@ describe('useToolAvailable', () => { expect(result.current.isToolDisabled('thread-1', 'tool-c')).toBe(true) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useTools.test.ts b/web-app/src/hooks/__tests__/useTools.test.ts index f60b4bf18..82561e2db 100644 --- a/web-app/src/hooks/__tests__/useTools.test.ts +++ b/web-app/src/hooks/__tests__/useTools.test.ts @@ -179,4 +179,4 @@ describe('useTools', () => { expect(mockGetTools).toHaveBeenCalledTimes(1) expect(mockListen).toHaveBeenCalledTimes(1) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/__tests__/useVulkan.test.ts b/web-app/src/hooks/__tests__/useVulkan.test.ts index a958279be..aaf88057c 100644 --- a/web-app/src/hooks/__tests__/useVulkan.test.ts +++ b/web-app/src/hooks/__tests__/useVulkan.test.ts @@ -214,4 +214,4 @@ describe('useVulkan', () => { expect(result.current.vulkanEnabled).toBe(false) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/hooks/useChat.ts b/web-app/src/hooks/useChat.ts index acb4ce804..935458326 100644 --- a/web-app/src/hooks/useChat.ts +++ b/web-app/src/hooks/useChat.ts @@ -131,8 +131,7 @@ export const useChat = () => { }) } return currentThread - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [createThread, retrieveThread, router]) + }, [createThread, retrieveThread, router, setMessages]) const restartModel = useCallback( async (provider: ProviderObject, modelId: string) => { diff --git a/web-app/src/hooks/useFavoriteModel.ts b/web-app/src/hooks/useFavoriteModel.ts index 979c1400e..778c9f9d0 100644 --- a/web-app/src/hooks/useFavoriteModel.ts +++ b/web-app/src/hooks/useFavoriteModel.ts @@ -50,4 +50,4 @@ export const useFavoriteModel = create()( storage: createJSONStorage(() => localStorage), } ) -) \ No newline at end of file +) diff --git a/web-app/src/hooks/useServiceHub.ts b/web-app/src/hooks/useServiceHub.ts index 22af1886b..0a0b00d05 100644 --- a/web-app/src/hooks/useServiceHub.ts +++ b/web-app/src/hooks/useServiceHub.ts @@ -52,4 +52,4 @@ export const initializeServiceHubStore = (serviceHub: ServiceHub) => { */ export const isServiceHubInitialized = (): boolean => { return useServiceStore.getState().serviceHub !== null -} \ No newline at end of file +} diff --git a/web-app/src/hooks/useThreadManagement.ts b/web-app/src/hooks/useThreadManagement.ts index 84e5b0e34..becb41def 100644 --- a/web-app/src/hooks/useThreadManagement.ts +++ b/web-app/src/hooks/useThreadManagement.ts @@ -13,7 +13,7 @@ type ThreadFolder = { type ThreadManagementState = { folders: ThreadFolder[] setFolders: (folders: ThreadFolder[]) => void - addFolder: (name: string) => void + addFolder: (name: string) => ThreadFolder updateFolder: (id: string, name: string) => void deleteFolder: (id: string) => void getFolderById: (id: string) => ThreadFolder | undefined @@ -37,6 +37,7 @@ export const useThreadManagement = create()( set((state) => ({ folders: [...state.folders, newFolder], })) + return newFolder }, updateFolder: (id, name) => { diff --git a/web-app/src/hooks/useThreadScrolling.tsx b/web-app/src/hooks/useThreadScrolling.tsx index 41362db61..a3c6d7ed2 100644 --- a/web-app/src/hooks/useThreadScrolling.tsx +++ b/web-app/src/hooks/useThreadScrolling.tsx @@ -78,7 +78,7 @@ export const useThreadScrolling = ( return () => scrollContainer.removeEventListener('scroll', handleScroll) } - }, [handleScroll]) + }, [handleScroll, scrollContainerRef]) const checkScrollState = useCallback(() => { const scrollContainer = scrollContainerRef.current @@ -90,7 +90,7 @@ export const useThreadScrolling = ( setIsAtBottom(isBottom) setHasScrollbar(hasScroll) - }, []) + }, [scrollContainerRef]) useEffect(() => { if (!scrollContainerRef.current) return @@ -101,7 +101,7 @@ export const useThreadScrolling = ( scrollToBottom(false) checkScrollState() } - }, [checkScrollState, scrollToBottom]) + }, [checkScrollState, scrollToBottom, scrollContainerRef]) const prevCountRef = useRef(messageCount) @@ -146,7 +146,7 @@ export const useThreadScrolling = ( } prevCountRef.current = messageCount - }, [messageCount, lastMessageRole]) + }, [messageCount, lastMessageRole, getDOMElements, setPaddingHeight]) useEffect(() => { const previouslyStreaming = wasStreamingRef.current @@ -197,7 +197,7 @@ export const useThreadScrolling = ( } wasStreamingRef.current = currentlyStreaming - }, [streamingContent, threadId]) + }, [streamingContent, threadId, getDOMElements, setPaddingHeight]) useEffect(() => { userIntendedPositionRef.current = null @@ -207,7 +207,7 @@ export const useThreadScrolling = ( prevCountRef.current = messageCount scrollToBottom(false) checkScrollState() - }, [threadId]) + }, [threadId, messageCount, scrollToBottom, checkScrollState, setPaddingHeight]) return useMemo( () => ({ diff --git a/web-app/src/hooks/useToolAvailable.ts b/web-app/src/hooks/useToolAvailable.ts index 17f576fff..707b84a59 100644 --- a/web-app/src/hooks/useToolAvailable.ts +++ b/web-app/src/hooks/useToolAvailable.ts @@ -8,6 +8,8 @@ type ToolDisabledState = { disabledTools: Record // threadId -> toolNames[] // Global default disabled tools (for new threads/index page) defaultDisabledTools: string[] + // Flag to track if defaults have been initialized from extension + defaultsInitialized: boolean // Actions setToolDisabledForThread: ( @@ -19,6 +21,8 @@ type ToolDisabledState = { getDisabledToolsForThread: (threadId: string) => string[] setDefaultDisabledTools: (toolNames: string[]) => void getDefaultDisabledTools: () => string[] + isDefaultsInitialized: () => boolean + markDefaultsAsInitialized: () => void // Initialize thread tools from default or existing thread settings initializeThreadTools: (threadId: string, allTools: MCPTool[]) => void } @@ -28,6 +32,7 @@ export const useToolAvailable = create()( (set, get) => ({ disabledTools: {}, defaultDisabledTools: [], + defaultsInitialized: false, setToolDisabledForThread: ( threadId: string, @@ -81,6 +86,14 @@ export const useToolAvailable = create()( return get().defaultDisabledTools }, + isDefaultsInitialized: () => { + return get().defaultsInitialized + }, + + markDefaultsAsInitialized: () => { + set({ defaultsInitialized: true }) + }, + initializeThreadTools: (threadId: string, allTools: MCPTool[]) => { const state = get() // If thread already has settings, don't override @@ -109,6 +122,7 @@ export const useToolAvailable = create()( partialize: (state) => ({ disabledTools: state.disabledTools, defaultDisabledTools: state.defaultDisabledTools, + defaultsInitialized: state.defaultsInitialized, }), } ) diff --git a/web-app/src/hooks/useTools.ts b/web-app/src/hooks/useTools.ts index 8fc9492b5..2ff61eb10 100644 --- a/web-app/src/hooks/useTools.ts +++ b/web-app/src/hooks/useTools.ts @@ -1,19 +1,38 @@ import { useEffect } from 'react' import { getServiceHub } from '@/hooks/useServiceHub' -import { MCPTool } from '@/types/completion' import { SystemEvent } from '@/types/events' import { useAppState } from './useAppState' +import { useToolAvailable } from './useToolAvailable' +import { ExtensionManager } from '@/lib/extension' +import { ExtensionTypeEnum, MCPExtension } from '@janhq/core' export const useTools = () => { const updateTools = useAppState((state) => state.updateTools) + const { isDefaultsInitialized, setDefaultDisabledTools, markDefaultsAsInitialized } = useToolAvailable() useEffect(() => { - function setTools() { - getServiceHub().mcp().getTools().then((data: MCPTool[]) => { + async function setTools() { + try { + // Get MCP extension first + const mcpExtension = ExtensionManager.getInstance().get( + ExtensionTypeEnum.MCP + ) + + // Fetch tools + const data = await getServiceHub().mcp().getTools() updateTools(data) - }).catch((error) => { + + // Initialize default disabled tools for new users (only once) + if (!isDefaultsInitialized() && data.length > 0 && mcpExtension?.getDefaultDisabledTools) { + const defaultDisabled = await mcpExtension.getDefaultDisabledTools() + if (defaultDisabled.length > 0) { + setDefaultDisabledTools(defaultDisabled) + markDefaultsAsInitialized() + } + } + } catch (error) { console.error('Failed to fetch MCP tools:', error) - }) + } } setTools() diff --git a/web-app/src/i18n.ts b/web-app/src/i18n.ts index 84abfe50c..7a8e34750 100644 --- a/web-app/src/i18n.ts +++ b/web-app/src/i18n.ts @@ -4,4 +4,4 @@ export { default } from '@/i18n/setup' // Re-export compatibility functions for existing code export { useTranslation } from '@/i18n/react-i18next-compat' export { useAppTranslation } from '@/i18n/hooks' -export { TranslationProvider } from '@/i18n/TranslationContext' \ No newline at end of file +export { TranslationProvider } from '@/i18n/TranslationContext' diff --git a/web-app/src/i18n/TranslationContext.tsx b/web-app/src/i18n/TranslationContext.tsx index de2643764..539afc03c 100644 --- a/web-app/src/i18n/TranslationContext.tsx +++ b/web-app/src/i18n/TranslationContext.tsx @@ -43,4 +43,4 @@ export const TranslationProvider: React.FC<{ children: ReactNode }> = ({ childre ) } -export default TranslationProvider \ No newline at end of file +export default TranslationProvider diff --git a/web-app/src/i18n/context.ts b/web-app/src/i18n/context.ts index 49a3be692..35ba65880 100644 --- a/web-app/src/i18n/context.ts +++ b/web-app/src/i18n/context.ts @@ -8,4 +8,4 @@ export const TranslationContext = createContext<{ }>({ t: (key: string) => key, i18n: i18next, -}) \ No newline at end of file +}) diff --git a/web-app/src/i18n/hooks.ts b/web-app/src/i18n/hooks.ts index f1c09f7f5..f1fdf1d67 100644 --- a/web-app/src/i18n/hooks.ts +++ b/web-app/src/i18n/hooks.ts @@ -2,4 +2,4 @@ import { useContext } from "react" import { TranslationContext } from "./context" // Custom hook for easy translations -export const useAppTranslation = () => useContext(TranslationContext) \ No newline at end of file +export const useAppTranslation = () => useContext(TranslationContext) diff --git a/web-app/src/i18n/index.ts b/web-app/src/i18n/index.ts index 7c8f53bda..26f21b4d5 100644 --- a/web-app/src/i18n/index.ts +++ b/web-app/src/i18n/index.ts @@ -5,4 +5,4 @@ export { default as i18n, loadTranslations } from './setup' export { TranslationProvider } from './TranslationContext' // Export types -export type { I18nInstance, TranslationResources } from './setup' \ No newline at end of file +export type { I18nInstance, TranslationResources } from './setup' diff --git a/web-app/src/i18n/react-i18next-compat.ts b/web-app/src/i18n/react-i18next-compat.ts index 585287b9a..ee6799b9f 100644 --- a/web-app/src/i18n/react-i18next-compat.ts +++ b/web-app/src/i18n/react-i18next-compat.ts @@ -31,4 +31,4 @@ export { default as i18n } from './setup' // Re-export other utilities export { TranslationProvider } from './TranslationContext' -export { useAppTranslation } from './hooks' \ No newline at end of file +export { useAppTranslation } from './hooks' diff --git a/web-app/src/i18n/setup.ts b/web-app/src/i18n/setup.ts index 9026100ee..9b4f4621e 100644 --- a/web-app/src/i18n/setup.ts +++ b/web-app/src/i18n/setup.ts @@ -153,4 +153,4 @@ export const loadTranslations = (): void => { // Initialize and export the i18n instance const i18n = initI18n() -export default i18n \ No newline at end of file +export default i18n diff --git a/web-app/src/lib/__tests__/completion.test.ts b/web-app/src/lib/__tests__/completion.test.ts index 2ea67068d..2b3ccaec7 100644 --- a/web-app/src/lib/__tests__/completion.test.ts +++ b/web-app/src/lib/__tests__/completion.test.ts @@ -187,4 +187,4 @@ describe('completion.ts', () => { expect(result.length).toBe(0) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/lib/__tests__/extension.test.ts b/web-app/src/lib/__tests__/extension.test.ts index d4a2e8da9..b6419a30e 100644 --- a/web-app/src/lib/__tests__/extension.test.ts +++ b/web-app/src/lib/__tests__/extension.test.ts @@ -138,4 +138,4 @@ describe('extension.ts', () => { expect(invoke).toHaveBeenCalledWith('test_command', { param: 'value' }) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/lib/analytics.ts b/web-app/src/lib/analytics.ts index b7e35dc60..a0ff9090f 100644 --- a/web-app/src/lib/analytics.ts +++ b/web-app/src/lib/analytics.ts @@ -14,4 +14,4 @@ export function trackEvent( } window.gtag('event', eventName, parameters) -} \ No newline at end of file +} diff --git a/web-app/src/lib/platform/index.ts b/web-app/src/lib/platform/index.ts index 08d34d4cc..807e9261c 100644 --- a/web-app/src/lib/platform/index.ts +++ b/web-app/src/lib/platform/index.ts @@ -10,4 +10,4 @@ export * from './types' export * from './utils' // Re-export components -export * from './PlatformGuard' \ No newline at end of file +export * from './PlatformGuard' diff --git a/web-app/src/lib/shortcuts/const.ts b/web-app/src/lib/shortcuts/const.ts index 8cac19536..969ee3c33 100644 --- a/web-app/src/lib/shortcuts/const.ts +++ b/web-app/src/lib/shortcuts/const.ts @@ -38,4 +38,4 @@ export const PlatformShortcuts: ShortcutMap = { key: '-', usePlatformMetaKey: true, }, -} \ No newline at end of file +} diff --git a/web-app/src/lib/shortcuts/index.ts b/web-app/src/lib/shortcuts/index.ts index ceefa40a3..ff545fdae 100644 --- a/web-app/src/lib/shortcuts/index.ts +++ b/web-app/src/lib/shortcuts/index.ts @@ -6,4 +6,4 @@ */ export * from './types' -export * from './const' \ No newline at end of file +export * from './const' diff --git a/web-app/src/lib/shortcuts/types.ts b/web-app/src/lib/shortcuts/types.ts index 8ac96ae0e..1ce5104e0 100644 --- a/web-app/src/lib/shortcuts/types.ts +++ b/web-app/src/lib/shortcuts/types.ts @@ -20,4 +20,4 @@ export interface ShortcutSpec { metaKey?: boolean } -export type ShortcutMap = Record \ No newline at end of file +export type ShortcutMap = Record diff --git a/web-app/src/lib/utils.ts b/web-app/src/lib/utils.ts index 663a5051b..b035600ab 100644 --- a/web-app/src/lib/utils.ts +++ b/web-app/src/lib/utils.ts @@ -3,10 +3,12 @@ import { twMerge } from 'tailwind-merge' import { ExtensionManager } from './extension' import path from "path" + export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)) } + export function basenameNoExt(filePath: string): string { const base = path.basename(filePath); const VALID_EXTENSIONS = [".tar.gz", ".zip"]; @@ -22,6 +24,7 @@ export function basenameNoExt(filePath: string): string { return base.slice(0, -path.extname(base).length); } + /** * Get the display name for a model, falling back to the model ID if no display name is set */ @@ -188,3 +191,15 @@ export function formatDuration(startTime: number, endTime?: number): string { export function sanitizeModelId(modelId: string): string { return modelId.replace(/[^a-zA-Z0-9/_\-.]/g, '').replace(/\./g, '_') } + +export const extractThinkingContent = (text: string) => { + return text + .replace(/<\/?think>/g, '') + .replace(/<\|channel\|>analysis<\|message\|>/g, '') + .replace(/<\|start\|>assistant<\|channel\|>final<\|message\|>/g, '') + .replace(/assistant<\|channel\|>final<\|message\|>/g, '') + .replace(/<\|channel\|>/g, '') // remove any remaining channel markers + .replace(/<\|message\|>/g, '') // remove any remaining message markers + .replace(/<\|start\|>/g, '') // remove any remaining start markers + .trim() +} diff --git a/web-app/src/locales/de-DE/providers.json b/web-app/src/locales/de-DE/providers.json index 7e6d68c0f..39c52e047 100644 --- a/web-app/src/locales/de-DE/providers.json +++ b/web-app/src/locales/de-DE/providers.json @@ -35,7 +35,9 @@ "modelId": "Modell ID", "enterModelId": "Modell ID eingeben", "exploreModels": "Sehe Modellliste von {{provider}}", - "addModel": "Modell hinzufügen" + "addModel": "Modell hinzufügen", + "modelExists": "Modell bereits vorhanden", + "modelExistsDesc": "Bitte wähle eine andere Modell-ID." }, "deleteModel": { "title": "Lösche Modell: {{modelId}}", diff --git a/web-app/src/locales/en/providers.json b/web-app/src/locales/en/providers.json index 62fe69b8e..2683432f9 100644 --- a/web-app/src/locales/en/providers.json +++ b/web-app/src/locales/en/providers.json @@ -35,7 +35,9 @@ "modelId": "Model ID", "enterModelId": "Enter model ID", "exploreModels": "See model list from {{provider}}", - "addModel": "Add Model" + "addModel": "Add Model", + "modelExists": "Model already exists", + "modelExistsDesc": "Please choose a different model ID." }, "deleteModel": { "title": "Delete Model: {{modelId}}", @@ -69,4 +71,4 @@ "addProvider": "Add Provider", "addOpenAIProvider": "Add OpenAI Provider", "enterNameForProvider": "Enter name for provider" -} \ No newline at end of file +} diff --git a/web-app/src/locales/id/providers.json b/web-app/src/locales/id/providers.json index 803aac3e7..5f89d69c6 100644 --- a/web-app/src/locales/id/providers.json +++ b/web-app/src/locales/id/providers.json @@ -35,7 +35,9 @@ "modelId": "ID Model", "enterModelId": "Masukkan ID model", "exploreModels": "Lihat daftar model dari {{provider}}", - "addModel": "Tambah Model" + "addModel": "Tambah Model", + "modelExists": "Model sudah ada", + "modelExistsDesc": "Silakan pilih ID model yang berbeda." }, "deleteModel": { "title": "Hapus Model: {{modelId}}", @@ -69,4 +71,4 @@ "addProvider": "Tambah Penyedia", "addOpenAIProvider": "Tambah Penyedia OpenAI", "enterNameForProvider": "Masukkan nama untuk penyedia" -} \ No newline at end of file +} diff --git a/web-app/src/locales/pl/providers.json b/web-app/src/locales/pl/providers.json index 55992e3bb..c1c03434e 100644 --- a/web-app/src/locales/pl/providers.json +++ b/web-app/src/locales/pl/providers.json @@ -35,7 +35,9 @@ "modelId": "Identyfikator Modelu", "enterModelId": "Wprowadź identyfikator modelu", "exploreModels": "Zobacz listę modeli dostawcy {{provider}}", - "addModel": "Dodaj Model" + "addModel": "Dodaj Model", + "modelExists": "Model już istnieje", + "modelExistsDesc": "Wybierz inny identyfikator modelu." }, "deleteModel": { "title": "Usuń Model: {{modelId}}", diff --git a/web-app/src/locales/vn/providers.json b/web-app/src/locales/vn/providers.json index bd6bdb334..8c0e6d1b8 100644 --- a/web-app/src/locales/vn/providers.json +++ b/web-app/src/locales/vn/providers.json @@ -35,7 +35,9 @@ "modelId": "ID mô hình", "enterModelId": "Nhập ID mô hình", "exploreModels": "Xem danh sách mô hình từ {{provider}}", - "addModel": "Thêm mô hình" + "addModel": "Thêm mô hình", + "modelExists": "Mô hình đã tồn tại", + "modelExistsDesc": "Vui lòng chọn một ID mô hình khác." }, "deleteModel": { "title": "Xóa mô hình: {{modelId}}", @@ -69,4 +71,4 @@ "addProvider": "Thêm nhà cung cấp", "addOpenAIProvider": "Thêm nhà cung cấp OpenAI", "enterNameForProvider": "Nhập tên cho nhà cung cấp" -} \ No newline at end of file +} diff --git a/web-app/src/locales/zh-CN/providers.json b/web-app/src/locales/zh-CN/providers.json index ecc04df49..2ca2beb2e 100644 --- a/web-app/src/locales/zh-CN/providers.json +++ b/web-app/src/locales/zh-CN/providers.json @@ -35,7 +35,9 @@ "modelId": "模型 ID", "enterModelId": "输入模型 ID", "exploreModels": "查看 {{provider}} 的模型列表", - "addModel": "添加模型" + "addModel": "添加模型", + "modelExists": "模型已存在", + "modelExistsDesc": "请选择不同的模型 ID。" }, "deleteModel": { "title": "删除模型:{{modelId}}", @@ -69,4 +71,4 @@ "addProvider": "添加提供商", "addOpenAIProvider": "添加 OpenAI 提供商", "enterNameForProvider": "输入提供商名称" -} \ No newline at end of file +} diff --git a/web-app/src/locales/zh-TW/providers.json b/web-app/src/locales/zh-TW/providers.json index 316a9ed08..39580818b 100644 --- a/web-app/src/locales/zh-TW/providers.json +++ b/web-app/src/locales/zh-TW/providers.json @@ -35,7 +35,9 @@ "modelId": "模型 ID", "enterModelId": "輸入模型 ID", "exploreModels": "查看 {{provider}} 的模型清單", - "addModel": "新增模型" + "addModel": "新增模型", + "modelExists": "模型已存在", + "modelExistsDesc": "請選擇不同的模型 ID。" }, "deleteModel": { "title": "刪除模型:{{modelId}}", @@ -69,4 +71,4 @@ "addProvider": "新增提供者", "addOpenAIProvider": "新增 OpenAI 提供者", "enterNameForProvider": "輸入提供者名稱" -} \ No newline at end of file +} diff --git a/web-app/src/providers/ServiceHubProvider.tsx b/web-app/src/providers/ServiceHubProvider.tsx index 68aa43c19..0ab261ae4 100644 --- a/web-app/src/providers/ServiceHubProvider.tsx +++ b/web-app/src/providers/ServiceHubProvider.tsx @@ -23,4 +23,4 @@ export function ServiceHubProvider({ children }: ServiceHubProviderProps) { }, []) return <>{isReady && children} -} \ No newline at end of file +} diff --git a/web-app/src/providers/__tests__/DataProvider.test.tsx b/web-app/src/providers/__tests__/DataProvider.test.tsx index 25a036350..19fd0418c 100644 --- a/web-app/src/providers/__tests__/DataProvider.test.tsx +++ b/web-app/src/providers/__tests__/DataProvider.test.tsx @@ -101,4 +101,4 @@ describe('DataProvider', () => { expect(screen.getByText('Test Child 1')).toBeInTheDocument() expect(screen.getByText('Test Child 2')).toBeInTheDocument() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/providers/__tests__/ThemeProvider.test.tsx b/web-app/src/providers/__tests__/ThemeProvider.test.tsx index 232035874..e0cf5748c 100644 --- a/web-app/src/providers/__tests__/ThemeProvider.test.tsx +++ b/web-app/src/providers/__tests__/ThemeProvider.test.tsx @@ -79,4 +79,4 @@ describe('ThemeProvider', () => { // Should be called on mount expect(useTheme).toHaveBeenCalled() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/routes/hub/__tests__/huggingface-conversion.test.ts b/web-app/src/routes/hub/__tests__/huggingface-conversion.test.ts index 23b65b9ef..8dbaf4720 100644 --- a/web-app/src/routes/hub/__tests__/huggingface-conversion.test.ts +++ b/web-app/src/routes/hub/__tests__/huggingface-conversion.test.ts @@ -304,4 +304,4 @@ describe('HuggingFace Repository Conversion', () => { expect(result.quants[0].file_size).toBe('1024.0 GB') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/routes/project/index.tsx b/web-app/src/routes/project/index.tsx index 5ab24bb39..300bb550b 100644 --- a/web-app/src/routes/project/index.tsx +++ b/web-app/src/routes/project/index.tsx @@ -1,4 +1,4 @@ -import { createFileRoute } from '@tanstack/react-router' +import { createFileRoute, useNavigate } from '@tanstack/react-router' import { useState, useMemo } from 'react' import { useThreadManagement } from '@/hooks/useThreadManagement' @@ -31,6 +31,7 @@ function Project() { function ProjectContent() { const { t } = useTranslation() + const navigate = useNavigate() const { folders, addFolder, updateFolder, deleteFolder, getFolderById } = useThreadManagement() const threads = useThreads((state) => state.threads) @@ -59,7 +60,12 @@ function ProjectContent() { if (editingKey) { updateFolder(editingKey, name) } else { - addFolder(name) + const newProject = addFolder(name) + // Navigate to the newly created project + navigate({ + to: '/project/$projectId', + params: { projectId: newProject.id }, + }) } setOpen(false) setEditingKey(null) diff --git a/web-app/src/routes/settings/__tests__/appearance.test.tsx b/web-app/src/routes/settings/__tests__/appearance.test.tsx index 6b2727588..c7560ad70 100644 --- a/web-app/src/routes/settings/__tests__/appearance.test.tsx +++ b/web-app/src/routes/settings/__tests__/appearance.test.tsx @@ -235,4 +235,4 @@ describe('Appearance Settings Route', () => { const settingsMenu = screen.getByTestId('settings-menu') expect(settingsMenu).toBeInTheDocument() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/routes/settings/__tests__/extensions.test.tsx b/web-app/src/routes/settings/__tests__/extensions.test.tsx index d7dcf22d6..a111ab741 100644 --- a/web-app/src/routes/settings/__tests__/extensions.test.tsx +++ b/web-app/src/routes/settings/__tests__/extensions.test.tsx @@ -226,4 +226,4 @@ describe('Extensions Settings Route', () => { const settingsContent = screen.getByTestId('settings-menu').nextElementSibling expect(settingsContent).toHaveClass('p-4', 'w-full', 'h-[calc(100%-32px)]', 'overflow-y-auto') }) -}) \ No newline at end of file +}) diff --git a/web-app/src/routes/settings/__tests__/hardware.test.tsx b/web-app/src/routes/settings/__tests__/hardware.test.tsx index 604ee639c..a57b5d7c7 100644 --- a/web-app/src/routes/settings/__tests__/hardware.test.tsx +++ b/web-app/src/routes/settings/__tests__/hardware.test.tsx @@ -182,4 +182,4 @@ describe('Hardware Settings', () => { expect(screen.queryByText('GPUs')).not.toBeInTheDocument() }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/routes/settings/__tests__/privacy.test.tsx b/web-app/src/routes/settings/__tests__/privacy.test.tsx index 57bc4b870..3c46a571c 100644 --- a/web-app/src/routes/settings/__tests__/privacy.test.tsx +++ b/web-app/src/routes/settings/__tests__/privacy.test.tsx @@ -184,4 +184,4 @@ describe('Privacy Settings Route', () => { fireEvent.click(analyticsSwitch) expect(analyticsSwitch).toBeInTheDocument() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/routes/settings/__tests__/shortcuts.test.tsx b/web-app/src/routes/settings/__tests__/shortcuts.test.tsx index 4e9eb7641..d5ac24ab1 100644 --- a/web-app/src/routes/settings/__tests__/shortcuts.test.tsx +++ b/web-app/src/routes/settings/__tests__/shortcuts.test.tsx @@ -190,4 +190,4 @@ describe('Shortcuts Settings Route', () => { const contentArea = screen.getAllByTestId('card') expect(contentArea.length).toBeGreaterThan(0) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/routes/settings/providers/$providerName.tsx b/web-app/src/routes/settings/providers/$providerName.tsx index a87a76f52..0e52cb059 100644 --- a/web-app/src/routes/settings/providers/$providerName.tsx +++ b/web-app/src/routes/settings/providers/$providerName.tsx @@ -318,17 +318,7 @@ function ProviderDetail() { .getActiveModels() .then((models) => setActiveModels(models || [])) } catch (error) { - console.error('Error starting model:', error) - if ( - error && - typeof error === 'object' && - 'message' in error && - typeof error.message === 'string' - ) { - setModelLoadError({ message: error.message }) - } else { - setModelLoadError(typeof error === 'string' ? error : `${error}`) - } + setModelLoadError(error as ErrorObject) } finally { // Remove model from loading state setLoadingModels((prev) => prev.filter((id) => id !== modelId)) diff --git a/web-app/src/routes/settings/providers/__tests__/index.test.tsx b/web-app/src/routes/settings/providers/__tests__/index.test.tsx index e6a95e2bc..767775b64 100644 --- a/web-app/src/routes/settings/providers/__tests__/index.test.tsx +++ b/web-app/src/routes/settings/providers/__tests__/index.test.tsx @@ -303,4 +303,4 @@ describe('Providers Settings Route', () => { // With empty providers array, should still render the page structure expect(screen.getByTestId('card')).toBeInTheDocument() }) -}) \ No newline at end of file +}) diff --git a/web-app/src/routes/threads/$threadId.tsx b/web-app/src/routes/threads/$threadId.tsx index 1d0fc4932..4857308d2 100644 --- a/web-app/src/routes/threads/$threadId.tsx +++ b/web-app/src/routes/threads/$threadId.tsx @@ -24,8 +24,8 @@ import { PlatformFeatures } from '@/lib/platform/const' import { PlatformFeature } from '@/lib/platform/types' import ScrollToBottom from '@/containers/ScrollToBottom' import { PromptProgress } from '@/components/PromptProgress' +import { ThreadPadding } from '@/containers/ThreadPadding' import { TEMPORARY_CHAT_ID, TEMPORARY_CHAT_QUERY_ID } from '@/constants/chat' -import { useThreadScrolling } from '@/hooks/useThreadScrolling' import { IconInfoCircle } from '@tabler/icons-react' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' @@ -102,9 +102,6 @@ function ThreadDetail() { const scrollContainerRef = useRef(null) - // Get padding height for ChatGPT-style message positioning - const { paddingHeight } = useThreadScrolling(threadId, scrollContainerRef) - // Listen for conversation not found events useEffect(() => { const handleConversationNotFound = (event: CustomEvent) => { @@ -126,7 +123,7 @@ function ThreadDetail() { return () => { window.removeEventListener(CONVERSATION_NOT_FOUND_EVENT, handleConversationNotFound as EventListener) } - }, [threadId, navigate]) + }, [threadId, navigate, t]) useEffect(() => { setCurrentThreadId(threadId) @@ -278,11 +275,7 @@ function ThreadDetail() { data-test-id="thread-content-text" /> {/* Persistent padding element for ChatGPT-style message positioning */} -
+
{ }) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/__tests__/assistants.test.ts b/web-app/src/services/__tests__/assistants.test.ts index 8c7d96e2c..0fe92f6b7 100644 --- a/web-app/src/services/__tests__/assistants.test.ts +++ b/web-app/src/services/__tests__/assistants.test.ts @@ -130,4 +130,4 @@ describe('DefaultAssistantsService', () => { await expect(assistantsService.deleteAssistant(assistant)).rejects.toThrow('Failed to delete assistant') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/__tests__/events.test.ts b/web-app/src/services/__tests__/events.test.ts index ab3d597f8..9781cee8f 100644 --- a/web-app/src/services/__tests__/events.test.ts +++ b/web-app/src/services/__tests__/events.test.ts @@ -142,4 +142,4 @@ describe('EventEmitter', () => { expect(handler).toHaveBeenCalledWith(complexData) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/__tests__/hardware.test.ts b/web-app/src/services/__tests__/hardware.test.ts index f9a16155b..69ccb0423 100644 --- a/web-app/src/services/__tests__/hardware.test.ts +++ b/web-app/src/services/__tests__/hardware.test.ts @@ -262,4 +262,4 @@ describe('TauriHardwareService', () => { expect(vi.mocked(invoke)).toHaveBeenNthCalledWith(2, 'plugin:hardware|get_system_usage') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/__tests__/mcp.test.ts b/web-app/src/services/__tests__/mcp.test.ts index 0f5e9d073..a2af0cc76 100644 --- a/web-app/src/services/__tests__/mcp.test.ts +++ b/web-app/src/services/__tests__/mcp.test.ts @@ -430,4 +430,4 @@ describe('TauriMCPService', () => { expect(result).toEqual(toolResult) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/__tests__/messages.test.ts b/web-app/src/services/__tests__/messages.test.ts index 445a9e53a..21032bb76 100644 --- a/web-app/src/services/__tests__/messages.test.ts +++ b/web-app/src/services/__tests__/messages.test.ts @@ -158,4 +158,4 @@ describe('DefaultMessagesService', () => { await expect(messagesService.deleteMessage(threadId, messageId)).rejects.toThrow('Failed to delete message') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/__tests__/serviceHub.integration.test.ts b/web-app/src/services/__tests__/serviceHub.integration.test.ts index 5b89347a1..8a8a10344 100644 --- a/web-app/src/services/__tests__/serviceHub.integration.test.ts +++ b/web-app/src/services/__tests__/serviceHub.integration.test.ts @@ -213,4 +213,4 @@ describe('ServiceHub Integration Tests', () => { }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/__tests__/web-specific.test.ts b/web-app/src/services/__tests__/web-specific.test.ts index 135b48c1c..52e544bf5 100644 --- a/web-app/src/services/__tests__/web-specific.test.ts +++ b/web-app/src/services/__tests__/web-specific.test.ts @@ -99,4 +99,4 @@ describe('Web-Specific Service Tests', () => { expect(typeof service.factoryReset).toBe('function') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/services/analytic/default.ts b/web-app/src/services/analytic/default.ts index eff3a14c3..e0ae67224 100644 --- a/web-app/src/services/analytic/default.ts +++ b/web-app/src/services/analytic/default.ts @@ -20,4 +20,4 @@ export class DefaultAnalyticService implements AnalyticService { await window.core?.api?.getAppConfigurations() return appConfiguration.distinct_id } -} \ No newline at end of file +} diff --git a/web-app/src/services/analytic/types.ts b/web-app/src/services/analytic/types.ts index e54e74424..b8f2fc6d7 100644 --- a/web-app/src/services/analytic/types.ts +++ b/web-app/src/services/analytic/types.ts @@ -5,4 +5,4 @@ export interface AnalyticService { updateDistinctId(id: string): Promise getAppDistinctId(): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/app/default.ts b/web-app/src/services/app/default.ts index 9e54c6791..68fe1f0ea 100644 --- a/web-app/src/services/app/default.ts +++ b/web-app/src/services/app/default.ts @@ -39,4 +39,4 @@ export class DefaultAppService implements AppService { console.log('readYaml called with path:', path) throw new Error('readYaml not implemented in default app service') } -} \ No newline at end of file +} diff --git a/web-app/src/services/app/tauri.ts b/web-app/src/services/app/tauri.ts index b59a9f676..af770e93d 100644 --- a/web-app/src/services/app/tauri.ts +++ b/web-app/src/services/app/tauri.ts @@ -75,4 +75,4 @@ export class TauriAppService extends DefaultAppService { async readYaml(path: string): Promise { return await invoke('read_yaml', { path }) } -} \ No newline at end of file +} diff --git a/web-app/src/services/app/types.ts b/web-app/src/services/app/types.ts index 9b0c25b7e..4be49c584 100644 --- a/web-app/src/services/app/types.ts +++ b/web-app/src/services/app/types.ts @@ -17,4 +17,4 @@ export interface AppService { relocateJanDataFolder(path: string): Promise getServerStatus(): Promise readYaml(path: string): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/app/web.ts b/web-app/src/services/app/web.ts index 06ba65080..5702079d1 100644 --- a/web-app/src/services/app/web.ts +++ b/web-app/src/services/app/web.ts @@ -45,4 +45,4 @@ export class WebAppService implements AppService { console.log('YAML reading not available in web mode') throw new Error('readYaml not implemented in web app service') } -} \ No newline at end of file +} diff --git a/web-app/src/services/assistants/default.ts b/web-app/src/services/assistants/default.ts index 65d3cc58f..b74582510 100644 --- a/web-app/src/services/assistants/default.ts +++ b/web-app/src/services/assistants/default.ts @@ -31,4 +31,4 @@ export class DefaultAssistantsService implements AssistantsService { .get(ExtensionTypeEnum.Assistant) ?.deleteAssistant(assistant) } -} \ No newline at end of file +} diff --git a/web-app/src/services/assistants/types.ts b/web-app/src/services/assistants/types.ts index 1be730fe2..be43cfb49 100644 --- a/web-app/src/services/assistants/types.ts +++ b/web-app/src/services/assistants/types.ts @@ -8,4 +8,4 @@ export interface AssistantsService { getAssistants(): Promise createAssistant(assistant: Assistant): Promise deleteAssistant(assistant: Assistant): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/core/default.ts b/web-app/src/services/core/default.ts index 235e38294..f019e235d 100644 --- a/web-app/src/services/core/default.ts +++ b/web-app/src/services/core/default.ts @@ -38,4 +38,4 @@ export class DefaultCoreService implements CoreService { async getAppToken(): Promise { return null } -} \ No newline at end of file +} diff --git a/web-app/src/services/core/tauri.ts b/web-app/src/services/core/tauri.ts index 8f83b0b2c..8b968510d 100644 --- a/web-app/src/services/core/tauri.ts +++ b/web-app/src/services/core/tauri.ts @@ -73,4 +73,4 @@ export class TauriCoreService extends DefaultCoreService { return null } } -} \ No newline at end of file +} diff --git a/web-app/src/services/core/types.ts b/web-app/src/services/core/types.ts index 8f518ffa4..6bcfb2983 100644 --- a/web-app/src/services/core/types.ts +++ b/web-app/src/services/core/types.ts @@ -21,4 +21,4 @@ export interface CoreService { // App token getAppToken(): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/core/web.ts b/web-app/src/services/core/web.ts index 39a248611..abb2c6330 100644 --- a/web-app/src/services/core/web.ts +++ b/web-app/src/services/core/web.ts @@ -89,4 +89,4 @@ export class WebCoreService implements CoreService { console.warn('App token not available in web environment') return null } -} \ No newline at end of file +} diff --git a/web-app/src/services/deeplink/default.ts b/web-app/src/services/deeplink/default.ts index a7f8cf5da..27971ce8a 100644 --- a/web-app/src/services/deeplink/default.ts +++ b/web-app/src/services/deeplink/default.ts @@ -15,4 +15,4 @@ export class DefaultDeepLinkService implements DeepLinkService { async getCurrent(): Promise { return [] } -} \ No newline at end of file +} diff --git a/web-app/src/services/deeplink/tauri.ts b/web-app/src/services/deeplink/tauri.ts index cab694353..722e5d209 100644 --- a/web-app/src/services/deeplink/tauri.ts +++ b/web-app/src/services/deeplink/tauri.ts @@ -24,4 +24,4 @@ export class TauriDeepLinkService extends DefaultDeepLinkService { return [] } } -} \ No newline at end of file +} diff --git a/web-app/src/services/deeplink/types.ts b/web-app/src/services/deeplink/types.ts index 19b3ff517..22ecd5426 100644 --- a/web-app/src/services/deeplink/types.ts +++ b/web-app/src/services/deeplink/types.ts @@ -6,4 +6,4 @@ export interface DeepLinkService { onOpenUrl(handler: (urls: string[]) => void): Promise<() => void> getCurrent(): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/deeplink/web.ts b/web-app/src/services/deeplink/web.ts index bba92c43c..1c8df40be 100644 --- a/web-app/src/services/deeplink/web.ts +++ b/web-app/src/services/deeplink/web.ts @@ -24,4 +24,4 @@ export class WebDeepLinkService implements DeepLinkService { // Return current URL return [window.location.href] } -} \ No newline at end of file +} diff --git a/web-app/src/services/dialog/default.ts b/web-app/src/services/dialog/default.ts index 3232fd638..f2ec6d1d5 100644 --- a/web-app/src/services/dialog/default.ts +++ b/web-app/src/services/dialog/default.ts @@ -14,4 +14,4 @@ export class DefaultDialogService implements DialogService { console.log('dialog.save called with options:', options) return null } -} \ No newline at end of file +} diff --git a/web-app/src/services/dialog/tauri.ts b/web-app/src/services/dialog/tauri.ts index 1b4efad28..d6d5fc2f2 100644 --- a/web-app/src/services/dialog/tauri.ts +++ b/web-app/src/services/dialog/tauri.ts @@ -33,4 +33,4 @@ export class TauriDialogService extends DefaultDialogService { return null } } -} \ No newline at end of file +} diff --git a/web-app/src/services/dialog/types.ts b/web-app/src/services/dialog/types.ts index 245155c36..8f3c4e3ab 100644 --- a/web-app/src/services/dialog/types.ts +++ b/web-app/src/services/dialog/types.ts @@ -16,4 +16,4 @@ export interface DialogOpenOptions { export interface DialogService { open(options?: DialogOpenOptions): Promise save(options?: DialogOpenOptions): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/dialog/web.ts b/web-app/src/services/dialog/web.ts index c735cbf41..9dbfd503d 100644 --- a/web-app/src/services/dialog/web.ts +++ b/web-app/src/services/dialog/web.ts @@ -54,4 +54,4 @@ export class WebDialogService implements DialogService { console.warn('Save dialog not supported in web environment') return null } -} \ No newline at end of file +} diff --git a/web-app/src/services/events/EventEmitter.ts b/web-app/src/services/events/EventEmitter.ts index bb9e57ebb..762ca5018 100644 --- a/web-app/src/services/events/EventEmitter.ts +++ b/web-app/src/services/events/EventEmitter.ts @@ -44,4 +44,4 @@ export class EventEmitter { handler(args) }) } -} \ No newline at end of file +} diff --git a/web-app/src/services/events/default.ts b/web-app/src/services/events/default.ts index 5b5a67492..f7f9aa83d 100644 --- a/web-app/src/services/events/default.ts +++ b/web-app/src/services/events/default.ts @@ -16,4 +16,4 @@ export class DefaultEventsService implements EventsService { // No-op unlisten function } } -} \ No newline at end of file +} diff --git a/web-app/src/services/events/tauri.ts b/web-app/src/services/events/tauri.ts index b15e1e338..c58b03781 100644 --- a/web-app/src/services/events/tauri.ts +++ b/web-app/src/services/events/tauri.ts @@ -27,4 +27,4 @@ export class TauriEventsService extends DefaultEventsService { return () => {} } } -} \ No newline at end of file +} diff --git a/web-app/src/services/events/types.ts b/web-app/src/services/events/types.ts index e57641114..0b033efdc 100644 --- a/web-app/src/services/events/types.ts +++ b/web-app/src/services/events/types.ts @@ -13,4 +13,4 @@ export interface UnlistenFn { export interface EventsService { emit(event: string, payload?: T, options?: EventOptions): Promise listen(event: string, handler: (event: { payload: T }) => void, options?: EventOptions): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/events/web.ts b/web-app/src/services/events/web.ts index a14a6fe0d..055fd4571 100644 --- a/web-app/src/services/events/web.ts +++ b/web-app/src/services/events/web.ts @@ -32,4 +32,4 @@ export class WebEventsService implements EventsService { this.eventTarget.removeEventListener(event, eventListener) } } -} \ No newline at end of file +} diff --git a/web-app/src/services/hardware/default.ts b/web-app/src/services/hardware/default.ts index 250e56de9..5f4df438c 100644 --- a/web-app/src/services/hardware/default.ts +++ b/web-app/src/services/hardware/default.ts @@ -21,4 +21,4 @@ export class DefaultHardwareService implements HardwareService { console.log('setActiveGpus called with data:', data) // No-op - not implemented in default service } -} \ No newline at end of file +} diff --git a/web-app/src/services/hardware/tauri.ts b/web-app/src/services/hardware/tauri.ts index 458b3037b..efcd23c37 100644 --- a/web-app/src/services/hardware/tauri.ts +++ b/web-app/src/services/hardware/tauri.ts @@ -30,4 +30,4 @@ export class TauriHardwareService extends DefaultHardwareService { // TODO: llama.cpp extension should handle this console.log(data) } -} \ No newline at end of file +} diff --git a/web-app/src/services/hardware/types.ts b/web-app/src/services/hardware/types.ts index 026d616c6..3307298f6 100644 --- a/web-app/src/services/hardware/types.ts +++ b/web-app/src/services/hardware/types.ts @@ -21,4 +21,4 @@ export interface HardwareService { } // Re-export hardware types for convenience -export type { HardwareData, SystemUsage } \ No newline at end of file +export type { HardwareData, SystemUsage } diff --git a/web-app/src/services/mcp/default.ts b/web-app/src/services/mcp/default.ts index 1bcce285a..b9a2f832e 100644 --- a/web-app/src/services/mcp/default.ts +++ b/web-app/src/services/mcp/default.ts @@ -66,4 +66,4 @@ export class DefaultMCPService implements MCPService { console.log('deactivateMCPServer called with name:', name) // No-op - not implemented in default service } -} \ No newline at end of file +} diff --git a/web-app/src/services/mcp/tauri.ts b/web-app/src/services/mcp/tauri.ts index 697bbc500..244e002c3 100644 --- a/web-app/src/services/mcp/tauri.ts +++ b/web-app/src/services/mcp/tauri.ts @@ -75,4 +75,4 @@ export class TauriMCPService extends DefaultMCPService { async deactivateMCPServer(name: string): Promise { return await invoke('deactivate_mcp_server', { name }) } -} \ No newline at end of file +} diff --git a/web-app/src/services/mcp/types.ts b/web-app/src/services/mcp/types.ts index c551c61c5..eef8e1bf3 100644 --- a/web-app/src/services/mcp/types.ts +++ b/web-app/src/services/mcp/types.ts @@ -32,4 +32,4 @@ export interface MCPService { // MCP Server lifecycle management activateMCPServer(name: string, config: MCPServerConfig): Promise deactivateMCPServer(name: string): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/mcp/web.ts b/web-app/src/services/mcp/web.ts index 124de93fd..c23283951 100644 --- a/web-app/src/services/mcp/web.ts +++ b/web-app/src/services/mcp/web.ts @@ -276,4 +276,4 @@ export class WebMCPService implements MCPService { private generateCancellationToken(): string { return `mcp_cancel_${Date.now()}_${Math.random().toString(36).substring(2, 11)}` } -} \ No newline at end of file +} diff --git a/web-app/src/services/messages/default.ts b/web-app/src/services/messages/default.ts index 177a09e29..8dbd63181 100644 --- a/web-app/src/services/messages/default.ts +++ b/web-app/src/services/messages/default.ts @@ -50,4 +50,4 @@ export class DefaultMessagesService implements MessagesService { .get(ExtensionTypeEnum.Conversational) ?.deleteMessage(threadId, messageId) } -} \ No newline at end of file +} diff --git a/web-app/src/services/messages/types.ts b/web-app/src/services/messages/types.ts index ad5ae72c8..731de06d0 100644 --- a/web-app/src/services/messages/types.ts +++ b/web-app/src/services/messages/types.ts @@ -8,4 +8,4 @@ export interface MessagesService { fetchMessages(threadId: string): Promise createMessage(message: ThreadMessage): Promise deleteMessage(threadId: string, messageId: string): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/opener/default.ts b/web-app/src/services/opener/default.ts index 287e927b8..d7f6f2722 100644 --- a/web-app/src/services/opener/default.ts +++ b/web-app/src/services/opener/default.ts @@ -9,4 +9,4 @@ export class DefaultOpenerService implements OpenerService { console.log('revealItemInDir called with path:', path) // No-op - not implemented in default service } -} \ No newline at end of file +} diff --git a/web-app/src/services/opener/tauri.ts b/web-app/src/services/opener/tauri.ts index 9c465e521..c47bf687a 100644 --- a/web-app/src/services/opener/tauri.ts +++ b/web-app/src/services/opener/tauri.ts @@ -14,4 +14,4 @@ export class TauriOpenerService extends DefaultOpenerService { throw error } } -} \ No newline at end of file +} diff --git a/web-app/src/services/opener/types.ts b/web-app/src/services/opener/types.ts index 21e0d17f0..1dd7bee87 100644 --- a/web-app/src/services/opener/types.ts +++ b/web-app/src/services/opener/types.ts @@ -5,4 +5,4 @@ export interface OpenerService { revealItemInDir(path: string): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/path/default.ts b/web-app/src/services/path/default.ts index 90ed46e82..cde56bf4a 100644 --- a/web-app/src/services/path/default.ts +++ b/web-app/src/services/path/default.ts @@ -28,4 +28,4 @@ export class DefaultPathService implements PathService { console.log('path.extname called with path:', path) return '' } -} \ No newline at end of file +} diff --git a/web-app/src/services/path/tauri.ts b/web-app/src/services/path/tauri.ts index 80b5808c9..618c15abd 100644 --- a/web-app/src/services/path/tauri.ts +++ b/web-app/src/services/path/tauri.ts @@ -55,4 +55,4 @@ export class TauriPathService extends DefaultPathService { return lastDot > lastSlash ? path.substring(lastDot) : '' } } -} \ No newline at end of file +} diff --git a/web-app/src/services/path/types.ts b/web-app/src/services/path/types.ts index 269adc1c9..5f5c9af13 100644 --- a/web-app/src/services/path/types.ts +++ b/web-app/src/services/path/types.ts @@ -9,4 +9,4 @@ export interface PathService { dirname(path: string): Promise basename(path: string): Promise extname(path: string): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/path/web.ts b/web-app/src/services/path/web.ts index 724acb130..e207fa01b 100644 --- a/web-app/src/services/path/web.ts +++ b/web-app/src/services/path/web.ts @@ -37,4 +37,4 @@ export class WebPathService implements PathService { if (lastDot === -1 || lastDot === 0) return '' return basename.substring(lastDot) } -} \ No newline at end of file +} diff --git a/web-app/src/services/providers/default.ts b/web-app/src/services/providers/default.ts index 241138d28..3143d203f 100644 --- a/web-app/src/services/providers/default.ts +++ b/web-app/src/services/providers/default.ts @@ -22,4 +22,4 @@ export class DefaultProvidersService implements ProvidersService { fetch(): typeof fetch { return fetch } -} \ No newline at end of file +} diff --git a/web-app/src/services/providers/types.ts b/web-app/src/services/providers/types.ts index 1c6d81d90..324058f78 100644 --- a/web-app/src/services/providers/types.ts +++ b/web-app/src/services/providers/types.ts @@ -7,4 +7,4 @@ export interface ProvidersService { fetchModelsFromProvider(provider: ModelProvider): Promise updateSettings(providerName: string, settings: ProviderSetting[]): Promise fetch(): typeof fetch -} \ No newline at end of file +} diff --git a/web-app/src/services/theme/default.ts b/web-app/src/services/theme/default.ts index 421cc102e..8dc131be5 100644 --- a/web-app/src/services/theme/default.ts +++ b/web-app/src/services/theme/default.ts @@ -18,4 +18,4 @@ export class DefaultThemeService implements ThemeService { } } } -} \ No newline at end of file +} diff --git a/web-app/src/services/theme/tauri.ts b/web-app/src/services/theme/tauri.ts index 0f2f1f64d..e05d60c74 100644 --- a/web-app/src/services/theme/tauri.ts +++ b/web-app/src/services/theme/tauri.ts @@ -24,4 +24,4 @@ export class TauriThemeService extends DefaultThemeService { } } } -} \ No newline at end of file +} diff --git a/web-app/src/services/theme/types.ts b/web-app/src/services/theme/types.ts index abcf8fc44..21c208072 100644 --- a/web-app/src/services/theme/types.ts +++ b/web-app/src/services/theme/types.ts @@ -7,4 +7,4 @@ export type ThemeMode = 'light' | 'dark' | null export interface ThemeService { setTheme(theme: ThemeMode): Promise getCurrentWindow(): { setTheme: (theme: ThemeMode) => Promise } -} \ No newline at end of file +} diff --git a/web-app/src/services/theme/web.ts b/web-app/src/services/theme/web.ts index 39b1ff903..cf6ce01cf 100644 --- a/web-app/src/services/theme/web.ts +++ b/web-app/src/services/theme/web.ts @@ -22,4 +22,4 @@ export class WebThemeService implements ThemeService { } } } -} \ No newline at end of file +} diff --git a/web-app/src/services/threads/types.ts b/web-app/src/services/threads/types.ts index d0ce195cc..9f739624f 100644 --- a/web-app/src/services/threads/types.ts +++ b/web-app/src/services/threads/types.ts @@ -7,4 +7,4 @@ export interface ThreadsService { createThread(thread: Thread): Promise updateThread(thread: Thread): Promise deleteThread(threadId: string): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/updater/default.ts b/web-app/src/services/updater/default.ts index b648c5622..c0ba14aa2 100644 --- a/web-app/src/services/updater/default.ts +++ b/web-app/src/services/updater/default.ts @@ -19,4 +19,4 @@ export class DefaultUpdaterService implements UpdaterService { console.log('downloadAndInstallWithProgress called with callback:', typeof progressCallback) // No-op for non-Tauri platforms } -} \ No newline at end of file +} diff --git a/web-app/src/services/updater/tauri.ts b/web-app/src/services/updater/tauri.ts index 1db1ad294..a84951e7e 100644 --- a/web-app/src/services/updater/tauri.ts +++ b/web-app/src/services/updater/tauri.ts @@ -60,4 +60,4 @@ export class TauriUpdaterService extends DefaultUpdaterService { throw error } } -} \ No newline at end of file +} diff --git a/web-app/src/services/updater/types.ts b/web-app/src/services/updater/types.ts index c61642666..5dceee89e 100644 --- a/web-app/src/services/updater/types.ts +++ b/web-app/src/services/updater/types.ts @@ -24,4 +24,4 @@ export interface UpdaterService { downloadAndInstallWithProgress( progressCallback: (event: UpdateProgressEvent) => void ): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/window/default.ts b/web-app/src/services/window/default.ts index 08483743c..ea48a0173 100644 --- a/web-app/src/services/window/default.ts +++ b/web-app/src/services/window/default.ts @@ -40,4 +40,4 @@ export class DefaultWindowService implements WindowService { async openLocalApiServerLogsWindow(): Promise { // No-op } -} \ No newline at end of file +} diff --git a/web-app/src/services/window/tauri.ts b/web-app/src/services/window/tauri.ts index 56c038425..a6dd643c7 100644 --- a/web-app/src/services/window/tauri.ts +++ b/web-app/src/services/window/tauri.ts @@ -139,4 +139,4 @@ export class TauriWindowService extends DefaultWindowService { throw error } } -} \ No newline at end of file +} diff --git a/web-app/src/services/window/types.ts b/web-app/src/services/window/types.ts index 029f008aa..2f109427c 100644 --- a/web-app/src/services/window/types.ts +++ b/web-app/src/services/window/types.ts @@ -32,4 +32,4 @@ export interface WindowService { openLogsWindow(): Promise openSystemMonitorWindow(): Promise openLocalApiServerLogsWindow(): Promise -} \ No newline at end of file +} diff --git a/web-app/src/services/window/web.ts b/web-app/src/services/window/web.ts index 8cc01b8cb..da49077a1 100644 --- a/web-app/src/services/window/web.ts +++ b/web-app/src/services/window/web.ts @@ -61,4 +61,4 @@ export class WebWindowService implements WindowService { async openLocalApiServerLogsWindow(): Promise { console.warn('Cannot open local API server logs window in web environment') } -} \ No newline at end of file +} diff --git a/web-app/src/test/mocks/extensions-web.ts b/web-app/src/test/mocks/extensions-web.ts index 908f56c90..0748e905f 100644 --- a/web-app/src/test/mocks/extensions-web.ts +++ b/web-app/src/test/mocks/extensions-web.ts @@ -18,4 +18,4 @@ export class ConversationalExtensionWeb { export default {} // Export registry type for TypeScript compatibility -export type WebExtensionRegistry = Record \ No newline at end of file +export type WebExtensionRegistry = Record diff --git a/web-app/src/test/setup.ts b/web-app/src/test/setup.ts index 9fde8b66b..b2286c2f3 100644 --- a/web-app/src/test/setup.ts +++ b/web-app/src/test/setup.ts @@ -207,4 +207,4 @@ Object.defineProperty(window, 'matchMedia', { // runs a cleanup after each test case (e.g. clearing jsdom) afterEach(() => { cleanup() -}) \ No newline at end of file +}) diff --git a/web-app/src/utils/__tests__/error.test.ts b/web-app/src/utils/__tests__/error.test.ts index e6286060c..07efd387c 100644 --- a/web-app/src/utils/__tests__/error.test.ts +++ b/web-app/src/utils/__tests__/error.test.ts @@ -11,4 +11,4 @@ describe('error utilities', () => { expect(typeof OUT_OF_CONTEXT_SIZE).toBe('string') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/utils/__tests__/formatDate.test.ts b/web-app/src/utils/__tests__/formatDate.test.ts index 296d95da6..9d559f66f 100644 --- a/web-app/src/utils/__tests__/formatDate.test.ts +++ b/web-app/src/utils/__tests__/formatDate.test.ts @@ -100,4 +100,4 @@ describe('formatDate', () => { expect(formatted).not.toMatch(/\d{1,2}:\d{2}/i) expect(formatted).not.toMatch(/(AM|PM)/i) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/utils/__tests__/highlight.test.ts b/web-app/src/utils/__tests__/highlight.test.ts index 0277ba41a..0379c60a1 100644 --- a/web-app/src/utils/__tests__/highlight.test.ts +++ b/web-app/src/utils/__tests__/highlight.test.ts @@ -68,4 +68,4 @@ describe('highlight utility', () => { expect(result2).toBe('Hello World') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/utils/__tests__/number.test.ts b/web-app/src/utils/__tests__/number.test.ts index ad5848f3c..c31ba9d09 100644 --- a/web-app/src/utils/__tests__/number.test.ts +++ b/web-app/src/utils/__tests__/number.test.ts @@ -66,4 +66,4 @@ describe('toNumber', () => { expect(toNumber('\t42\n')).toBe(42) expect(toNumber('\r\n -5.5 \t')).toBe(-5.5) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/utils/__tests__/reasoning.test.ts b/web-app/src/utils/__tests__/reasoning.test.ts index f9717b753..76c7853d9 100644 --- a/web-app/src/utils/__tests__/reasoning.test.ts +++ b/web-app/src/utils/__tests__/reasoning.test.ts @@ -378,4 +378,4 @@ describe('ReasoningProcessor', () => { expect(result3).toBe('Second thought') }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/utils/__tests__/teamEmoji.test.ts b/web-app/src/utils/__tests__/teamEmoji.test.ts index eda023c01..2163f9cdf 100644 --- a/web-app/src/utils/__tests__/teamEmoji.test.ts +++ b/web-app/src/utils/__tests__/teamEmoji.test.ts @@ -39,4 +39,4 @@ describe('teamEmoji utility', () => { }) }) }) -}) \ No newline at end of file +}) diff --git a/web-app/src/utils/highlight.ts b/web-app/src/utils/highlight.ts index fe7cedcef..282ca9aa6 100644 --- a/web-app/src/utils/highlight.ts +++ b/web-app/src/utils/highlight.ts @@ -38,4 +38,4 @@ export function highlightFzfMatch(text: string, positions: number[], highlightCl : part.text ) .join(''); -} \ No newline at end of file +} diff --git a/yarn.lock b/yarn.lock index 23608118e..c167e87f0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3466,12 +3466,17 @@ __metadata: dependencies: "@janhq/core": "workspace:*" "@modelcontextprotocol/sdk": "npm:1.17.5" + "@tabler/icons-react": "npm:^3.34.0" + "@types/react": "npm:19.1.2" + react: "npm:19.0.0" typescript: "npm:5.9.2" vite: "npm:5.4.20" vitest: "npm:2.1.9" zustand: "npm:5.0.8" peerDependencies: "@janhq/core": "*" + "@tabler/icons-react": "*" + react: 19.0.0 zustand: 5.0.3 languageName: unknown linkType: soft @@ -3482,11 +3487,13 @@ __metadata: dependencies: "@npmcli/arborist": "npm:^7.1.0" "@types/node": "npm:^22.10.0" + "@types/react": "npm:19.1.2" "@vitest/coverage-v8": "npm:^2.1.8" "@vitest/ui": "npm:^2.1.8" eslint: "npm:8.57.0" happy-dom: "npm:^15.11.6" pacote: "npm:^21.0.0" + react: "npm:19.0.0" request: "npm:^2.88.2" request-progress: "npm:^3.0.0" rimraf: "npm:^6.0.1" @@ -3496,6 +3503,8 @@ __metadata: typescript: "npm:^5.8.3" ulidx: "npm:^2.3.0" vitest: "npm:^2.1.8" + peerDependencies: + react: 19.0.0 languageName: unknown linkType: soft @@ -6880,6 +6889,17 @@ __metadata: languageName: node linkType: hard +"@tabler/icons-react@npm:^3.34.0": + version: 3.35.0 + resolution: "@tabler/icons-react@npm:3.35.0" + dependencies: + "@tabler/icons": "npm:3.35.0" + peerDependencies: + react: ">= 16" + checksum: 10c0/8d280fcdae00916b001142ba0800ea05d8fa2acdcbd82f88a299b4141fb941237be2e826b86b1af710e038b4f8bb6f76f452c3309c29fd62398b4d5789c2b3e0 + languageName: node + linkType: hard + "@tabler/icons@npm:3.34.0": version: 3.34.0 resolution: "@tabler/icons@npm:3.34.0" @@ -6887,6 +6907,13 @@ __metadata: languageName: node linkType: hard +"@tabler/icons@npm:3.35.0": + version: 3.35.0 + resolution: "@tabler/icons@npm:3.35.0" + checksum: 10c0/93098828128ffed2cf412b39bd78992f93f25b22349a4e04523d2a018b7fe376ddeff105babcc3efedd707aa00b705425c7d9f598d6987552a563c62125795a2 + languageName: node + linkType: hard + "@tailwindcss/node@npm:4.1.4": version: 4.1.4 resolution: "@tailwindcss/node@npm:4.1.4"