Merge branch 'dev' into de_de-i18n

This commit is contained in:
Bob Ros 2025-07-07 22:18:55 +02:00 committed by GitHub
commit 0a3185f88d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 1544 additions and 645 deletions

View File

@ -49,20 +49,25 @@ jobs:
with:
node-version: 20
- name: 'Cleanup cache'
continue-on-error: true
run: |
rm -rf ~/jan
make clean
- name: Install dependencies
run: |
make config-yarn
yarn
yarn build:core
make lint
- name: Run test coverage
run: yarn test:coverage
run: |
yarn test:coverage
- name: Upload code coverage for ref branch
uses: actions/upload-artifact@v4
with:
name: ref-lcov.info
path: ./coverage/lcov.info
path: coverage/merged/lcov.info
test-on-macos:
runs-on: ${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) && 'macos-latest' || 'macos-selfhosted-12-arm64' }}
@ -78,10 +83,6 @@ jobs:
with:
node-version: 20
- name: Set IS_TEST environment variable
if: github.event.pull_request.head.repo.full_name == github.repository
run: echo "IS_TEST=true" >> $GITHUB_ENV
- name: 'Cleanup cache'
continue-on-error: true
run: |
@ -223,50 +224,44 @@ jobs:
path: electron/playwright-report/
retention-days: 2
# coverage-check:
# runs-on: ubuntu-latest
# needs: base_branch_cov
# continue-on-error: true
# if: (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) || github.event_name == 'push' || github.event_name == 'workflow_dispatch'
# steps:
# - name: Getting the repo
# uses: actions/checkout@v3
# with:
# fetch-depth: 0
coverage-check:
runs-on: ubuntu-latest
needs: base_branch_cov
continue-on-error: true
steps:
- name: Getting the repo
uses: actions/checkout@v3
with:
fetch-depth: 0
# - name: Installing node
# uses: actions/setup-node@v3
# with:
# node-version: 20
- name: Installing node
uses: actions/setup-node@v3
with:
node-version: 20
- name: 'Cleanup cache'
continue-on-error: true
run: |
rm -rf ~/jan
make clean
# - name: Install yarn
# run: npm install -g yarn
- name: Install dependencies
run: |
make lint
# - name: 'Cleanup cache'
# continue-on-error: true
# run: |
# rm -rf ~/jan
# make clean
- name: Run test coverage
run: |
yarn test:coverage
# - name: Download code coverage report from base branch
# uses: actions/download-artifact@v4
# with:
# name: ref-lcov.info
# - name: Linter and test coverage
# run: |
# export DISPLAY=$(w -h | awk 'NR==1 {print $2}')
# echo -e "Display ID: $DISPLAY"
# make lint
# yarn build:test
# yarn test:coverage
# - name: Generate Code Coverage report
# id: code-coverage
# uses: barecheck/code-coverage-action@v1
# with:
# github-token: ${{ secrets.GITHUB_TOKEN }}
# lcov-file: './coverage/lcov.info'
# base-lcov-file: './lcov.info'
# send-summary-comment: true
# show-annotations: 'warning'
- name: Download code coverage report from base branch
uses: actions/download-artifact@v4
with:
name: ref-lcov.info
- name: Generate Code Coverage report
id: code-coverage
uses: barecheck/code-coverage-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
lcov-file: './coverage/merged/lcov.info'
base-lcov-file: './lcov.info'
send-summary-comment: true
show-annotations: 'warning'

View File

@ -7,8 +7,8 @@ module.exports = {
},
runner: './testRunner.js',
transform: {
"^.+\\.tsx?$": [
"ts-jest",
'^.+\\.tsx?$': [
'ts-jest',
{
diagnostics: false,
},

View File

@ -32,7 +32,7 @@
"eslint-plugin-jest": "^27.9.0",
"jest": "^30.0.3",
"jest-junit": "^16.0.0",
"jest-runner": "^29.7.0",
"jest-runner": "^30.0.3",
"pacote": "^21.0.0",
"request": "^2.88.2",
"request-progress": "^3.0.0",

View File

@ -43,41 +43,41 @@ describe('EngineManager', () => {
})
describe('cortex engine migration', () => {
test('should map nitro to cortex engine', () => {
test.skip('should map nitro to cortex engine', () => {
const cortexEngine = new MockAIEngine(InferenceEngine.cortex)
// @ts-ignore
engineManager.register(cortexEngine)
// @ts-ignore
const retrievedEngine = engineManager.get<MockAIEngine>(InferenceEngine.nitro)
expect(retrievedEngine).toBe(cortexEngine)
})
test('should map cortex_llamacpp to cortex engine', () => {
test.skip('should map cortex_llamacpp to cortex engine', () => {
const cortexEngine = new MockAIEngine(InferenceEngine.cortex)
// @ts-ignore
engineManager.register(cortexEngine)
// @ts-ignore
const retrievedEngine = engineManager.get<MockAIEngine>(InferenceEngine.cortex_llamacpp)
expect(retrievedEngine).toBe(cortexEngine)
})
test('should map cortex_onnx to cortex engine', () => {
test.skip('should map cortex_onnx to cortex engine', () => {
const cortexEngine = new MockAIEngine(InferenceEngine.cortex)
// @ts-ignore
engineManager.register(cortexEngine)
// @ts-ignore
const retrievedEngine = engineManager.get<MockAIEngine>(InferenceEngine.cortex_onnx)
expect(retrievedEngine).toBe(cortexEngine)
})
test('should map cortex_tensorrtllm to cortex engine', () => {
test.skip('should map cortex_tensorrtllm to cortex engine', () => {
const cortexEngine = new MockAIEngine(InferenceEngine.cortex)
// @ts-ignore
engineManager.register(cortexEngine)
// @ts-ignore
const retrievedEngine = engineManager.get<MockAIEngine>(InferenceEngine.cortex_tensorrtllm)
expect(retrievedEngine).toBe(cortexEngine)
@ -89,19 +89,19 @@ describe('EngineManager', () => {
const mockEngineManager = new EngineManager()
// @ts-ignore
window.core = { engineManager: mockEngineManager }
const instance = EngineManager.instance()
expect(instance).toBe(mockEngineManager)
// Clean up
// @ts-ignore
delete window.core
})
test('should create a new instance if window.core.engineManager is not available', () => {
// @ts-ignore
delete window.core
const instance = EngineManager.instance()
expect(instance).toBeInstanceOf(EngineManager)
})

View File

@ -23,7 +23,7 @@ describe('fs module', () => {
it('should call writeFileSync with correct arguments', () => {
const args = ['path/to/file', 'data']
fs.writeFileSync(...args)
expect(globalThis.core.api.writeFileSync).toHaveBeenCalledWith(...args)
expect(globalThis.core.api.writeFileSync).toHaveBeenCalledWith({ args })
})
it('should call writeBlob with correct arguments', async () => {
@ -90,8 +90,7 @@ describe('fs module', () => {
it('should call fileStat with correct arguments', async () => {
const path = 'path/to/file'
const outsideJanDataFolder = true
await fs.fileStat(path, outsideJanDataFolder)
expect(globalThis.core.api.fileStat).toHaveBeenCalledWith(path, outsideJanDataFolder)
await fs.fileStat(path)
expect(globalThis.core.api.fileStat).toHaveBeenCalledWith({ args: path })
})
})

View File

@ -1,7 +1,7 @@
import { useExtensions } from './index'
import { useExtensions } from './index'
test('testUseExtensionsMissingPath', () => {
expect(() => useExtensions(undefined as any)).toThrowError('A path to the extensions folder is required to use extensions')
})
test('testUseExtensionsMissingPath', () => {
expect(() => useExtensions(undefined as any)).toThrow(
'A path to the extensions folder is required to use extensions'
)
})

View File

@ -1,30 +1,29 @@
import { Model, ModelSettingParams, ModelRuntimeParams } from '../model'
import { InferenceEngine } from '../engine'
test.skip('testValidModelCreation', () => {
const model: Model = {
object: 'model',
version: '1.0',
format: 'format1',
sources: [{ filename: 'model.bin', url: 'http://example.com/model.bin' }],
id: 'model1',
name: 'Test Model',
created: Date.now(),
description: 'A cool model from Huggingface',
settings: { ctx_len: 100, ngl: 50, embedding: true },
parameters: { temperature: 0.5, token_limit: 100, top_k: 10 },
metadata: { author: 'Author', tags: ['tag1', 'tag2'], size: 100 },
engine: InferenceEngine.anthropic,
}
import { Model, ModelSettingParams, ModelRuntimeParams, InferenceEngine } from '../model'
test('testValidModelCreation', () => {
const model: Model = {
object: 'model',
version: '1.0',
format: 'format1',
sources: [{ filename: 'model.bin', url: 'http://example.com/model.bin' }],
id: 'model1',
name: 'Test Model',
created: Date.now(),
description: 'A cool model from Huggingface',
settings: { ctx_len: 100, ngl: 50, embedding: true },
parameters: { temperature: 0.5, token_limit: 100, top_k: 10 },
metadata: { author: 'Author', tags: ['tag1', 'tag2'], size: 100 },
engine: InferenceEngine.anthropic
};
expect(model).toBeDefined();
expect(model.object).toBe('model');
expect(model.version).toBe('1.0');
expect(model.sources).toHaveLength(1);
expect(model.sources[0].filename).toBe('model.bin');
expect(model.settings).toBeDefined();
expect(model.parameters).toBeDefined();
expect(model.metadata).toBeDefined();
expect(model.engine).toBe(InferenceEngine.anthropic);
});
expect(model).toBeDefined()
expect(model.object).toBe('model')
expect(model.version).toBe('1.0')
expect(model.sources).toHaveLength(1)
expect(model.sources[0].filename).toBe('model.bin')
expect(model.settings).toBeDefined()
expect(model.parameters).toBeDefined()
expect(model.metadata).toBeDefined()
expect(model.engine).toBe(InferenceEngine.anthropic)
})

View File

@ -1,19 +1,9 @@
import * as SettingComponent from './settingComponent'
import { createSettingComponent } from './settingComponent';
it('should not throw any errors when importing settingComponent', () => {
expect(() => require('./settingComponent')).not.toThrow()
})
it('should throw an error when creating a setting component with invalid controller type', () => {
const props: SettingComponentProps = {
key: 'invalidControllerKey',
title: 'Invalid Controller Title',
description: 'Invalid Controller Description',
controllerType: 'invalid' as any,
controllerProps: {
placeholder: 'Enter text',
value: 'Initial Value',
type: 'text',
textAlign: 'left',
inputActions: ['unobscure'],
},
};
expect(() => createSettingComponent(props)).toThrowError();
});
it('should export SettingComponentProps type', () => {
expect(SettingComponent).toBeDefined()
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -94,7 +94,7 @@ const Hero = () => {
</p>
<div className="w-4/5 mx-auto mt-10 relative">
<ThemeImage
className="absolute object-cover w-full object-center mx-auto h-full top-0 left-0 scale-150"
className="absolute object-cover w-full object-center mx-auto h-full top-0 left-0 scale-125"
source={{
light: '/assets/images/homepage/glow.png',
dark: '/assets/images/homepage/glow.png',

View File

@ -587,6 +587,22 @@ __metadata:
languageName: node
linkType: hard
"@isaacs/balanced-match@npm:^4.0.1":
version: 4.0.1
resolution: "@isaacs/balanced-match@npm:4.0.1"
checksum: 10c0/7da011805b259ec5c955f01cee903da72ad97c5e6f01ca96197267d3f33103d5b2f8a1af192140f3aa64526c593c8d098ae366c2b11f7f17645d12387c2fd420
languageName: node
linkType: hard
"@isaacs/brace-expansion@npm:^5.0.0":
version: 5.0.0
resolution: "@isaacs/brace-expansion@npm:5.0.0"
dependencies:
"@isaacs/balanced-match": "npm:^4.0.1"
checksum: 10c0/b4d4812f4be53afc2c5b6c545001ff7a4659af68d4484804e9d514e183d20269bb81def8682c01a22b17c4d6aed14292c8494f7d2ac664e547101c1a905aa977
languageName: node
linkType: hard
"@isaacs/cliui@npm:^8.0.2":
version: 8.0.2
resolution: "@isaacs/cliui@npm:8.0.2"
@ -636,7 +652,7 @@ __metadata:
dependencies:
"@janhq/core": ../../core/package.tgz
cpx: "npm:^1.5.0"
rimraf: "npm:^3.0.2"
rimraf: "npm:^6.0.1"
rolldown: "npm:1.0.0-beta.1"
run-script-os: "npm:^1.1.6"
ts-loader: "npm:^9.5.0"
@ -650,7 +666,7 @@ __metadata:
dependencies:
"@janhq/core": ../../core/package.tgz
cpx: "npm:^1.5.0"
rimraf: "npm:^3.0.2"
rimraf: "npm:^6.0.1"
rolldown: "npm:1.0.0-beta.1"
ts-loader: "npm:^9.5.0"
typescript: "npm:^5.7.2"
@ -659,64 +675,89 @@ __metadata:
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5531aa&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f2e0b5&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/ee7fe21267cf795dba890781d1e7807a6cb3ecb915ce9ecbd3a8386a2ebc916a8b70a775ce5d9d9f74d2ec29e20b65cea4ef6cdd0ea250a8ff2d5e6bd2237b1e
checksum: 10c0/6f085adb6211c2d0735c2ebccf38c26b90c78a786d6d327fab3e1a9e10380a522429e19f9b9752f7c5a42eb6d80f37c7022c5146b2e4ca1fada609dccbea4194
languageName: node
linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5531aa&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f2e0b5&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/ee7fe21267cf795dba890781d1e7807a6cb3ecb915ce9ecbd3a8386a2ebc916a8b70a775ce5d9d9f74d2ec29e20b65cea4ef6cdd0ea250a8ff2d5e6bd2237b1e
checksum: 10c0/6f085adb6211c2d0735c2ebccf38c26b90c78a786d6d327fab3e1a9e10380a522429e19f9b9752f7c5a42eb6d80f37c7022c5146b2e4ca1fada609dccbea4194
languageName: node
linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fdownload-extension%40workspace%3Adownload-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f2e0b5&locator=%40janhq%2Fdownload-extension%40workspace%3Adownload-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/6f085adb6211c2d0735c2ebccf38c26b90c78a786d6d327fab3e1a9e10380a522429e19f9b9752f7c5a42eb6d80f37c7022c5146b2e4ca1fada609dccbea4194
languageName: node
linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5531aa&locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f2e0b5&locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/ee7fe21267cf795dba890781d1e7807a6cb3ecb915ce9ecbd3a8386a2ebc916a8b70a775ce5d9d9f74d2ec29e20b65cea4ef6cdd0ea250a8ff2d5e6bd2237b1e
checksum: 10c0/6f085adb6211c2d0735c2ebccf38c26b90c78a786d6d327fab3e1a9e10380a522429e19f9b9752f7c5a42eb6d80f37c7022c5146b2e4ca1fada609dccbea4194
languageName: node
linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fhardware-management-extension%40workspace%3Ahardware-management-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5531aa&locator=%40janhq%2Fhardware-management-extension%40workspace%3Ahardware-management-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f2e0b5&locator=%40janhq%2Fhardware-management-extension%40workspace%3Ahardware-management-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/ee7fe21267cf795dba890781d1e7807a6cb3ecb915ce9ecbd3a8386a2ebc916a8b70a775ce5d9d9f74d2ec29e20b65cea4ef6cdd0ea250a8ff2d5e6bd2237b1e
checksum: 10c0/6f085adb6211c2d0735c2ebccf38c26b90c78a786d6d327fab3e1a9e10380a522429e19f9b9752f7c5a42eb6d80f37c7022c5146b2e4ca1fada609dccbea4194
languageName: node
linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5531aa&locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f2e0b5&locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/ee7fe21267cf795dba890781d1e7807a6cb3ecb915ce9ecbd3a8386a2ebc916a8b70a775ce5d9d9f74d2ec29e20b65cea4ef6cdd0ea250a8ff2d5e6bd2237b1e
checksum: 10c0/6f085adb6211c2d0735c2ebccf38c26b90c78a786d6d327fab3e1a9e10380a522429e19f9b9752f7c5a42eb6d80f37c7022c5146b2e4ca1fada609dccbea4194
languageName: node
linkType: hard
"@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension":
version: 0.1.10
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5531aa&locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f2e0b5&locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/ee7fe21267cf795dba890781d1e7807a6cb3ecb915ce9ecbd3a8386a2ebc916a8b70a775ce5d9d9f74d2ec29e20b65cea4ef6cdd0ea250a8ff2d5e6bd2237b1e
checksum: 10c0/6f085adb6211c2d0735c2ebccf38c26b90c78a786d6d327fab3e1a9e10380a522429e19f9b9752f7c5a42eb6d80f37c7022c5146b2e4ca1fada609dccbea4194
languageName: node
linkType: hard
"@janhq/download-extension@workspace:download-extension":
version: 0.0.0-use.local
resolution: "@janhq/download-extension@workspace:download-extension"
dependencies:
"@janhq/core": ../../core/package.tgz
"@tauri-apps/api": "npm:^2.5.0"
cpx: "npm:^1.5.0"
rimraf: "npm:^6.0.1"
rolldown: "npm:1.0.0-beta.1"
run-script-os: "npm:^1.1.6"
typescript: "npm:5.8.3"
vitest: "npm:^3.0.6"
languageName: unknown
linkType: soft
"@janhq/engine-management-extension@workspace:engine-management-extension":
version: 0.0.0-use.local
resolution: "@janhq/engine-management-extension@workspace:engine-management-extension"
@ -1436,6 +1477,13 @@ __metadata:
languageName: node
linkType: hard
"@tauri-apps/api@npm:^2.5.0":
version: 2.6.0
resolution: "@tauri-apps/api@npm:2.6.0"
checksum: 10c0/211353d951c7e3e5298f074ec762b5853ff0cdee261478c27db1e450fcf3d6f2c03a616483abbf9dfc79f13c6dfcfa7db0b790c1384c113951c0d694809f05ef
languageName: node
linkType: hard
"@tybys/wasm-util@npm:^0.9.0":
version: 0.9.0
resolution: "@tybys/wasm-util@npm:0.9.0"
@ -2587,7 +2635,7 @@ __metadata:
languageName: node
linkType: hard
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3":
"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.3, cross-spawn@npm:^7.0.6":
version: 7.0.6
resolution: "cross-spawn@npm:7.0.6"
dependencies:
@ -3392,6 +3440,16 @@ __metadata:
languageName: node
linkType: hard
"foreground-child@npm:^3.3.1":
version: 3.3.1
resolution: "foreground-child@npm:3.3.1"
dependencies:
cross-spawn: "npm:^7.0.6"
signal-exit: "npm:^4.0.1"
checksum: 10c0/8986e4af2430896e65bc2788d6679067294d6aee9545daefc84923a0a4b399ad9c7a3ea7bd8c0b2b80fdf4a92de4c69df3f628233ff3224260e9c1541a9e9ed3
languageName: node
linkType: hard
"fragment-cache@npm:^0.2.1":
version: 0.2.1
resolution: "fragment-cache@npm:0.2.1"
@ -3583,6 +3641,22 @@ __metadata:
languageName: node
linkType: hard
"glob@npm:^11.0.0":
version: 11.0.3
resolution: "glob@npm:11.0.3"
dependencies:
foreground-child: "npm:^3.3.1"
jackspeak: "npm:^4.1.1"
minimatch: "npm:^10.0.3"
minipass: "npm:^7.1.2"
package-json-from-dist: "npm:^1.0.0"
path-scurry: "npm:^2.0.0"
bin:
glob: dist/esm/bin.mjs
checksum: 10c0/7d24457549ec2903920dfa3d8e76850e7c02aa709122f0164b240c712f5455c0b457e6f2a1eee39344c6148e39895be8094ae8cfef7ccc3296ed30bce250c661
languageName: node
linkType: hard
"glob@npm:^7.0.5, glob@npm:^7.1.3, glob@npm:^7.1.4":
version: 7.2.3
resolution: "glob@npm:7.2.3"
@ -4205,6 +4279,15 @@ __metadata:
languageName: node
linkType: hard
"jackspeak@npm:^4.1.1":
version: 4.1.1
resolution: "jackspeak@npm:4.1.1"
dependencies:
"@isaacs/cliui": "npm:^8.0.2"
checksum: 10c0/84ec4f8e21d6514db24737d9caf65361511f75e5e424980eebca4199f400874f45e562ac20fa8aeb1dd20ca2f3f81f0788b6e9c3e64d216a5794fd6f30e0e042
languageName: node
linkType: hard
"jake@npm:^10.8.5":
version: 10.9.2
resolution: "jake@npm:10.9.2"
@ -4829,6 +4912,13 @@ __metadata:
languageName: node
linkType: hard
"lru-cache@npm:^11.0.0":
version: 11.1.0
resolution: "lru-cache@npm:11.1.0"
checksum: 10c0/85c312f7113f65fae6a62de7985348649937eb34fb3d212811acbf6704dc322a421788aca253b62838f1f07049a84cc513d88f494e373d3756514ad263670a64
languageName: node
linkType: hard
"lru-cache@npm:^5.1.1":
version: 5.1.1
resolution: "lru-cache@npm:5.1.1"
@ -5028,6 +5118,15 @@ __metadata:
languageName: node
linkType: hard
"minimatch@npm:^10.0.3":
version: 10.0.3
resolution: "minimatch@npm:10.0.3"
dependencies:
"@isaacs/brace-expansion": "npm:^5.0.0"
checksum: 10c0/e43e4a905c5d70ac4cec8530ceaeccb9c544b1ba8ac45238e2a78121a01c17ff0c373346472d221872563204eabe929ad02669bb575cb1f0cc30facab369f70f
languageName: node
linkType: hard
"minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2":
version: 3.1.2
resolution: "minimatch@npm:3.1.2"
@ -5574,6 +5673,16 @@ __metadata:
languageName: node
linkType: hard
"path-scurry@npm:^2.0.0":
version: 2.0.0
resolution: "path-scurry@npm:2.0.0"
dependencies:
lru-cache: "npm:^11.0.0"
minipass: "npm:^7.1.2"
checksum: 10c0/3da4adedaa8e7ef8d6dc4f35a0ff8f05a9b4d8365f2b28047752b62d4c1ad73eec21e37b1579ef2d075920157856a3b52ae8309c480a6f1a8bbe06ff8e52b33c
languageName: node
linkType: hard
"path-type@npm:^1.0.0":
version: 1.1.0
resolution: "path-type@npm:1.1.0"
@ -5987,6 +6096,18 @@ __metadata:
languageName: node
linkType: hard
"rimraf@npm:^6.0.1":
version: 6.0.1
resolution: "rimraf@npm:6.0.1"
dependencies:
glob: "npm:^11.0.0"
package-json-from-dist: "npm:^1.0.0"
bin:
rimraf: dist/esm/bin.mjs
checksum: 10c0/b30b6b072771f0d1e73b4ca5f37bb2944ee09375be9db5f558fcd3310000d29dfcfa93cf7734d75295ad5a7486dc8e40f63089ced1722a664539ffc0c3ece8c6
languageName: node
linkType: hard
"rolldown@npm:1.0.0-beta.1":
version: 1.0.0-beta.1
resolution: "rolldown@npm:1.0.0-beta.1"
@ -6979,6 +7100,16 @@ __metadata:
languageName: node
linkType: hard
"typescript@npm:5.8.3":
version: 5.8.3
resolution: "typescript@npm:5.8.3"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10c0/5f8bb01196e542e64d44db3d16ee0e4063ce4f3e3966df6005f2588e86d91c03e1fb131c2581baf0fb65ee79669eea6e161cd448178986587e9f6844446dbb48
languageName: node
linkType: hard
"typescript@npm:^5.3.3, typescript@npm:^5.7.2":
version: 5.7.2
resolution: "typescript@npm:5.7.2"
@ -6999,6 +7130,16 @@ __metadata:
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A5.8.3#optional!builtin<compat/typescript>":
version: 5.8.3
resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin<compat/typescript>::version=5.8.3&hash=5786d5"
bin:
tsc: bin/tsc
tsserver: bin/tsserver
checksum: 10c0/39117e346ff8ebd87ae1510b3a77d5d92dae5a89bde588c747d25da5c146603a99c8ee588c7ef80faaf123d89ed46f6dbd918d534d641083177d5fac38b8a1cb
languageName: node
linkType: hard
"typescript@patch:typescript@npm%3A^5.3.3#optional!builtin<compat/typescript>, typescript@patch:typescript@npm%3A^5.7.2#optional!builtin<compat/typescript>":
version: 5.7.2
resolution: "typescript@patch:typescript@npm%3A5.7.2#optional!builtin<compat/typescript>::version=5.7.2&hash=5786d5"

View File

@ -1,3 +1,3 @@
module.exports = {
projects: ['<rootDir>/core', '<rootDir>/web', '<rootDir>/joi'],
projects: ['<rootDir>/core'],
}

View File

@ -12,8 +12,11 @@
"lint": "yarn workspace @janhq/web-app lint",
"dev": "yarn dev:tauri",
"build": "yarn build:web && yarn build:tauri",
"test": "yarn workspace @janhq/web-app test",
"test:coverage": "yarn workspace @janhq/web-app test",
"test": "jest && yarn workspace @janhq/web-app test",
"test:coverage": "yarn test:coverage:jest && yarn test:coverage:vitest && yarn merge:coverage",
"test:coverage:jest": "jest --coverage --coverageDirectory=coverage/jest",
"test:coverage:vitest": "yarn workspace @janhq/web-app test:coverage",
"merge:coverage": "node scripts/merge-coverage.js",
"test:prepare": "yarn build:icon && yarn copy:lib && yarn copy:assets:tauri && yarn build --no-bundle ",
"test:e2e:linux": "yarn test:prepare && xvfb-run yarn workspace tests-e2-js test",
"test:e2e:win32": "yarn test:prepare && yarn workspace tests-e2-js test",
@ -45,8 +48,13 @@
"cpx": "^1.5.0",
"cross-env": "^7.0.3",
"husky": "^9.1.5",
"istanbul-api": "^3.0.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-reports": "^3.1.7",
"jest": "^30.0.3",
"jest-environment-jsdom": "^29.7.0",
"nyc": "^17.1.0",
"rimraf": "^3.0.2",
"run-script-os": "^1.1.6",
"tar": "^4.4.19",

145
scripts/merge-coverage.js Normal file
View File

@ -0,0 +1,145 @@
const { createCoverageMap } = require('istanbul-lib-coverage')
const { createReporter } = require('istanbul-api')
const fs = require('fs')
const path = require('path')
const coverageDir = path.join(__dirname, '../coverage')
const jestCoverage = path.join(coverageDir, 'jest/coverage-final.json')
const vitestCoverage = path.join(coverageDir, 'vitest/coverage-final.json')
const mergedDir = path.join(coverageDir, 'merged')
function normalizePath(filePath, workspace) {
if (workspace === 'jest') {
return `[CORE] ${filePath}`
} else if (workspace === 'vitest') {
return `[WEB-APP] ${filePath}`
}
return filePath
}
async function mergeCoverage() {
const map = createCoverageMap({})
console.log('🔍 Checking coverage files...')
console.log('Jest coverage path:', jestCoverage)
console.log('Vitest coverage path:', vitestCoverage)
console.log('Jest file exists:', fs.existsSync(jestCoverage))
console.log('Vitest file exists:', fs.existsSync(vitestCoverage))
// Load Jest coverage (core workspace)
if (fs.existsSync(jestCoverage)) {
const jestData = JSON.parse(fs.readFileSync(jestCoverage, 'utf8'))
console.log('Jest data keys:', Object.keys(jestData).length)
map.merge(jestData)
console.log('✓ Merged Jest coverage (core workspace)')
} else {
console.log('❌ Jest coverage file not found')
}
// Load Vitest coverage (web-app workspace)
if (fs.existsSync(vitestCoverage)) {
const vitestData = JSON.parse(fs.readFileSync(vitestCoverage, 'utf8'))
console.log('Vitest data keys:', Object.keys(vitestData).length)
map.merge(vitestData)
console.log('✓ Merged Vitest coverage (web-app workspace)')
} else {
console.log('❌ Vitest coverage file not found')
}
console.log('📊 Total files in coverage map:', map.files().length)
// Create merged directory
if (!fs.existsSync(mergedDir)) {
fs.mkdirSync(mergedDir, { recursive: true })
console.log('✓ Created merged directory')
}
try {
console.log('🔄 Generating reports...')
const context = require('istanbul-lib-report').createContext({
dir: mergedDir,
coverageMap: map,
})
const htmlReporter = require('istanbul-reports').create('html')
const lcovReporter = require('istanbul-reports').create('lcov')
const textReporter = require('istanbul-reports').create('text')
// Generate reports
htmlReporter.execute(context)
lcovReporter.execute(context)
textReporter.execute(context)
console.log('\n📊 Coverage reports merged successfully!')
console.log('📁 HTML report: coverage/merged/index.html')
console.log('📁 LCOV report: coverage/merged/lcov.info')
// Check if files were created
if (fs.existsSync(mergedDir)) {
const mergedFiles = fs.readdirSync(mergedDir)
console.log('📁 Files in merged directory:', mergedFiles)
}
} catch (error) {
console.error('❌ Error generating reports:', error.message)
console.error('Stack trace:', error.stack)
throw error
}
// Generate separate reports for each workspace
await generateWorkspaceReports()
}
async function generateWorkspaceReports() {
// Generate separate core report
if (fs.existsSync(jestCoverage)) {
const coreMap = createCoverageMap({})
const jestData = JSON.parse(fs.readFileSync(jestCoverage, 'utf8'))
coreMap.merge(jestData)
const coreDir = path.join(coverageDir, 'core-only')
if (!fs.existsSync(coreDir)) {
fs.mkdirSync(coreDir, { recursive: true })
}
const coreContext = require('istanbul-lib-report').createContext({
dir: coreDir,
coverageMap: coreMap,
})
const htmlReporter = require('istanbul-reports').create('html')
const textSummaryReporter =
require('istanbul-reports').create('text-summary')
htmlReporter.execute(coreContext)
textSummaryReporter.execute(coreContext)
console.log('📁 Core-only report: coverage/core-only/index.html')
}
// Generate separate web-app report
if (fs.existsSync(vitestCoverage)) {
const webAppMap = createCoverageMap({})
const vitestData = JSON.parse(fs.readFileSync(vitestCoverage, 'utf8'))
webAppMap.merge(vitestData)
const webAppDir = path.join(coverageDir, 'web-app-only')
if (!fs.existsSync(webAppDir)) {
fs.mkdirSync(webAppDir, { recursive: true })
}
const webAppContext = require('istanbul-lib-report').createContext({
dir: webAppDir,
coverageMap: webAppMap,
})
const htmlReporter = require('istanbul-reports').create('html')
const textSummaryReporter =
require('istanbul-reports').create('text-summary')
htmlReporter.execute(webAppContext)
textSummaryReporter.execute(webAppContext)
console.log('📁 Web-app-only report: coverage/web-app-only/index.html')
}
}
mergeCoverage().catch(console.error)

View File

@ -8,7 +8,8 @@
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview",
"test": "vitest"
"test": "vitest --run",
"test:coverage": "vitest --coverage --run"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
@ -45,6 +46,7 @@
"fzf": "^0.5.2",
"i18next": "^25.0.1",
"katex": "^0.16.22",
"lodash.clonedeep": "^4.5.0",
"lodash.debounce": "^4.0.8",
"lucide-react": "^0.522.0",
"motion": "^12.10.5",
@ -77,16 +79,24 @@
"@eslint/js": "^9.22.0",
"@tanstack/router-plugin": "^1.116.1",
"@types/culori": "^2.1.1",
"@types/istanbul-lib-report": "^3",
"@types/istanbul-reports": "^3",
"@types/lodash.clonedeep": "^4",
"@types/lodash.debounce": "^4",
"@types/node": "^22.14.1",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
"@vitest/coverage-v8": "3.2.4",
"clsx": "^2.1.1",
"eslint": "^9.22.0",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.19",
"globals": "^16.0.0",
"istanbul-api": "^3.0.0",
"istanbul-lib-coverage": "^3.2.2",
"istanbul-lib-report": "^3.0.1",
"istanbul-reports": "^3.1.7",
"tailwind-merge": "^3.2.0",
"typescript": "~5.8.3",
"typescript-eslint": "^8.26.1",

View File

@ -26,7 +26,6 @@ import {
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Textarea } from '@/components/ui/textarea'
import { toast } from 'sonner'
import {
Tooltip,
TooltipContent,
@ -75,6 +74,69 @@ const CopyButton = ({ text }: { text: string }) => {
)
}
const EditDialog = ({
message,
setMessage,
}: {
message: string
setMessage: (message: string) => void
}) => {
const { t } = useTranslation()
const [draft, setDraft] = useState(message)
const handleSave = () => {
if (draft !== message) {
setMessage(draft)
}
}
return (
<Dialog>
<DialogTrigger>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex outline-0 items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative">
<IconPencil size={16} />
</div>
</TooltipTrigger>
<TooltipContent>
<p>{t('edit')}</p>
</TooltipContent>
</Tooltip>
</DialogTrigger>
<DialogContent className="w-3/4 h-3/4">
<DialogHeader>
<DialogTitle>{t('common:dialogs.editMessage.title')}</DialogTitle>
<Textarea
value={draft}
onChange={(e) => setDraft(e.target.value)}
className="mt-2 resize-none h-full w-full"
onKeyDown={(e) => {
// Prevent key from being captured by parent components
e.stopPropagation()
}}
/>
<DialogFooter className="mt-2 flex items-center">
<DialogClose asChild>
<Button variant="link" size="sm" className="hover:no-underline">
Cancel
</Button>
</DialogClose>
<DialogClose asChild>
<Button
disabled={draft === message || !draft}
onClick={handleSave}
>
Save
</Button>
</DialogClose>
</DialogFooter>
</DialogHeader>
</DialogContent>
</Dialog>
)
}
// Use memo to prevent unnecessary re-renders, but allow re-renders when props change
export const ThreadContent = memo(
(
@ -85,9 +147,9 @@ export const ThreadContent = memo(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
streamTools?: any
contextOverflowModal?: React.ReactNode | null
updateMessage?: (item: ThreadMessage, message: string) => void
}
) => {
const [message, setMessage] = useState(item.content?.[0]?.text?.value || '')
const { t } = useTranslation()
// Use useMemo to stabilize the components prop
@ -166,23 +228,6 @@ export const ThreadContent = memo(
}
}, [deleteMessage, getMessages, item])
const editMessage = useCallback(
(messageId: string) => {
const threadMessages = getMessages(item.thread_id)
const index = threadMessages.findIndex((msg) => msg.id === messageId)
if (index === -1) return
// Delete all messages after the edited message
for (let i = threadMessages.length - 1; i >= index; i--) {
deleteMessage(threadMessages[i].thread_id, threadMessages[i].id)
}
sendMessage(message)
},
[deleteMessage, getMessages, item.thread_id, message, sendMessage]
)
const isToolCalls =
item.metadata &&
'tool_calls' in item.metadata &&
@ -209,61 +254,14 @@ export const ThreadContent = memo(
</div>
</div>
<div className="flex items-center justify-end gap-2 text-main-view-fg/60 text-xs mt-2">
<Dialog>
<DialogTrigger>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex outline-0 items-center gap-1 hover:text-accent transition-colors cursor-pointer group relative">
<IconPencil size={16} />
</div>
</TooltipTrigger>
<TooltipContent>
<p>{t('edit')}</p>
</TooltipContent>
</Tooltip>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('common:dialogs.editMessage.title')}</DialogTitle>
<Textarea
value={message}
onChange={(e) => {
setMessage(e.target.value)
}}
className="mt-2 resize-none"
onKeyDown={(e) => {
// Prevent key from being captured by parent components
e.stopPropagation()
}}
/>
<DialogFooter className="mt-2 flex items-center">
<DialogClose asChild>
<Button
variant="link"
size="sm"
className="hover:no-underline"
>
Cancel
</Button>
</DialogClose>
<DialogClose asChild>
<Button
disabled={!message}
onClick={() => {
editMessage(item.id)
toast.success(t('common:toast.editMessage.title'), {
id: 'edit-message',
description: t('common:toast.editMessage.description'),
})
}}
>
Save
</Button>
</DialogClose>
</DialogFooter>
</DialogHeader>
</DialogContent>
</Dialog>
<EditDialog
message={item.content?.[0]?.text.value}
setMessage={(message) => {
if (item.updateMessage) {
item.updateMessage(item, message)
}
}}
/>
<Tooltip>
<TooltipTrigger asChild>
<button
@ -360,6 +358,12 @@ export const ThreadContent = memo(
'hidden'
)}
>
<EditDialog
message={item.content?.[0]?.text.value}
setMessage={(message) =>
item.updateMessage && item.updateMessage(item, message)
}
/>
<CopyButton text={item.content?.[0]?.text.value || ''} />
<Tooltip>
<TooltipTrigger asChild>
@ -391,7 +395,9 @@ export const ThreadContent = memo(
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('common:dialogs.messageMetadata.title')}</DialogTitle>
<DialogTitle>
{t('common:dialogs.messageMetadata.title')}
</DialogTitle>
<div className="space-y-2">
<div className="border border-main-view-fg/10 rounded-md overflow-hidden">
<CodeEditor

View File

@ -138,6 +138,14 @@ export const sendCompletion = async (
baseURL: provider.base_url,
// Use Tauri's fetch to avoid CORS issues only for openai-compatible provider
...(providerName === 'openai-compatible' && { fetch: fetchTauri }),
// OpenRouter identification headers for Jan
// ref: https://openrouter.ai/docs/api-reference/overview#headers
...(provider.provider === 'openrouter' && {
defaultHeaders: {
'HTTP-Referer': 'https://jan.ai',
'X-Title': 'Jan',
},
}),
} as ExtendedConfigOptions)
if (
thread.model.id &&
@ -286,10 +294,10 @@ export const extractToolCall = (
* @param calls
* @param builder
* @param message
* @param content
* @param approvedTools - Record of approved tools per thread
* @param showModal - Function to show approval modal, returns true if approved
* @param allowAllMCPPermissions - Global setting to allow all MCP permissions without modal
* @param abortController
* @param approvedTools
* @param showModal
* @param allowAllMCPPermissions
*/
export const postMessageProcessing = async (
calls: ChatCompletionMessageToolCall[],

View File

@ -2,8 +2,10 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import { createFileRoute, useParams } from '@tanstack/react-router'
import { UIEventHandler } from 'react'
import debounce from 'lodash.debounce'
import cloneDeep from 'lodash.clonedeep'
import { cn } from '@/lib/utils'
import { ArrowDown } from 'lucide-react'
import { Play } from 'lucide-react'
import HeaderPage from '@/containers/HeaderPage'
import { useThreads } from '@/hooks/useThreads'
@ -18,7 +20,9 @@ import { useAppState } from '@/hooks/useAppState'
import DropdownAssistant from '@/containers/DropdownAssistant'
import { useAssistant } from '@/hooks/useAssistant'
import { useAppearance } from '@/hooks/useAppearance'
import { ContentType, ThreadMessage } from '@janhq/core'
import { useTranslation } from '@/i18n/react-i18next-compat'
import { useChat } from '@/hooks/useChat'
import { useSmallScreen } from '@/hooks/useMediaQuery'
// as route.threadsDetail
@ -38,6 +42,7 @@ function ThreadDetail() {
const { setMessages } = useMessages()
const { streamingContent } = useAppState()
const { appMainViewBgColor, chatWidth } = useAppearance()
const { sendMessage } = useChat()
const isSmallScreen = useSmallScreen()
const { messages } = useMessages(
@ -180,6 +185,26 @@ function ThreadDetail() {
lastScrollTopRef.current = scrollTop
}
const updateMessage = (item: ThreadMessage, message: string) => {
const newMessages: ThreadMessage[] = messages.map((m) => {
if (m.id === item.id) {
const msg: ThreadMessage = cloneDeep(m)
msg.content = [
{
type: ContentType.Text,
text: {
value: message,
annotations: m.content[0].text?.annotations ?? [],
},
},
]
return msg
}
return m
})
setMessages(threadId, newMessages)
}
// Use a shorter debounce time for more responsive scrolling
const debouncedScroll = debounce(handleDOMScroll)
@ -193,10 +218,22 @@ function ThreadDetail() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// used when there is a sent/added user message and no assistant message (error or manual deletion)
const generateAIResponse = () => {
const latestUserMessage = messages[messages.length - 1]
if (latestUserMessage?.content?.[0]?.text?.value) {
sendMessage(latestUserMessage.content[0].text.value, false)
}
}
const threadModel = useMemo(() => thread?.model, [thread])
if (!messages || !threadModel) return null
const showScrollToBottomBtn = !isAtBottom && hasScrollbar
const showGenerateAIResponseBtn =
messages[messages.length - 1]?.role === 'user' && !streamingContent
return (
<div className="flex flex-col h-full">
<HeaderPage>
@ -243,6 +280,7 @@ function ThreadDetail() {
))
}
index={index}
updateMessage={updateMessage}
/>
</div>
)
@ -266,19 +304,31 @@ function ThreadDetail() {
appMainViewBgColor.a === 1
? 'from-main-view/20 bg-gradient-to-b to-main-view backdrop-blur'
: 'bg-transparent',
!isAtBottom && hasScrollbar && 'visibility-visible opacity-100'
(showScrollToBottomBtn || showGenerateAIResponseBtn) &&
'visibility-visible opacity-100'
)}
>
<div
className="bg-main-view-fg/10 px-4 border border-main-view-fg/5 flex items-center justify-center rounded-xl gap-x-2 cursor-pointer pointer-events-auto"
onClick={() => {
scrollToBottom(true)
setIsUserScrolling(false)
}}
>
<p className="text-xs">{t('scrollToBottom')}</p>
<ArrowDown size={12} />
</div>
{showScrollToBottomBtn && (
<div
className="bg-main-view-fg/10 px-4 border border-main-view-fg/5 flex items-center justify-center rounded-xl gap-x-2 cursor-pointer pointer-events-auto"
onClick={() => {
scrollToBottom(true)
setIsUserScrolling(false)
}}
>
<p className="text-xs">{t('scrollToBottom')}</p>
<ArrowDown size={12} />
</div>
)}
{showGenerateAIResponseBtn && (
<div
className="bg-main-view-fg/10 px-4 border border-main-view-fg/5 flex items-center justify-center rounded-xl gap-x-2 cursor-pointer pointer-events-auto"
onClick={generateAIResponse}
>
<p className="text-xs">{t('Generate AI Response')}</p>
<Play size={12} />
</div>
)}
</div>
<ChatInput model={threadModel} />
</div>

View File

@ -72,5 +72,13 @@ export default defineConfig(({ mode }) => {
ignored: ['**/src-tauri/**'],
},
},
test: {
environment: 'jsdom',
coverage: {
provider: 'v8',
reporter: ['json', 'lcov'],
reportsDirectory: '../coverage/vitest',
},
},
}
})

1366
yarn.lock

File diff suppressed because it is too large Load Diff