mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-09 03:59:45 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2f0964758 | ||
|
|
9522b52499 | ||
|
|
712fec6b42 | ||
|
|
e8f2380f36 | ||
|
|
93e0abb845 | ||
|
|
d5d5b08379 | ||
|
|
b82a368102 | ||
|
|
b7bcbcde99 | ||
|
|
e3bd5cfdef | ||
|
|
61d8833155 | ||
|
|
9f1550c3cf | ||
|
|
46ea3947e6 | ||
|
|
e1da007f82 | ||
|
|
d362bc5f00 | ||
|
|
90ca808a20 | ||
|
|
63de72ce17 | ||
|
|
ab56b84831 | ||
|
|
8920bd493b | ||
|
|
febc0226f5 | ||
|
|
82d3a03458 | ||
|
|
f16e297e5e | ||
|
|
a16f3fe39e | ||
|
|
a1b9cc89ac | ||
|
|
b5f5e38a44 | ||
|
|
6dba262794 | ||
|
|
a65e14cc05 | ||
|
|
201ee46b46 | ||
|
|
8a9feaedde | ||
|
|
a04860fadf | ||
|
|
6ec4d68ab2 | ||
|
|
60d0f66de7 | ||
|
|
1ecb9de791 | ||
|
|
51b75ea25f | ||
|
|
c47e9b9929 | ||
|
|
35b776c34d | ||
|
|
132c5809b8 | ||
|
|
e6569ae4df | ||
|
|
3289d8280d | ||
|
|
9947e7d6f5 | ||
|
|
b12773820f | ||
|
|
458c312c9c | ||
|
|
4890fd9539 | ||
|
|
41e20da530 | ||
|
|
28bd0185f9 | ||
|
|
ee1b382e9c | ||
|
|
88ea8e2a02 | ||
|
|
508e6999ab | ||
|
|
451f3e8a60 | ||
|
|
03882d09db | ||
|
|
b40d65ea4c | ||
|
|
f223047daa | ||
|
|
eddbf228dc | ||
|
|
e66450705e | ||
|
|
1115121839 | ||
|
|
974c0e3467 | ||
|
|
8b16677015 | ||
|
|
19418261e9 | ||
|
|
bc83c088de | ||
|
|
067b4dda5f | ||
|
|
fdb376f398 | ||
|
|
5c2e80ff43 | ||
|
|
0fdfa40b47 | ||
|
|
97a50dc24c | ||
|
|
7f46672a48 | ||
|
|
db137d917a | ||
|
|
0bf2efe331 | ||
|
|
ed1f58d275 | ||
|
|
dcaf4755c1 | ||
|
|
a875d702cf | ||
|
|
7893d4972e | ||
|
|
97a1df1b30 | ||
|
|
c3bdd42dc2 | ||
|
|
496a5382e4 | ||
|
|
906aa63c75 | ||
|
|
021e07be72 | ||
|
|
a78b2d6278 | ||
|
|
a479b17a15 | ||
|
|
943e67ea98 | ||
|
|
ab5a09b6ff | ||
|
|
e172375b25 | ||
|
|
0ff18ef93d | ||
|
|
269f33859b | ||
|
|
2b36e8a027 | ||
|
|
321794636c | ||
|
|
41717f29c1 | ||
|
|
09eb827914 | ||
|
|
8893ba5eec | ||
|
|
b72b451529 | ||
|
|
e1e9d6ff95 | ||
|
|
30fabc8ac9 | ||
|
|
c598662834 | ||
|
|
b9de2cdb8c | ||
|
|
3b64e076c8 | ||
|
|
5306fee502 | ||
|
|
da7b1d022e | ||
|
|
2351f2cccb | ||
|
|
6e4ef31e47 | ||
|
|
78c27655c8 | ||
|
|
b273c8006e | ||
|
|
3d209733d2 | ||
|
|
4dad59ab37 | ||
|
|
fc1c4e4295 | ||
|
|
d10a8cd7e1 | ||
|
|
646826312a | ||
|
|
0f3ae296bf | ||
|
|
cb73b620a2 | ||
|
|
61632296f3 | ||
|
|
1631a9fde8 | ||
|
|
0458430719 | ||
|
|
204a503d05 | ||
|
|
c9e9f133ef | ||
|
|
38ab10e03e | ||
|
|
03dd20f82b | ||
|
|
d3f696fe4e | ||
|
|
3d26434f21 | ||
|
|
ff6cd56cb3 | ||
|
|
242e8f333e | ||
|
|
b3f696ecad | ||
|
|
8c743a697d | ||
|
|
de9c0448b2 | ||
|
|
bcee5f92c4 | ||
|
|
2baea39521 | ||
|
|
f19907ba77 | ||
|
|
e40597a066 | ||
|
|
fd5b3c4559 | ||
|
|
78f5417195 | ||
|
|
48f17ff35e | ||
|
|
e709a5ff44 | ||
|
|
4e50c30a59 | ||
|
|
fad95e2611 | ||
|
|
741310713c | ||
|
|
b4e9ac7c29 | ||
|
|
c5d3f55dfe | ||
|
|
f637d759b9 | ||
|
|
6f379c8e7c | ||
|
|
caebc48411 | ||
|
|
f3e99e58d5 | ||
|
|
99807e8da4 | ||
|
|
ddbfa1d5c8 | ||
|
|
75822f3e9e | ||
|
|
edee9ea256 | ||
|
|
20f7f1ad0b | ||
|
|
499d971666 | ||
|
|
d19ab73020 | ||
|
|
6a71879979 | ||
|
|
3c32937a2b | ||
|
|
d1121b8931 | ||
|
|
88e6634390 | ||
|
|
055274c882 | ||
|
|
2bc8c6692a | ||
|
|
3a8254d046 | ||
|
|
cd14b976af | ||
|
|
c94ef2375a | ||
|
|
5e1ea3ff6e | ||
|
|
4041af8e6c | ||
|
|
af7759b69e | ||
|
|
b0fc432ed9 | ||
|
|
1300170273 | ||
|
|
b8d604d287 | ||
|
|
e8c3acd2d1 | ||
|
|
391f7ce8dc | ||
|
|
e2acc3a950 | ||
|
|
e62f3c1ffd | ||
|
|
64fac3177c | ||
|
|
41b87a4bd2 | ||
|
|
dff2c91145 | ||
|
|
cf0161bfb3 | ||
|
|
5d60cf13f3 | ||
|
|
506f65781e | ||
|
|
7b52ab055c | ||
|
|
0ea3b73145 | ||
|
|
af9f44e522 | ||
|
|
ffb94c578c | ||
|
|
567c46873f | ||
|
|
0a50eda160 | ||
|
|
6858c919ca | ||
|
|
6bb7e7a2a3 | ||
|
|
0029379234 | ||
|
|
456b07cbe7 | ||
|
|
343b8eae6c | ||
|
|
c9abd38a7f | ||
|
|
1015f5515d | ||
|
|
cb012aa5df | ||
|
|
96b45a8415 | ||
|
|
7ce44a1fd3 | ||
|
|
118e20757c | ||
|
|
d8db7ced36 | ||
|
|
a37d0a57ef | ||
|
|
93a1e18bce | ||
|
|
b6769b7f9a | ||
|
|
b6f585164c | ||
|
|
8feca419ff | ||
|
|
e396ce821f | ||
|
|
6ea300dde5 | ||
|
|
daf035a8c1 | ||
|
|
be32c7f3fb | ||
|
|
01f9c62a0a | ||
|
|
affa7d96e7 | ||
|
|
4a5a6ece70 | ||
|
|
c10b746970 | ||
|
|
146df3c468 | ||
|
|
9b02c62ab5 | ||
|
|
9780a8c27d | ||
|
|
311926416a | ||
|
|
0fe4ae5090 | ||
|
|
ea765a5717 | ||
|
|
c49f198c77 | ||
|
|
b1541ab245 | ||
|
|
8a42a9457d | ||
|
|
05506f9532 | ||
|
|
95381770f8 | ||
|
|
d7e555bdbb | ||
|
|
75acfca321 | ||
|
|
63fd759227 | ||
|
|
54828c7efb | ||
|
|
68ab15ddc1 | ||
|
|
7be42963e6 | ||
|
|
d4b72bc901 | ||
|
|
37e91bfdbb | ||
|
|
8221fb8c1c | ||
|
|
5b1728358b | ||
|
|
e17394fe1c | ||
|
|
9bbbb66061 | ||
|
|
a123ba696c | ||
|
|
e7d4844637 | ||
|
|
7084a49b7b | ||
|
|
1e88e88c5f | ||
|
|
c152be538c | ||
|
|
aae7721b6d | ||
|
|
706efc5b0f | ||
|
|
0a0e4463ea | ||
|
|
e77bd43bc9 | ||
|
|
37b5cf0db2 | ||
|
|
c0e7a6f4f8 | ||
|
|
645b9177e3 | ||
|
|
071e4dc51e | ||
|
|
bca7931f3d | ||
|
|
2b9f05093d | ||
|
|
24c6c2f682 | ||
|
|
386f9959f1 | ||
|
|
f281e3fe26 | ||
|
|
f46e88d610 | ||
|
|
d486fbe7d6 | ||
|
|
65909b2d0f | ||
|
|
cae562fa01 | ||
|
|
63f063852f | ||
|
|
cc3f9142e3 | ||
|
|
f138653938 | ||
|
|
ce6bbd7d54 | ||
|
|
aba4ddaca7 | ||
|
|
31928fbe34 | ||
|
|
10831eceb2 | ||
|
|
7604725595 | ||
|
|
2a8217c0ce | ||
|
|
a7cc8faa5d | ||
|
|
20885f7af0 | ||
|
|
abf2953be0 | ||
|
|
78c5a834e3 | ||
|
|
51548e5539 | ||
|
|
7597e383cf | ||
|
|
f2eedfe9f2 | ||
|
|
bd0e878f42 | ||
|
|
4084979c95 | ||
|
|
530a29ea2d | ||
|
|
3e8d3b2940 | ||
|
|
039cecc5a5 | ||
|
|
d735d66a4d | ||
|
|
7bf5ec4ebf | ||
|
|
178d1c74b5 | ||
|
|
eecd7f61d7 | ||
|
|
86c3367a89 | ||
|
|
04b73d061d | ||
|
|
1daec2a5b8 | ||
|
|
b94ab5f4c6 | ||
|
|
445cdf4540 | ||
|
|
8463d15c84 | ||
|
|
1c768f9d12 | ||
|
|
b0b0348c2b | ||
|
|
7e3fae57ca | ||
|
|
32944ce1ee | ||
|
|
f000d4a455 | ||
|
|
6368dc68ab | ||
|
|
55dc17697c | ||
|
|
241b2f203c | ||
|
|
044105d8de | ||
|
|
994029b02f | ||
|
|
89f4fa18ee | ||
|
|
e2d3159996 | ||
|
|
1ed923f59e | ||
|
|
aeaca205eb | ||
|
|
f225844d19 | ||
|
|
40849df0a8 | ||
|
|
a5b820a415 | ||
|
|
b97433df05 | ||
|
|
64cc20a8e2 | ||
|
|
1d342f0836 | ||
|
|
0e1d440713 | ||
|
|
19136808f9 | ||
|
|
2c0ad33f71 | ||
|
|
3cc5714f37 | ||
|
|
687a7d79d7 | ||
|
|
6e31946cbe | ||
|
|
66b346c29f | ||
|
|
830732a33d | ||
|
|
e431a4a8e2 | ||
|
|
4dbaf703fd | ||
|
|
c4fa1d9e1e | ||
|
|
9f7d977d22 | ||
|
|
5d37280db1 | ||
|
|
dc9dfcfd29 |
No files matched your search
@@ -28,3 +28,4 @@ docker/compose/* text eol=lf
|
||||
|
||||
# vendored KaTeX (serve/web/vendor): the files are hashed byte for byte (PROVENANCE.json), never convert their line endings.
|
||||
serve/web/vendor/** -text
|
||||
third_party/strata-ggml/** text eol=lf
|
||||
@@ -1 +1,2 @@
|
||||
github: Niko1221
|
||||
buy_me_a_coffee: strataengine
|
||||
@@ -66,7 +66,7 @@ body:
|
||||
attributes:
|
||||
label: Strata version
|
||||
description: "In the app: Settings > About. In the Terminal: `strata --version`. From source: the About tab of the web page, or `engine` in `GET /v1/status`, or the git commit."
|
||||
placeholder: "Strata 0.2.11 · engine 0.1.57"
|
||||
placeholder: "Strata 0.2.13 · engine 0.1.59"
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
name: Backend compile checks
|
||||
|
||||
# Compile and link every engine backend with a pinned toolchain, without a GPU: nothing is run, so a call into a kernel that only
|
||||
# exists for another backend (or an API a toolkit version dropped) fails here instead of on a user's machine. Hardware runs are separate.
|
||||
# CUDA 12.9 and 13.0 nvidia/cuda devel container targets strata, strata_kq (CMake preset cuda-compile)
|
||||
# HIP (ROCm 7.2) rocm/dev container target strata (CMake preset hip-compile)
|
||||
# SYCL (oneAPI 2026.1) ubuntu + Intel's apt repo target strata of sycl/ (the commands of packaging/build_sycl_linux.sh)
|
||||
# Metal macOS runner targets strata and strata_ds4 (metal/; metal-build.yml keeps its own strata build)
|
||||
# llama.cpp (ggml) comes at the commit pinned in setup.py (LLAMA_CPP_COMMIT), the same one the release builds use.
|
||||
# Architectures are fewer than the release package's (compile time); the flags that decide which sources and defines are built are the release's.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- .github/workflows/backends.yml
|
||||
- CMakeLists.txt
|
||||
- CMakePresets.json
|
||||
- cmake/**
|
||||
- src/**
|
||||
- include/**
|
||||
- third_party/**
|
||||
- sycl/**
|
||||
- metal/**
|
||||
- setup.py
|
||||
- packaging/build_cuda_linux.sh
|
||||
- packaging/build_hip_linux.sh
|
||||
- packaging/build_sycl_linux.sh
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/backends.yml
|
||||
- CMakeLists.txt
|
||||
- CMakePresets.json
|
||||
- cmake/**
|
||||
- src/**
|
||||
- include/**
|
||||
- third_party/**
|
||||
- sycl/**
|
||||
- metal/**
|
||||
- setup.py
|
||||
- packaging/build_cuda_linux.sh
|
||||
- packaging/build_hip_linux.sh
|
||||
- packaging/build_sycl_linux.sh
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: backends-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_MAXSIZE: 2G
|
||||
|
||||
jobs:
|
||||
cuda:
|
||||
name: CUDA ${{ matrix.cuda }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
container: nvidia/cuda:${{ matrix.image }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# the cuda12 package is the one for Pascal and Volta: sm_60 needs STRATA_EXPERIMENTAL_SM60 (packaging/build_cuda_linux.sh)
|
||||
- { cuda: '12.9', image: '12.9.1-devel-ubuntu24.04', archs: '60-real;61-real;70-real;86-real', sm60: 'ON' }
|
||||
- { cuda: '13.0', image: '13.0.2-devel-ubuntu24.04', archs: '75-real;86-real;120-real', sm60: 'OFF' }
|
||||
steps:
|
||||
- name: Tools
|
||||
run: |
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends git ca-certificates cmake ninja-build ccache
|
||||
cmake --version | head -1
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: llama.cpp (ggml) at the pinned commit
|
||||
run: |
|
||||
commit=$(sed -n 's/^LLAMA_CPP_COMMIT = "\(.*\)"/\1/p' setup.py)
|
||||
mkdir llama.cpp && cd llama.cpp && git init -q
|
||||
git remote add origin https://github.com/ggml-org/llama.cpp.git
|
||||
git fetch -q --depth 1 origin "$commit" && git checkout -q FETCH_HEAD
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: .ccache
|
||||
key: ccache-cuda${{ matrix.cuda }}-${{ hashFiles('CMakeLists.txt', 'src/**', 'include/**', 'cmake/**') }}
|
||||
restore-keys: ccache-cuda${{ matrix.cuda }}-
|
||||
- name: Configure (preset cuda-compile)
|
||||
run: |
|
||||
nvcc --version | tail -2
|
||||
cmake --preset cuda-compile -B build -DSTRATA_GGML_DIR="$PWD/llama.cpp" \
|
||||
"-DCMAKE_CUDA_ARCHITECTURES=${{ matrix.archs }}" -DSTRATA_EXPERIMENTAL_SM60=${{ matrix.sm60 }} \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_CUDA_COMPILER_LAUNCHER=ccache
|
||||
- name: Build strata and strata_kq
|
||||
run: cmake --build build --target strata strata_kq -j "$(nproc)"
|
||||
- name: Cache statistics
|
||||
run: ccache -s | head -8
|
||||
|
||||
hip:
|
||||
name: HIP ROCm 7.2
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
container: rocm/dev-ubuntu-24.04:7.2.4-complete
|
||||
steps:
|
||||
- name: Tools
|
||||
run: |
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends git ca-certificates cmake ninja-build ccache
|
||||
cmake --version | head -1
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: llama.cpp (ggml) at the pinned commit
|
||||
run: |
|
||||
commit=$(sed -n 's/^LLAMA_CPP_COMMIT = "\(.*\)"/\1/p' setup.py)
|
||||
mkdir llama.cpp && cd llama.cpp && git init -q
|
||||
git remote add origin https://github.com/ggml-org/llama.cpp.git
|
||||
git fetch -q --depth 1 origin "$commit" && git checkout -q FETCH_HEAD
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: .ccache
|
||||
key: ccache-hip72-${{ hashFiles('CMakeLists.txt', 'src/**', 'include/**', 'cmake/**') }}
|
||||
restore-keys: ccache-hip72-
|
||||
- name: Configure (preset hip-compile)
|
||||
run: |
|
||||
/opt/rocm/llvm/bin/clang++ --version | head -1
|
||||
cmake --preset hip-compile -B build -DSTRATA_GGML_DIR="$PWD/llama.cpp" \
|
||||
-DCMAKE_HIP_COMPILER=/opt/rocm/llvm/bin/clang++ -DCMAKE_HIP_COMPILER_ROCM_ROOT=/opt/rocm -DCMAKE_PREFIX_PATH=/opt/rocm \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache -DCMAKE_HIP_COMPILER_LAUNCHER=ccache
|
||||
- name: Build strata
|
||||
run: cmake --build build --target strata -j "$(nproc)"
|
||||
- name: Cache statistics
|
||||
run: ccache -s | head -8
|
||||
|
||||
sycl:
|
||||
# The engine needs oneAPI 2026.1 or newer (sycl/include/dpct/util.hpp uses the entangle and chunked_partition extensions, which the
|
||||
# 2025.3.2 compiler does not have: it stops at "no member named 'entangle'"; the intel/oneapi-basekit images end at 2025.3), so the
|
||||
# compiler comes from Intel's apt repository at a pinned version.
|
||||
name: SYCL oneAPI 2026.1
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 120
|
||||
container: ubuntu:24.04
|
||||
steps:
|
||||
- name: Tools and oneAPI 2026.1
|
||||
run: |
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends git ca-certificates cmake ninja-build ccache wget gpg
|
||||
wget -qO- https://apt.repos.intel.com/intel-gpg-keys/GPG-PUB-KEY-INTEL-SW-PRODUCTS.PUB | gpg --dearmor -o /usr/share/keyrings/oneapi-archive-keyring.gpg
|
||||
echo "deb [signed-by=/usr/share/keyrings/oneapi-archive-keyring.gpg] https://apt.repos.intel.com/oneapi all main" > /etc/apt/sources.list.d/oneAPI.list
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||
intel-oneapi-compiler-dpcpp-cpp-2026.1=2026.1.1-325 intel-oneapi-mkl-devel-2026.1=2026.1.0-236
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: llama.cpp (ggml) at the pinned commit
|
||||
run: |
|
||||
commit=$(sed -n 's/^LLAMA_CPP_COMMIT = "\(.*\)"/\1/p' setup.py)
|
||||
mkdir llama.cpp && cd llama.cpp && git init -q
|
||||
git remote add origin https://github.com/ggml-org/llama.cpp.git
|
||||
git fetch -q --depth 1 origin "$commit" && git checkout -q FETCH_HEAD
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: .ccache
|
||||
key: ccache-sycl-${{ hashFiles('CMakeLists.txt', 'src/**', 'include/**', 'sycl/**') }}
|
||||
restore-keys: ccache-sycl-
|
||||
- name: Configure and build (the commands of packaging/build_sycl_linux.sh)
|
||||
shell: bash
|
||||
run: |
|
||||
. /opt/intel/oneapi/setvars.sh --force >/dev/null
|
||||
icpx --version | head -1
|
||||
cmake -S sycl -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=icx -DCMAKE_CXX_COMPILER=icpx \
|
||||
-DSTRATA_SYCL_PARITY=OFF -DSTRATA_SYCL_IQ_DECODE_FAST=ON -DSTRATA_GGML_DIR="$PWD/llama.cpp" \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
|
||||
cmake --build build --target strata -j "$(nproc)"
|
||||
- name: Cache statistics
|
||||
run: ccache -s | head -8
|
||||
|
||||
metal:
|
||||
name: Metal strata + strata_ds4
|
||||
# the macOS 26 SDK: metal/src/mtl_rt.mm names MTLGPUFamilyMetal4 behind @available (a run-time check); the names must be in the SDK
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 60
|
||||
env:
|
||||
# metal/setup_mac.py's metal-cpp pin (the same as metal-build.yml)
|
||||
METAL_CPP_URL: https://developer.apple.com/metal/cpp/files/metal-cpp_26.zip
|
||||
METAL_CPP_SHA256: 4df3c078b9aadcb516212e9cb03004cbc5ce9a3e9c068fa3144d021db585a3a4
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Tools
|
||||
run: brew install ninja ccache
|
||||
- name: metal-cpp
|
||||
run: |
|
||||
curl -fsSL "$METAL_CPP_URL" -o metal-cpp.zip
|
||||
echo "$METAL_CPP_SHA256 metal-cpp.zip" | shasum -a 256 -c -
|
||||
unzip -q metal-cpp.zip -d _mcpp
|
||||
- name: llama.cpp (ggml) at the pinned commit
|
||||
run: |
|
||||
commit=$(sed -n 's/^LLAMA_CPP_COMMIT = "\(.*\)"/\1/p' setup.py)
|
||||
mkdir llama.cpp && cd llama.cpp && git init -q
|
||||
git remote add origin https://github.com/ggml-org/llama.cpp.git
|
||||
git fetch -q --depth 1 origin "$commit" && git checkout -q FETCH_HEAD
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: .ccache
|
||||
key: ccache-metal-${{ hashFiles('CMakeLists.txt', 'src/**', 'include/**', 'metal/**') }}
|
||||
restore-keys: ccache-metal-
|
||||
- name: Configure
|
||||
run: |
|
||||
cmake -S metal -B build-metal -G Ninja -DCMAKE_BUILD_TYPE=Release \
|
||||
-DMETAL_CPP_DIR="$PWD/_mcpp/metal-cpp" -DSTRATA_GGML_DIR="$PWD/llama.cpp" \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache
|
||||
- name: Build and link strata and strata_ds4
|
||||
run: cmake --build build-metal --target strata strata_ds4
|
||||
@@ -0,0 +1,185 @@
|
||||
name: Checks
|
||||
|
||||
# The fast checks (tools/check), one job per step so a failure names its step: the CPU CMake preset, the Python suites, the host's
|
||||
# typecheck / test / build. No GPU, no model and no download of weights. The config snapshot has its own workflow (snapshot.yml)
|
||||
# and the backend compile checks theirs (backends.yml). Run the same thing by hand: python tools/check --only <step>
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- .github/workflows/check.yml
|
||||
- tools/check
|
||||
- requirements*.txt
|
||||
- CMakeLists.txt
|
||||
- CMakePresets.json
|
||||
- cmake/**
|
||||
- src/**
|
||||
- include/**
|
||||
- bench/micro/**
|
||||
- third_party/**
|
||||
- tests/**
|
||||
- serve/**
|
||||
- strata.py
|
||||
- setup.py
|
||||
- strata_cli/**
|
||||
- strata_control/**
|
||||
- tools/**
|
||||
- packaging/**
|
||||
- docker/**
|
||||
- apps/host/**
|
||||
- apps/desktop/**
|
||||
- metal/**
|
||||
- sycl/**
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/check.yml
|
||||
- tools/check
|
||||
- requirements*.txt
|
||||
- CMakeLists.txt
|
||||
- CMakePresets.json
|
||||
- cmake/**
|
||||
- src/**
|
||||
- include/**
|
||||
- bench/micro/**
|
||||
- third_party/**
|
||||
- tests/**
|
||||
- serve/**
|
||||
- strata.py
|
||||
- setup.py
|
||||
- strata_cli/**
|
||||
- strata_control/**
|
||||
- tools/**
|
||||
- packaging/**
|
||||
- docker/**
|
||||
- apps/host/**
|
||||
- apps/desktop/**
|
||||
- metal/**
|
||||
- sycl/**
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: checks-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
name: Linux / ${{ matrix.name }}
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 90
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { name: cpu CMake preset, only: cmake-cpu }
|
||||
- { name: Python tests/, only: py-tests }
|
||||
- { name: Python serve/, only: py-serve }
|
||||
- { name: Python tools/, only: py-tools }
|
||||
- { name: host typecheck + test + build, only: "host-typecheck,host-test,host-build" }
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: pip
|
||||
cache-dependency-path: requirements*.txt
|
||||
- uses: actions/setup-node@v4
|
||||
if: ${{ startsWith(matrix.only, 'host') }}
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: npm
|
||||
cache-dependency-path: apps/host/package-lock.json
|
||||
- name: Python packages
|
||||
run: python -m pip install -r requirements.txt -r requirements-dev.txt -r requirements-build.txt
|
||||
- name: tools/check
|
||||
run: python tools/check --only "${{ matrix.only }}" --keep --scratch "$RUNNER_TEMP/check" --json "$RUNNER_TEMP/check/result.json"
|
||||
- name: Logs
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: check-linux-${{ strategy.job-index }}
|
||||
path: |
|
||||
${{ runner.temp }}/check/*.log
|
||||
${{ runner.temp }}/check/result.json
|
||||
retention-days: 7
|
||||
|
||||
windows:
|
||||
name: Windows / ${{ matrix.name }}
|
||||
runs-on: windows-2022
|
||||
timeout-minutes: 120
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { name: cpu CMake preset (MSVC), only: cmake-cpu }
|
||||
- { name: Python tests/, only: py-tests }
|
||||
- { name: Python serve/, only: py-serve }
|
||||
- { name: Python tools/, only: py-tools }
|
||||
- { name: host typecheck + test + build, only: "host-typecheck,host-test,host-build" }
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: pip
|
||||
cache-dependency-path: requirements*.txt
|
||||
- uses: actions/setup-node@v4
|
||||
if: ${{ startsWith(matrix.only, 'host') }}
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: npm
|
||||
cache-dependency-path: apps/host/package-lock.json
|
||||
- name: Python packages
|
||||
run: python -m pip install -r requirements.txt -r requirements-dev.txt -r requirements-build.txt
|
||||
# tools/check finds Visual Studio's compiler itself (vcvars64.bat); bash is Git for Windows' (the tester-README tests need it)
|
||||
- name: tools/check
|
||||
run: python tools/check --only "${{ matrix.only }}" --keep --scratch "$env:RUNNER_TEMP\check" --json "$env:RUNNER_TEMP\check\result.json"
|
||||
- name: Logs
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: check-windows-${{ strategy.job-index }}
|
||||
path: |
|
||||
${{ runner.temp }}\check\*.log
|
||||
${{ runner.temp }}\check\result.json
|
||||
retention-days: 7
|
||||
|
||||
macos:
|
||||
# Cheap and useful (the audit found the host and Python fixtures reading the Mac's own paths), but this job has not run on a
|
||||
# Mac runner yet: it reports and does not block until its first green run.
|
||||
name: macOS / ${{ matrix.name }}
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 60
|
||||
continue-on-error: true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { name: Python tools/, only: py-tools }
|
||||
- { name: host typecheck + test + build, only: "host-typecheck,host-test,host-build" }
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: pip
|
||||
cache-dependency-path: requirements*.txt
|
||||
- uses: actions/setup-node@v4
|
||||
if: ${{ startsWith(matrix.only, 'host') }}
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: npm
|
||||
cache-dependency-path: apps/host/package-lock.json
|
||||
- name: Python packages
|
||||
run: python -m pip install -r requirements.txt -r requirements-dev.txt
|
||||
- name: tools/check
|
||||
run: python tools/check --only "${{ matrix.only }}"
|
||||
@@ -0,0 +1,72 @@
|
||||
name: Docs
|
||||
|
||||
# The documentation checks (python tools/check --only docs): every local link in the current pages resolves, the version lines and
|
||||
# installer names agree with docs/data/release.json, the capability table and its summaries are what docs/data/capabilities.json says, and the
|
||||
# settings page matches the register. Python standard library only; nothing is built, downloaded or run on a GPU.
|
||||
# Archived pages (history/, release notes, bench/results) are not checked; their baseline is history/LINK-BASELINE.md.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- .github/workflows/docs.yml
|
||||
- '**/*.md'
|
||||
- docs/data/release.json
|
||||
- docs/data/capabilities.json
|
||||
- data/settings-register.json
|
||||
- tools/check
|
||||
- tools/check_links.py
|
||||
- tools/doc_pages.py
|
||||
- tools/release_manifest.py
|
||||
- tools/capability_table.py
|
||||
- tools/settings_register.py
|
||||
- strata_cli/__init__.py
|
||||
- CMakeLists.txt
|
||||
- apps/host/package.json
|
||||
- apps/desktop/package.json
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/docs.yml
|
||||
- '**/*.md'
|
||||
- docs/data/release.json
|
||||
- docs/data/capabilities.json
|
||||
- data/settings-register.json
|
||||
- tools/check
|
||||
- tools/check_links.py
|
||||
- tools/doc_pages.py
|
||||
- tools/release_manifest.py
|
||||
- tools/capability_table.py
|
||||
- tools/settings_register.py
|
||||
- strata_cli/__init__.py
|
||||
- CMakeLists.txt
|
||||
- apps/host/package.json
|
||||
- apps/desktop/package.json
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: docs-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
docs:
|
||||
name: Links, release manifest, capability table
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: tools/check docs
|
||||
run: python tools/check --only docs --keep --scratch "$RUNNER_TEMP/check" --json "$RUNNER_TEMP/check/result.json"
|
||||
- name: Log
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: docs-check
|
||||
path: ${{ runner.temp }}/check/*.log
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,83 @@
|
||||
name: Config snapshot
|
||||
|
||||
# tests/snapshot: what `strata launch`, the app's plain Load and the app's Models-screen Load hand the engine (arguments, environment, drafter,
|
||||
# confidence), for every simulated machine class and model set, against the golden files in tests/snapshot/golden. Nothing is loaded,
|
||||
# downloaded or run on a GPU. Windows and Linux must give byte-identical output, so both run. A change that moves what users
|
||||
# get has to update the golden files in the same commit (STRATA_SNAPSHOT_UPDATE=1 python -m pytest tests/snapshot, see tests/snapshot/README.md).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- .github/workflows/snapshot.yml
|
||||
- tests/snapshot/**
|
||||
- serve/**
|
||||
- strata_cli/**
|
||||
- strata_control/**
|
||||
- setup.py
|
||||
- strata.py
|
||||
- requirements*.txt
|
||||
- apps/host/server/**
|
||||
- apps/host/package*.json
|
||||
- packaging/**
|
||||
- tools/check
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/snapshot.yml
|
||||
- tests/snapshot/**
|
||||
- serve/**
|
||||
- strata_cli/**
|
||||
- strata_control/**
|
||||
- setup.py
|
||||
- strata.py
|
||||
- requirements*.txt
|
||||
- apps/host/server/**
|
||||
- apps/host/package*.json
|
||||
- packaging/**
|
||||
- tools/check
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: snapshot-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
snapshot:
|
||||
name: ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-24.04, windows-2022]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
cache: pip
|
||||
cache-dependency-path: requirements*.txt
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24'
|
||||
cache: npm
|
||||
cache-dependency-path: apps/host/package-lock.json
|
||||
- name: Python packages
|
||||
run: python -m pip install -r requirements.txt -r requirements-dev.txt
|
||||
- name: Host packages (the app's Load runs in Node)
|
||||
working-directory: apps/host
|
||||
run: npm ci --no-audit --no-fund
|
||||
- name: tools/check snapshot
|
||||
run: python tools/check --only snapshot --keep --scratch "${{ runner.temp }}/check"
|
||||
- name: Logs
|
||||
if: ${{ failure() }}
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: snapshot-${{ matrix.os }}
|
||||
path: ${{ runner.temp }}/check/*.log
|
||||
retention-days: 7
|
||||
@@ -1,6 +1,6 @@
|
||||
# AGENTS.md
|
||||
|
||||
Version 0.2.11, engine 0.1.57. The public model families are DeepSeekV4.1-Flash-1017,
|
||||
Version 0.2.13, engine 0.1.59. The public model families are DeepSeekV4.1-Flash-1017,
|
||||
Qwen3.8-Flash-Next and Qwen3.6-35B-A3B. DeepSeek V4.1-Flash runs in the app and in the Terminal
|
||||
(`strata launch --family deepseek41 --model ist-das-2.00bpw`; sets in
|
||||
[docs/MODELS.md](docs/MODELS.md#deepseek-v41-flash)). Read **[docs/AI_REFERENCE.md](docs/AI_REFERENCE.md)** for model availability, APIs, hardware limits and
|
||||
@@ -15,7 +15,7 @@ Strata has three parts:
|
||||
|
||||
- **The app** (`apps/desktop`, the Tauri shell; `apps/host`, the Node host and the web UI). It is for people who do
|
||||
not want a terminal. One installer per system ([latest release](https://github.com/Niko1221/Strata/releases/latest)):
|
||||
`Strata-0.2.9-windows-x64-setup.exe`, `Strata-0.2.9-linux-x86_64.AppImage`, `Strata-0.2.9-macos-arm64.dmg`.
|
||||
`Strata-0.2.13-windows-x64-setup.exe`, `Strata-0.2.13-linux-x86_64.AppImage`, `Strata-0.2.13-macos-arm64.dmg`.
|
||||
The Windows and Linux installers carry no engine: the app downloads the engine for the graphics card on first
|
||||
start, or installs a file the user already has. The Mac dmg keeps its small Metal engine inside.
|
||||
- **The Terminal** (`strata.py`, `strata_cli/`, `strata_control/`): the `strata` command. It sets up a model, serves
|
||||
|
||||
+107
-2490
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,78 @@
|
||||
{
|
||||
"version": 5,
|
||||
"cmakeMinimumRequired": { "major": 3, "minor": 24, "patch": 0 },
|
||||
"configurePresets": [
|
||||
{
|
||||
"name": "cpu-check",
|
||||
"displayName": "CPU check build (no GPU, no network)",
|
||||
"description": "The fast check: the CPU-only engine pieces and every test that needs no GPU and no model. STRATA_NATIVE_EXPERTS=OFF, so ggml is not needed. tools/check runs this preset.",
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release",
|
||||
"STRATA_BUILD_TESTS": "ON",
|
||||
"STRATA_NATIVE_EXPERTS": "OFF"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cpu-check-native",
|
||||
"displayName": "CPU check build with the native expert tests",
|
||||
"description": "As cpu-check plus the native (IQ) expert kernels and their tests. ggml comes from the vendored third_party/strata-ggml (no download); -DSTRATA_GGML_DIR=<llama.cpp checkout> overrides it.",
|
||||
"inherits": "cpu-check",
|
||||
"cacheVariables": { "STRATA_NATIVE_EXPERTS": "ON" }
|
||||
},
|
||||
{
|
||||
"name": "cuda-compile",
|
||||
"displayName": "CUDA compile-only (the release flags of packaging/build_cuda_linux.sh, no tests)",
|
||||
"description": "Builds the strata and strata_kq targets for the given CUDA architectures. No GPU is needed to compile; nothing is run. ggml is the vendored third_party/strata-ggml; -DSTRATA_GGML_DIR=<llama.cpp checkout> overrides it (CI still passes one).",
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release",
|
||||
"STRATA_ENABLE_CUDA": "ON",
|
||||
"STRATA_BUILD_TESTS": "OFF",
|
||||
"STRATA_NATIVE_EXPERTS": "ON",
|
||||
"STRATA_PORTABLE": "ON",
|
||||
"STRATA_Q6K_EXPERTS": "ON",
|
||||
"STRATA_MMQ_KQUANTS": "ON",
|
||||
"STRATA_EXPERIMENTAL_SM60": "OFF",
|
||||
"CMAKE_CUDA_ARCHITECTURES": "86-real;89-real;89-virtual"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "hip-compile",
|
||||
"displayName": "HIP compile-only (the release flags of packaging/build_hip_linux.sh, no tests)",
|
||||
"description": "Builds the strata target for gfx1100 and gfx1201 with ROCm's hipcc. ggml is the vendored third_party/strata-ggml (-DSTRATA_GGML_DIR=<llama.cpp checkout> overrides it); if hipcc is not on PATH pass -DCMAKE_HIP_COMPILER=<rocm>/llvm/bin/clang++.",
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||
"cacheVariables": {
|
||||
"CMAKE_BUILD_TYPE": "Release",
|
||||
"STRATA_ENABLE_HIP": "ON",
|
||||
"STRATA_ENABLE_CUDA": "OFF",
|
||||
"STRATA_BUILD_TESTS": "OFF",
|
||||
"STRATA_PREFILL_MMQ": "ON",
|
||||
"STRATA_PORTABLE": "ON",
|
||||
"CMAKE_HIP_ARCHITECTURES": "gfx1100;gfx1201"
|
||||
}
|
||||
}
|
||||
],
|
||||
"buildPresets": [
|
||||
{ "name": "cpu-check", "configurePreset": "cpu-check", "jobs": 0 },
|
||||
{ "name": "cpu-check-native", "configurePreset": "cpu-check-native", "jobs": 0 },
|
||||
{ "name": "cuda-compile", "configurePreset": "cuda-compile", "targets": ["strata", "strata_kq"], "jobs": 0 },
|
||||
{ "name": "hip-compile", "configurePreset": "hip-compile", "targets": ["strata"], "jobs": 0 }
|
||||
],
|
||||
"testPresets": [
|
||||
{
|
||||
"name": "cpu-check",
|
||||
"configurePreset": "cpu-check",
|
||||
"output": { "outputOnFailure": true, "shortProgress": true },
|
||||
"execution": { "timeout": 120, "noTestsAction": "error", "jobs": 4 }
|
||||
},
|
||||
{
|
||||
"name": "cpu-check-native",
|
||||
"configurePreset": "cpu-check-native",
|
||||
"inherits": "cpu-check"
|
||||
}
|
||||
]
|
||||
}
|
||||
+17
-3
@@ -17,6 +17,7 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Strata herunterladen"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT-Lizenz"></a>
|
||||
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Strata über GitHub Sponsors unterstützen"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Strata auf Buy Me a Coffee unterstützen"></a>
|
||||
</p>
|
||||
|
||||
@@ -62,7 +63,7 @@ In der **Desktop-App** und im **Terminal**. Was nicht in den Arbeitsspeicher pas
|
||||
| 2.00 bpw | 257 GB | etwa 265 GB |
|
||||
| 2.25 bpw | 274 GB | etwa 280 GB |
|
||||
|
||||
Läuft auf NVIDIA RTX 30, 40 und 50 (Windows und Linux) und auf AMD Radeon AI PRO R9700 (Linux). Das Setup lädt die Dateien und die DeepSeek-Engine herunter und startet dann das Modell.
|
||||
Läuft auf NVIDIA RTX 30, 40 und 50 (Windows und Linux), AMD Radeon AI PRO R9700 (Linux), AMD Ryzen AI Max — Strix Halo (Windows) und Apple-Silicon-Macs mit 64 GB oder mehr ([Mac](docs/DEEPSEEK_MAC.md)). Das Setup lädt die Dateien und die DeepSeek-Engine herunter und startet dann das Modell.
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
@@ -95,7 +96,7 @@ Unsicher, was passt? [Wähle ein Modell für deinen Computer](docs/MODELS.md#pic
|
||||
|
||||
Installiere die App, wähle ein Modell und leg mit dem Chat los. Es gibt einen Installer pro System. Beim ersten Start lädt die App die Engine für deine Grafikkarte herunter. [Hilfe zum Installer](docs/INSTALL.md#the-app).
|
||||
|
||||
<sub>Dateien: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
|
||||
<sub>Dateien: `Strata-0.2.13-windows-x64-setup.exe` · `Strata-0.2.13-linux-x86_64.AppImage` · `Strata-0.2.13-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Strata-Desktop-App mit Chat und Live-Codevorschau nebeneinander">
|
||||
@@ -175,10 +176,23 @@ Führe **`strata connect`** aus, um die Einrichtung für dein Tool zu sehen. [Ve
|
||||
|
||||
## Strata unterstützen
|
||||
|
||||
**Hilf mit, lokale KI für alle offen zu halten.** Ein Kaffee hilft, Entwicklung und Tests auf mehr Hardware zu finanzieren.
|
||||
**Hilf mit, lokale KI für alle offen zu halten.** Ein GitHub-Sponsoring oder ein Kaffee hilft, Entwicklung und Tests auf mehr Hardware zu finanzieren.
|
||||
|
||||
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: Strata unterstützen" height="48"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: Strata unterstützen" height="48"></a>
|
||||
|
||||
### Treue Unterstützer
|
||||
|
||||
Danke an alle, die Strata unterstützen.
|
||||
|
||||
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
|
||||
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
|
||||
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
|
||||
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
|
||||
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
|
||||
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
|
||||
<!-- supporters:end -->
|
||||
|
||||
[MIT-Lizenz](LICENSE) · [Danksagung und Modelllizenzen](docs/HOW_IT_WORKS.md#credits) · [Datenschutz](docs/SAFETY.md)
|
||||
|
||||
<a id="option-c-let-your-ai-set-it-up"></a>
|
||||
|
||||
+17
-3
@@ -17,6 +17,7 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Descargar Strata"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licencia MIT"></a>
|
||||
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Patrocina Strata en GitHub Sponsors"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Apoya Strata en Buy Me a Coffee"></a>
|
||||
</p>
|
||||
|
||||
@@ -62,7 +63,7 @@ En la **aplicación de escritorio** y en el **Terminal**. Lo que no cabe en la m
|
||||
| 2.00 bpw | 257 GB | unos 265 GB |
|
||||
| 2.25 bpw | 274 GB | unos 280 GB |
|
||||
|
||||
Funciona en NVIDIA RTX 30, 40 y 50 (Windows y Linux) y en AMD Radeon AI PRO R9700 (Linux). La instalación descarga los archivos y el motor de DeepSeek, y después inicia el modelo.
|
||||
Funciona en NVIDIA RTX 30, 40 y 50 (Windows y Linux), AMD Radeon AI PRO R9700 (Linux), AMD Ryzen AI Max — Strix Halo (Windows) y Macs con Apple Silicon de 64 GB o más ([Mac](docs/DEEPSEEK_MAC.md)). La instalación descarga los archivos y el motor de DeepSeek, y después inicia el modelo.
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
@@ -95,7 +96,7 @@ Medido con el conjunto de 2.00 bpw en Strata 0.2.5. «Salida» es la velocidad d
|
||||
|
||||
Instala la aplicación, elige un modelo y empieza a chatear. Hay un instalador por sistema. En el primer arranque, la aplicación descarga el motor para tu tarjeta gráfica. [Ayuda con el instalador](docs/INSTALL.md#the-app).
|
||||
|
||||
<sub>Archivos: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
|
||||
<sub>Archivos: `Strata-0.2.13-windows-x64-setup.exe` · `Strata-0.2.13-linux-x86_64.AppImage` · `Strata-0.2.13-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Aplicación de escritorio de Strata con un chat y una vista previa del código en vivo, una junto a otra">
|
||||
@@ -175,10 +176,23 @@ Ejecuta **`strata connect`** para ver las instrucciones de tu herramienta. [Guí
|
||||
|
||||
## Apoya Strata
|
||||
|
||||
**Ayuda a mantener la IA local abierta a todos.** Un café ayuda a financiar el desarrollo y las pruebas en más hardware.
|
||||
**Ayuda a mantener la IA local abierta a todos.** Un patrocinio en GitHub o un café ayuda a financiar el desarrollo y las pruebas en más hardware.
|
||||
|
||||
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: patrocina Strata" height="48"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: apoya Strata" height="48"></a>
|
||||
|
||||
### Apoyos fieles
|
||||
|
||||
Gracias a todas las personas que apoyan Strata.
|
||||
|
||||
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
|
||||
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
|
||||
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
|
||||
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
|
||||
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
|
||||
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
|
||||
<!-- supporters:end -->
|
||||
|
||||
[Licencia MIT](LICENSE) · [Créditos y licencias de los modelos](docs/HOW_IT_WORKS.md#credits) · [Privacidad](docs/SAFETY.md)
|
||||
|
||||
<a id="option-c-let-your-ai-set-it-up"></a>
|
||||
|
||||
+17
-3
@@ -17,6 +17,7 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Télécharger Strata"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licence MIT"></a>
|
||||
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Soutenir Strata sur GitHub Sponsors"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Soutenir Strata sur Buy Me a Coffee"></a>
|
||||
</p>
|
||||
|
||||
@@ -62,7 +63,7 @@ Dans l'**application de bureau** et dans le **Terminal**. Ce qui ne tient pas en
|
||||
| 2.00 bpw | 257 GB | environ 265 GB |
|
||||
| 2.25 bpw | 274 GB | environ 280 GB |
|
||||
|
||||
Fonctionne sur les NVIDIA RTX 30, 40 et 50 (Windows et Linux) et sur l'AMD Radeon AI PRO R9700 (Linux). L'installation télécharge les fichiers et le moteur DeepSeek, puis démarre le modèle.
|
||||
Fonctionne sur les NVIDIA RTX 30, 40 et 50 (Windows et Linux), l'AMD Radeon AI PRO R9700 (Linux), l'AMD Ryzen AI Max — Strix Halo (Windows) et les Mac Apple Silicon avec 64 Go ou plus ([Mac](docs/DEEPSEEK_MAC.md)). L'installation télécharge les fichiers et le moteur DeepSeek, puis démarre le modèle.
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
@@ -95,7 +96,7 @@ Vous ne savez pas ce qui tient ? [Choisir un modèle pour votre ordinateur](docs
|
||||
|
||||
Installez l'application, choisissez un modèle et commencez à discuter. Il y a un seul installateur par système. Au premier lancement, l'application télécharge le moteur de votre carte graphique. [Aide pour l'installateur](docs/INSTALL.md#the-app).
|
||||
|
||||
<sub>Fichiers: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
|
||||
<sub>Fichiers: `Strata-0.2.13-windows-x64-setup.exe` · `Strata-0.2.13-linux-x86_64.AppImage` · `Strata-0.2.13-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Application de bureau Strata avec une discussion et un aperçu du code en direct côte à côte">
|
||||
@@ -175,10 +176,23 @@ Lancez **`strata connect`** pour voir la configuration de votre outil. [Guide de
|
||||
|
||||
## Soutenir Strata
|
||||
|
||||
**Aidez à garder l'IA locale ouverte à tous.** Un café aide à financer le développement et les tests sur davantage de matériel.
|
||||
**Aidez à garder l'IA locale ouverte à tous.** Un parrainage sur GitHub ou un café aide à financer le développement et les tests sur davantage de matériel.
|
||||
|
||||
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors : soutenir Strata" height="48"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee : soutenir Strata" height="48"></a>
|
||||
|
||||
### Fidèles soutiens
|
||||
|
||||
Merci à toutes les personnes qui soutiennent Strata.
|
||||
|
||||
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
|
||||
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
|
||||
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
|
||||
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
|
||||
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
|
||||
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
|
||||
<!-- supporters:end -->
|
||||
|
||||
[Licence MIT](LICENSE) · [Crédits et licences des modèles](docs/HOW_IT_WORKS.md#credits) · [Confidentialité](docs/SAFETY.md)
|
||||
|
||||
<a id="option-c-let-your-ai-set-it-up"></a>
|
||||
|
||||
+17
-3
@@ -17,6 +17,7 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Strata をダウンロード"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT ライセンス"></a>
|
||||
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="GitHub Sponsors で Strata を支援する"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Buy Me a Coffee で Strata を支援する"></a>
|
||||
</p>
|
||||
|
||||
@@ -62,7 +63,7 @@
|
||||
| 2.00 bpw | 257 GB | 約265 GB |
|
||||
| 2.25 bpw | 274 GB | 約280 GB |
|
||||
|
||||
NVIDIA RTX 30・40・50 シリーズ(Windows と Linux)と、AMD Radeon AI PRO R9700(Linux)で動きます。セットアップがファイルと DeepSeek エンジンをダウンロードし、モデルを起動します。
|
||||
NVIDIA RTX 30・40・50 シリーズ(Windows と Linux)、AMD Radeon AI PRO R9700(Linux)、AMD Ryzen AI Max — Strix Halo(Windows)、メモリ 64 GB 以上の Apple シリコン搭載 Mac([Mac](docs/DEEPSEEK_MAC.md))で動きます。セットアップがファイルと DeepSeek エンジンをダウンロードし、モデルを起動します。
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
@@ -95,7 +96,7 @@ Strata 0.2.5 で 2.00 bpw のセットを使って測定しました。「出力
|
||||
|
||||
アプリをインストールし、モデルを選んで、チャットを始めましょう。インストーラーはシステムごとに1つです。初回起動時に、アプリがお使いのグラフィックスカード用のエンジンをダウンロードします。[インストーラーのヘルプ](docs/INSTALL.md#the-app)。
|
||||
|
||||
<sub>ファイル: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
|
||||
<sub>ファイル: `Strata-0.2.13-windows-x64-setup.exe` · `Strata-0.2.13-linux-x86_64.AppImage` · `Strata-0.2.13-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="チャットとライブのコードプレビューを並べて表示した Strata デスクトップアプリ">
|
||||
@@ -175,10 +176,23 @@ strata launch
|
||||
|
||||
## Strata を支援する
|
||||
|
||||
**ローカル AI をみんなに開かれたものに。** コーヒー1杯が、開発と、より多くのハードウェアでのテストの支えになります。
|
||||
**ローカル AI をみんなに開かれたものに。** GitHub でのスポンサーやコーヒー1杯が、開発と、より多くのハードウェアでのテストの支えになります。
|
||||
|
||||
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: Strata を支援する" height="48"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: Strata を支援する" height="48"></a>
|
||||
|
||||
### いつも支えてくださる方々
|
||||
|
||||
Strata を支えてくださるすべての方に感謝します。
|
||||
|
||||
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
|
||||
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
|
||||
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
|
||||
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
|
||||
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
|
||||
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
|
||||
<!-- supporters:end -->
|
||||
|
||||
[MIT ライセンス](LICENSE) · [クレジットとモデルのライセンス](docs/HOW_IT_WORKS.md#credits) · [プライバシー](docs/SAFETY.md)
|
||||
|
||||
<a id="option-c-let-your-ai-set-it-up"></a>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Download Strata"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT license"></a>
|
||||
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Sponsor Strata on GitHub Sponsors"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Support Strata on Buy Me a Coffee"></a>
|
||||
</p>
|
||||
|
||||
@@ -65,7 +66,7 @@ disk space.
|
||||
| ISTA-DAS Coder (half of the experts removed) | 191 GB | about 197 GB |
|
||||
|
||||
Runs on NVIDIA RTX 30, 40 and 50 series (Windows and Linux), AMD Radeon AI PRO R9700 (Linux), AMD Ryzen AI Max — Strix Halo
|
||||
(Windows), and Apple Silicon Macs with 64 GB or more, text only ([Mac](docs/DEEPSEEK_MAC.md)). Setup downloads the files and the DeepSeek engine, then starts the model.
|
||||
(Windows), and Apple Silicon Macs with 64 GB or more ([Mac](docs/DEEPSEEK_MAC.md)). Setup downloads the files and the DeepSeek engine, then starts the model.
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
@@ -100,7 +101,7 @@ Not sure what fits? [Choose a model for your computer](docs/MODELS.md#pick-by-me
|
||||
Install the app, choose a model, and start chatting. There is one installer for each system. On first start the app
|
||||
downloads the engine for your graphics card. [Help with the installer](docs/INSTALL.md#the-app).
|
||||
|
||||
<sub>Files: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
|
||||
<sub>Files: `Strata-0.2.13-windows-x64-setup.exe` · `Strata-0.2.13-linux-x86_64.AppImage` · `Strata-0.2.13-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Strata desktop app with a chat and live code preview side by side">
|
||||
@@ -181,10 +182,23 @@ Run **`strata connect`** to see setup instructions for your tool. [Connection gu
|
||||
|
||||
## Support Strata
|
||||
|
||||
**Help keep local AI open to everyone.** A coffee helps fund development and testing on more hardware.
|
||||
**Help keep local AI open to everyone.** A GitHub sponsorship or a coffee helps fund development and testing on more hardware.
|
||||
|
||||
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: sponsor Strata" height="48"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: support Strata" height="48"></a>
|
||||
|
||||
### Loyal supporters
|
||||
|
||||
Thank you to everyone who supports Strata.
|
||||
|
||||
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
|
||||
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
|
||||
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
|
||||
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
|
||||
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
|
||||
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
|
||||
<!-- supporters:end -->
|
||||
|
||||
[MIT license](LICENSE) · [Credits and model licenses](docs/HOW_IT_WORKS.md#credits) · [Privacy](docs/SAFETY.md)
|
||||
|
||||
<a id="option-c-let-your-ai-set-it-up"></a>
|
||||
|
||||
+17
-3
@@ -17,6 +17,7 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Pobierz Stratę"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licencja MIT"></a>
|
||||
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Wesprzyj Stratę na GitHub Sponsors"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Wesprzyj Stratę na Buy Me a Coffee"></a>
|
||||
</p>
|
||||
|
||||
@@ -64,7 +65,7 @@ dużo wolnego miejsca na dysku.
|
||||
| 2.25 bpw | 274 GB | około 280 GB |
|
||||
|
||||
Działa na kartach NVIDIA RTX serii 30, 40 i 50 (Windows i Linux), AMD Radeon AI PRO R9700 (Linux) oraz AMD Ryzen AI Max — Strix Halo
|
||||
(Windows). Instalacja pobiera pliki i silnik DeepSeek, a potem uruchamia model.
|
||||
(Windows) oraz na komputerach Mac z Apple Silicon i co najmniej 64 GB pamięci ([Mac](docs/DEEPSEEK_MAC.md)). Instalacja pobiera pliki i silnik DeepSeek, a potem uruchamia model.
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
@@ -99,7 +100,7 @@ Nie wiesz, co się zmieści? [Wybierz model do swojego komputera](docs/MODELS.md
|
||||
Zainstaluj aplikację, wybierz model i zacznij rozmawiać. Dla każdego systemu jest jeden instalator. Przy pierwszym
|
||||
uruchomieniu aplikacja pobiera silnik dla twojej karty graficznej. [Pomoc z instalatorem](docs/INSTALL.md#the-app).
|
||||
|
||||
<sub>Pliki: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
|
||||
<sub>Pliki: `Strata-0.2.13-windows-x64-setup.exe` · `Strata-0.2.13-linux-x86_64.AppImage` · `Strata-0.2.13-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Aplikacja Strata na komputer: czat i podgląd kodu na żywo obok siebie">
|
||||
@@ -179,10 +180,23 @@ Uruchom **`strata connect`**, aby zobaczyć instrukcję konfiguracji dla swojego
|
||||
|
||||
## Wesprzyj Stratę
|
||||
|
||||
**Pomóż zachować lokalną sztuczną inteligencję otwartą dla wszystkich.** Kawa pomaga finansować rozwój i testy na większej liczbie sprzętu.
|
||||
**Pomóż zachować lokalną sztuczną inteligencję otwartą dla wszystkich.** Sponsoring na GitHubie lub kawa pomaga finansować rozwój i testy na większej liczbie sprzętu.
|
||||
|
||||
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: wesprzyj Stratę" height="48"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: wesprzyj Stratę" height="48"></a>
|
||||
|
||||
### Wierni wspierający
|
||||
|
||||
Dziękujemy wszystkim, którzy wspierają Stratę.
|
||||
|
||||
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
|
||||
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
|
||||
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
|
||||
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
|
||||
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
|
||||
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
|
||||
<!-- supporters:end -->
|
||||
|
||||
[Licencja MIT](LICENSE) · [Podziękowania i licencje modeli](docs/HOW_IT_WORKS.md#credits) · [Prywatność](docs/SAFETY.md)
|
||||
|
||||
<a id="option-c-let-your-ai-set-it-up"></a>
|
||||
|
||||
+17
-3
@@ -17,6 +17,7 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="Baixar o Strata"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="Licença MIT"></a>
|
||||
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="Apoie o Strata no GitHub Sponsors"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="Apoie o Strata no Buy Me a Coffee"></a>
|
||||
</p>
|
||||
|
||||
@@ -62,7 +63,7 @@ No **aplicativo de desktop** e no **Terminal**. O que não cabe na memória é l
|
||||
| 2.00 bpw | 257 GB | cerca de 265 GB |
|
||||
| 2.25 bpw | 274 GB | cerca de 280 GB |
|
||||
|
||||
Roda em NVIDIA RTX 30, 40 e 50 (Windows e Linux) e em AMD Radeon AI PRO R9700 (Linux). A instalação baixa os arquivos e o motor do DeepSeek e depois inicia o modelo.
|
||||
Roda em NVIDIA RTX 30, 40 e 50 (Windows e Linux), AMD Radeon AI PRO R9700 (Linux), AMD Ryzen AI Max — Strix Halo (Windows) e Macs com Apple Silicon e 64 GB ou mais ([Mac](docs/DEEPSEEK_MAC.md)). A instalação baixa os arquivos e o motor do DeepSeek e depois inicia o modelo.
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
@@ -95,7 +96,7 @@ Não sabe o que cabe? [Escolha um modelo para o seu computador](docs/MODELS.md#p
|
||||
|
||||
Instale o aplicativo, escolha um modelo e comece a conversar. Há um instalador por sistema. Na primeira vez que abre, o aplicativo baixa o motor da sua placa de vídeo. [Ajuda com o instalador](docs/INSTALL.md#the-app).
|
||||
|
||||
<sub>Arquivos: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
|
||||
<sub>Arquivos: `Strata-0.2.13-windows-x64-setup.exe` · `Strata-0.2.13-linux-x86_64.AppImage` · `Strata-0.2.13-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Aplicativo de desktop do Strata com um chat e uma prévia do código ao vivo lado a lado">
|
||||
@@ -175,10 +176,23 @@ Execute **`strata connect`** para ver as instruções da sua ferramenta. [Guia d
|
||||
|
||||
## Apoie o Strata
|
||||
|
||||
**Ajude a manter a IA local aberta para todos.** Um café ajuda a financiar o desenvolvimento e os testes em mais hardware.
|
||||
**Ajude a manter a IA local aberta para todos.** Um patrocínio no GitHub ou um café ajuda a financiar o desenvolvimento e os testes em mais hardware.
|
||||
|
||||
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors: apoie o Strata" height="48"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee: apoie o Strata" height="48"></a>
|
||||
|
||||
### Apoiadores fiéis
|
||||
|
||||
Obrigado a todas as pessoas que apoiam o Strata.
|
||||
|
||||
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
|
||||
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
|
||||
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
|
||||
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
|
||||
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
|
||||
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
|
||||
<!-- supporters:end -->
|
||||
|
||||
[Licença MIT](LICENSE) · [Créditos e licenças dos modelos](docs/HOW_IT_WORKS.md#credits) · [Privacidade](docs/SAFETY.md)
|
||||
|
||||
<a id="option-c-let-your-ai-set-it-up"></a>
|
||||
|
||||
+17
-3
@@ -17,6 +17,7 @@
|
||||
<p align="center">
|
||||
<a href="https://github.com/Niko1221/Strata/releases/latest"><img src="https://img.shields.io/github/v/release/Niko1221/Strata?label=download&color=0ab887" alt="下载 Strata"></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/github/license/Niko1221/Strata" alt="MIT 许可证"></a>
|
||||
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor-EA4AAA?logo=githubsponsors&logoColor=fff" alt="在 GitHub Sponsors 上支持 Strata"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://img.shields.io/badge/Buy_me_a_coffee-FFDD00?logo=buymeacoffee&logoColor=000" alt="在 Buy Me a Coffee 上支持 Strata"></a>
|
||||
</p>
|
||||
|
||||
@@ -62,7 +63,7 @@
|
||||
| 2.00 bpw | 257 GB | 约265 GB |
|
||||
| 2.25 bpw | 274 GB | 约280 GB |
|
||||
|
||||
支持 NVIDIA RTX 30、40 和 50 系列(Windows 和 Linux),以及 AMD Radeon AI PRO R9700(Linux)。安装程序会下载模型文件和 DeepSeek 引擎,然后启动模型。
|
||||
支持 NVIDIA RTX 30、40 和 50 系列(Windows 和 Linux)、AMD Radeon AI PRO R9700(Linux)、AMD Ryzen AI Max — Strix Halo(Windows),以及内存 64 GB 或以上的 Apple 芯片 Mac([Mac](docs/DEEPSEEK_MAC.md))。安装程序会下载模型文件和 DeepSeek 引擎,然后启动模型。
|
||||
|
||||
```sh
|
||||
strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
@@ -95,7 +96,7 @@ strata launch --family deepseek41 --model ist-das-2.00bpw --plan
|
||||
|
||||
安装应用,选择模型,就可以开始聊天。每个系统只有一个安装程序。首次启动时,应用会下载适合你显卡的引擎。[安装程序帮助](docs/INSTALL.md#the-app)。
|
||||
|
||||
<sub>文件: `Strata-0.2.9-windows-x64-setup.exe` · `Strata-0.2.9-linux-x86_64.AppImage` · `Strata-0.2.9-macos-arm64.dmg`</sub>
|
||||
<sub>文件: `Strata-0.2.13-windows-x64-setup.exe` · `Strata-0.2.13-linux-x86_64.AppImage` · `Strata-0.2.13-macos-arm64.dmg`</sub>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/media/app-chat.png" width="900" alt="Strata 桌面应用,聊天和实时代码预览并排显示">
|
||||
@@ -175,10 +176,23 @@ strata launch
|
||||
|
||||
## 支持 Strata
|
||||
|
||||
**帮助本地 AI 保持开放。** 一杯咖啡能帮助我们在更多硬件上开发和测试。
|
||||
**帮助本地 AI 保持开放。** GitHub 赞助或一杯咖啡,都能帮助我们在更多硬件上开发和测试。
|
||||
|
||||
<a href="https://github.com/sponsors/Niko1221"><img src="https://img.shields.io/badge/Sponsor_on_GitHub-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="GitHub Sponsors:支持 Strata" height="48"></a>
|
||||
<a href="https://buymeacoffee.com/strataengine"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me a Coffee:支持 Strata" height="48"></a>
|
||||
|
||||
### 忠实支持者
|
||||
|
||||
感谢每一位支持 Strata 的人。
|
||||
|
||||
<!-- supporters:start (one line per public supporter, newest last; copy a line and change the GitHub name in all four places) -->
|
||||
<a href="https://github.com/ryan4yin"><img src="https://github.com/ryan4yin.png?size=96" width="48" height="48" alt="ryan4yin" title="ryan4yin"></a>
|
||||
<a href="https://github.com/themactep"><img src="https://github.com/themactep.png?size=96" width="48" height="48" alt="themactep" title="themactep"></a>
|
||||
<a href="https://github.com/macintog"><img src="https://github.com/macintog.png?size=96" width="48" height="48" alt="macintog" title="macintog"></a>
|
||||
<a href="https://github.com/bisonbet"><img src="https://github.com/bisonbet.png?size=96" width="48" height="48" alt="bisonbet" title="bisonbet"></a>
|
||||
<a href="https://github.com/vizv"><img src="https://github.com/vizv.png?size=96" width="48" height="48" alt="vizv" title="vizv"></a>
|
||||
<!-- supporters:end -->
|
||||
|
||||
[MIT 许可证](LICENSE) · [致谢和模型许可证](docs/HOW_IT_WORKS.md#credits) · [隐私](docs/SAFETY.md)
|
||||
|
||||
<a id="option-c-let-your-ai-set-it-up"></a>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "strata-desktop",
|
||||
"productName": "Strata",
|
||||
"version": "0.2.11",
|
||||
"version": "0.2.13",
|
||||
"private": true,
|
||||
"description": "Strata Desktop - local AI, tuned to one model.",
|
||||
"scripts": {
|
||||
|
||||
Generated
+1
-1
@@ -3625,7 +3625,7 @@ checksum = "ff7589a1859b09232522bb9edf076ca7fcf14ded96685d3fa629db73023ffbb7"
|
||||
|
||||
[[package]]
|
||||
name = "strata-desktop"
|
||||
version = "0.2.11"
|
||||
version = "0.2.13"
|
||||
dependencies = [
|
||||
"gtk",
|
||||
"serde",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "strata-desktop"
|
||||
version = "0.2.11"
|
||||
version = "0.2.13"
|
||||
description = "Strata Desktop — local AI, tuned to one model."
|
||||
edition = "2021"
|
||||
rust-version = "1.77"
|
||||
|
||||
+44
-5
@@ -1,15 +1,54 @@
|
||||
# Strata app source
|
||||
# Strata app: developer guide
|
||||
|
||||
Start with [the complete developer handoff](../handoff.md) and [the runbook](../docs/RUNBOOK.md).
|
||||
This folder is the app's Node host (`server/`) and its web interface (`src/`, React and Vite). The desktop shell that wraps
|
||||
them is [`../desktop`](../desktop) (Tauri). The engine and Strata Control are Python and C++ and live elsewhere in the
|
||||
repository; see [docs/SOURCE_INSTALL.md](../../docs/SOURCE_INSTALL.md) and [packaging/README.md](../../packaging/README.md).
|
||||
|
||||
## Start from a fresh clone
|
||||
|
||||
You need **Node 24.12 or newer** (`engines` in `package.json`) and npm. Nothing else: no model, no engine, no GPU, no
|
||||
sibling design folder.
|
||||
|
||||
```sh
|
||||
cd apps/host
|
||||
npm ci
|
||||
npm run preview:ui
|
||||
npm run build # makes dist/, which the preview and `npm run serve` need (dist/ is not checked in)
|
||||
npm run preview:ui # simulated hardware and models; open http://localhost:5272/#/models
|
||||
```
|
||||
|
||||
The built UI is included. To rebuild after source edits, run `npm run build`. `preview:ui` uses isolated simulated data; `npm run dev` connects the normal host.
|
||||
`preview:ui` starts an isolated host and a fixture server on temporary data and prints "UI PREVIEW ONLY". Nothing it shows
|
||||
is real: no engine is started, no model is loaded, no files outside its temporary folder are touched. Ctrl+C stops it.
|
||||
|
||||
The previous project README is retained in [docs/README-historical.md](docs/README-historical.md). It contains older implementation status and is historical reference.
|
||||
| Command | What it does |
|
||||
| --- | --- |
|
||||
| `npm run build` | Builds the interface into `dist/`. |
|
||||
| `npm run typecheck` | `tsc --noEmit`. |
|
||||
| `npm test` | The host and interface tests (`tests/*.test.mjs`, `node --test`). They write to scratch copies of the per-user folders, never to your own profile. A test that hangs fails after 10 minutes (`STRATA_TEST_TIMEOUT_MS`). |
|
||||
| `npm run dev` | The source host (port 8791) and the Vite dev server (port 5199) together; Vite proxies `/api` to the host. |
|
||||
| `npm run serve` | The host with the built `dist/` (run `npm run build` first). |
|
||||
| `npm run check` | Typecheck plus the interface audits (`audit`, `contrast`, `measure`, `luminance`). |
|
||||
|
||||
## Preview, source host and packaged Control
|
||||
|
||||
- **Simulated preview** (`npm run preview:ui`): fixtures only. Use it to look at screens.
|
||||
- **Source host** (`npm run dev`, `npm run serve`): the real host code from this checkout. Without `STRATA_REAL_ENGINE=1` it
|
||||
runs a clearly labelled example engine; with it, the host looks for a Strata installation as a source checkout does.
|
||||
Loading a real model needs an engine and model files; none of the commands above do that.
|
||||
- **Packaged app**: the installer ships the host next to Strata Control (Python plus `strata_control`), which does installs,
|
||||
updates and engine starts. A source checkout has no Control, so those paths keep their older behaviour; how Control is
|
||||
packaged is in [packaging/README.md](../../packaging/README.md).
|
||||
|
||||
The version numbers in `package.json` are the app's. They are checked against `docs/data/release.json` by `python tools/release_manifest.py --check`.
|
||||
|
||||
## Styles, icons and fonts
|
||||
|
||||
The checked-in files are the source. There is no generator and no sibling design folder.
|
||||
|
||||
- Kit styles: `src/styles/kit/tokens.css` and `components.css`; app styles sit on top in `src/styles/*.css`.
|
||||
- Icons: `public/icons/sprite.svg`. Add an icon as a `<symbol id="i-NAME">`; `npm run audit` fails on an icon that is used but missing.
|
||||
- Fonts: `public/fonts`.
|
||||
- The standalone server page (`serve/web/`) serves its own copy of `components.css`. The two copies are byte-identical and
|
||||
`tests/kit-copy.test.mjs` fails when they differ: change both together.
|
||||
|
||||
## Languages
|
||||
|
||||
|
||||
@@ -1,71 +1,5 @@
|
||||
# Strata UI fixes — handoff for Claude Code
|
||||
# Moved
|
||||
|
||||
This package comes from the Strata UI audit of 7 October 2026: 177 findings across 210 screen states. Here they are split into 19 tasks. Each task file lists the findings it covers, the files to change, and how to check the result.
|
||||
|
||||
## What's inside
|
||||
- `tasks/` has one file per task, ready to give to a subagent.
|
||||
- `findings.json` has all 177 findings with severity, state, issue, suggested fix and assigned task.
|
||||
- `designs/` holds the reference designs for T09 (Model) and T10 (Engine v3). They are design-component source files: read them for structure, copy, colours and behaviour. They won't run on their own.
|
||||
- `.claude/agents/strata-fixer.md` is a subagent definition (Sonnet, focused on one task).
|
||||
- `PROMPTS.md` has the prompts to paste into Claude Code.
|
||||
- `REVIEW.md` is the checklist for the review after each phase.
|
||||
- `PROGRESS.md` is the log that each task appends to.
|
||||
|
||||
## Setup
|
||||
1. Copy this folder into the repo root as `audit-fixes/` (so paths read `audit-fixes/tasks/T01-…`).
|
||||
2. Copy `audit-fixes/.claude/agents/strata-fixer.md` to `.claude/agents/strata-fixer.md` in the repo.
|
||||
3. Make sure `docs/evidence/audit-2026-10-07/` (the screenshots) is in the repo, because the task files point to it.
|
||||
4. Run `npm install`, then `npm run check` and `node --test tests/` once. Note anything that already fails, so it isn't blamed on a task.
|
||||
|
||||
## Order
|
||||
Phase 1 fixes shared components, so later tasks get smaller. Tasks in the same phase with no dependency between them can run in parallel on separate branches.
|
||||
|
||||
### Phase 1 — root causes (shared components)
|
||||
| Task | Title | Findings | High / Med / Low | Depends on |
|
||||
|---|---|---|---|---|
|
||||
| T01 | One model, one name | 11 | 6 / 2 / 3 | — |
|
||||
| T02 | Overlay layering and dialogs | 5 | 2 / 3 / 0 | — |
|
||||
| T03 | Primary, disabled and on states | 11 | 1 / 7 / 3 | — |
|
||||
| T04 | One error component, no fixture text | 10 | 0 / 9 / 1 | — |
|
||||
| T05 | Show numbers when they matter | 6 | 2 / 2 / 2 | T01 |
|
||||
| T06 | Fix the response hover layout | 1 | 1 / 0 / 0 | — |
|
||||
| T07 | Hide what is not built | 2 | 0 / 1 / 1 | — |
|
||||
| T08 | Engine and Model in the main nav | 1 | 1 / 0 / 0 | T01 |
|
||||
|
||||
### Phase 2 — rebuild Model and Engine
|
||||
| Task | Title | Findings | High / Med / Low | Depends on |
|
||||
|---|---|---|---|---|
|
||||
| T09 | Build the single-model Model page | 17 | 0 / 9 / 8 | T01, T03, T04 |
|
||||
| T10 | Build Engine v3 | 17 | 2 / 9 / 6 | T01, T02, T03 |
|
||||
|
||||
### Phase 3 — remaining findings per screen
|
||||
| Task | Title | Findings | High / Med / Low | Depends on |
|
||||
|---|---|---|---|---|
|
||||
| T11 | Chat: remaining findings | 38 | 0 / 20 / 18 | T01, T05, T06, T07 |
|
||||
| T12 | Time-skip: remaining findings | 6 | 0 / 3 / 3 | — |
|
||||
| T13 | Serving: remaining findings | 19 | 0 / 6 / 13 | T04 |
|
||||
| T14 | Monitor: remaining findings | 2 | 0 / 1 / 1 | T05 |
|
||||
| T15 | Skills and Tasks: remaining findings | 9 | 0 / 2 / 7 | T02, T03, T04 |
|
||||
| T16 | Settings and Setup: remaining findings | 7 | 0 / 2 / 5 | T02, T09 |
|
||||
| T17 | Navigation and overlays: remaining findings | 4 | 0 / 2 / 2 | T02, T08 |
|
||||
| T18 | Preview panel: remaining findings | 7 | 0 / 3 / 4 | — |
|
||||
| T19 | Dark mode: remaining findings | 4 | 0 / 1 / 3 | T02, T03 |
|
||||
|
||||
## Running it cheaply
|
||||
- Do high and medium first. Every task file puts low findings under "Optional polish". Skip them in the first pass. That cuts about a third of the work.
|
||||
- Use a fresh session per task. Each subagent starts clean with only its task file, so context stays small.
|
||||
- Check with tests first, and use screenshots only for the states listed in the task.
|
||||
- After each phase, run one review (see REVIEW.md) instead of reviewing after every task.
|
||||
|
||||
Rough cost at API list prices (Sonnet 5.5, medium effort, for the subagents; one review per phase):
|
||||
- Phase 1 (8 tasks): $10–20
|
||||
- Phase 2 (2 large tasks): $8–16
|
||||
- Phase 3 (9 tasks): $10–20, or about half that if you skip the low findings
|
||||
- Reviews: $8–15
|
||||
|
||||
## Ground rules for every task
|
||||
- Stay within the task's scope. If a fix needs a file that another task owns, make the smallest change and note it in PROGRESS.md.
|
||||
- Don't change the design kit tokens in `src/styles/kit/` unless the task says so. Run `npm run sync:kit` if you do.
|
||||
- Keep both themes working. Check light and dark.
|
||||
- Existing tests must stay green. Add tests for new logic (formatters, clamping, fit rules).
|
||||
- A finding can be closed as "already fixed by Txx" or "not reproducible", with a one-line reason.
|
||||
The UI audit-fix campaign (7 October 2026: 19 tasks, the finding register, the review and the scripts that applied them) is finished.
|
||||
It is kept whole, unchanged, in [history/apps/host/audit-fixes](../../../history/apps/host/audit-fixes/README.md).
|
||||
Open items from it are in [../docs/BACKLOG.md](../docs/BACKLOG.md).
|
||||
@@ -0,0 +1,54 @@
|
||||
# UI backlog from the October 2026 campaign
|
||||
|
||||
When the dated UI documents moved to [history/apps/host](../../../history/apps/host/docs), the requirements in them that were not done, or not shown to be done, were copied here.
|
||||
Each row names its source in `history/`. "Open" means a search of `src/` and `server/` on 7 November 2026 found nothing for it. "Check" means related
|
||||
code exists but nothing shows the requirement was met or tested. This is a list to triage, not a promise.
|
||||
|
||||
## Open
|
||||
|
||||
| Item | Source |
|
||||
| --- | --- |
|
||||
| Measure dragging, scrolling and streaming together on representative hardware (`scripts/motionbudget.mjs` reports NOT MEASURED). | docs/engine-models-onboarding-2026-10-05.md, Remaining 6 |
|
||||
| A browser performance benchmark with a 3,000-row model catalog. | docs/engine-models-onboarding-2026-10-05.md, Verification |
|
||||
| Decide the model card metrics (B08: reading speed, answer speed and task quality) and the profile names (B13: keep Agentic / Balanced / Performance, or rename) with real participants. The shipped choices were provisional. | docs/persona-review-2026-10-05/decision-report.md, Unresolved choices |
|
||||
| Platform checks never run: a normal-browser export download, launch from the installed manifest, favicon fallbacks, OS appearance changes, forced-colors mode, a deliberate image failure, native icon packaging. | docs/design-improvements-2026-10-05.md, Remaining boundaries 2 |
|
||||
| A safety cue when the engine settings leave their defaults. | docs/plans/03-engine-tuning.md |
|
||||
| "Copy as cURL" in the Serving request drawer. | docs/plans/05-serving.md |
|
||||
| Settings: grouping and per-section resets. | docs/plans/08-settings.md |
|
||||
| First run: a confirmation about local data. | docs/plans/09-first-run.md |
|
||||
| Command palette: a Recents group. | docs/plans/11-command-palette.md |
|
||||
| Time expectations on long operations. | docs/plans/14-states.md |
|
||||
| An accessibility "fatigue budget". | docs/plans/16-accessibility.md |
|
||||
|
||||
## Check
|
||||
|
||||
| Item | Source |
|
||||
| --- | --- |
|
||||
| A screen-reader pass, forced colors, contrast in every interactive state, the reading-size and focus matrix. 200% browser zoom was checked for Engine, Models and setup only. | engine-models-onboarding (Remaining 5); design-improvements (UI-18); persona implementation-verification |
|
||||
| Model fit from measured free headroom and the native engine's real footprint (the recommendation uses memory class). | engine-models-onboarding (Remaining 4); design-improvements (MODEL-03) |
|
||||
| Benchmark evidence: engine build and driver identity, and rules for when a measurement stops being valid. | engine-models-onboarding, Remaining 3 |
|
||||
| Curated agent and tool candidates, the Balanced ordering and comparable throughput data. | engine-models-onboarding, Remaining 2 |
|
||||
| Download resume and browser fault injection on the native path. | engine-models-onboarding, Verification and Recovery |
|
||||
| Real subagent orchestration (the demo engine had none). | annotations-followup-2026-10-09.md |
|
||||
| PDF input to the model (pictures work). | annotations-followup-2026-10-09.md |
|
||||
| Scheduled tasks that use file, shell, web, email and calendar tools. | user-feedback-2026-10-06.md |
|
||||
| LAN sharing as an explicit opt-in, with a test. | user-feedback-2026-10-06.md; dsh-recon.md |
|
||||
| A Windows and Linux save picker for the Monitor image export. | ui-followup-2026-10-06.md |
|
||||
| A live Cloudflare tunnel run (only a connector fixture was used). | ui-followup-2026-10-06.md |
|
||||
| Live microphone dictation (tested with simulated events only). | annotations-19-2026-10-09.md |
|
||||
| One coordinated arrow-key navigation system for menus (optional). | design-improvements, UI-11-12 |
|
||||
| Session-log export delivery in a normal browser. | design-improvements, CHAT-08 |
|
||||
| Windows and Linux runtime lifecycle, native tool and image grants, privacy and logging, native sensors. | design-improvements, Remaining boundaries 1 |
|
||||
| A human credential workflow test for Serving; native cancellation; assistive-technology announcements for invalid fields. | design-improvements, SERVE-05 to 07, UI-01-04 |
|
||||
| Plans: first-run scan substance, a labelled theme toggle, gauge animation, skeletons, operation-failed empties, a motion duration budget. | docs/plans/09, 10, 12, 14, 15 |
|
||||
| Shared items with no recorded closure: the loaded model shown in four places, context described in different words, destructive actions styled like navigation, value fills that jump. | docs/plans/INDEX-CHECKLIST.md, "Present on multiple screens" |
|
||||
|
||||
## Not captured individually
|
||||
|
||||
- The 16 screen plans list about 190 numbered items, of which the index says about a quarter were executed. The rest stay in
|
||||
[history/apps/host/docs/plans](../../../history/apps/host/docs/plans/README.md); read the plan for a screen before changing that screen.
|
||||
- The audit-fix campaign left 80 of its 177 findings as "optional, not scheduled" (all low priority); 91 were implemented and 6 closed by removal.
|
||||
The finding ids and the task each belongs to are in [history/apps/host/audit-fixes/FINDING-STATUS.md](../../../history/apps/host/audit-fixes/FINDING-STATUS.md).
|
||||
By task: T11 chat 18, T13 serving 13, T09 model page 8, T15 skills and tasks 7, T10 engine page 6, T16 settings and setup 5, T18 preview panel 4,
|
||||
T01, T03, T12 and T19 three each, T05 and T17 two each, T04, T07 and T14 one each.
|
||||
- Declined by the owner, so not backlog: Tuning's nine flat fields in two groups, the Settings section order, renaming "More" to Settings, and repeating About's config paths.
|
||||
@@ -0,0 +1,47 @@
|
||||
# App developer docs
|
||||
|
||||
Current:
|
||||
|
||||
- [interaction-guidelines.md](interaction-guidelines.md): motion, materials, navigation and contrast rules. `scripts/audit-ui.mjs` and the style sheets cite its sections.
|
||||
- [design-system-notes.md](design-system-notes.md): how the app uses the kit styles in `src/styles/kit/`.
|
||||
- [BACKLOG.md](BACKLOG.md): UI requirements from the October 2026 campaign that are still open or unchecked.
|
||||
- [../README.md](../README.md): start here (build, preview, tests).
|
||||
|
||||
## How the host is organised
|
||||
|
||||
This replaces the 14 October 2026 native-engine handover (kept in [history](../../../history/apps/host/docs/native-engine-handover.md); it was written against engine 0.1.38).
|
||||
|
||||
| Module | Responsibility |
|
||||
| --- | --- |
|
||||
| `server/native-control.mjs` | The bridge to Strata Control and the installed engine: catalog, status, install jobs, saved job state, per-instance config copies, process start and stop, benchmark, calibration and updates. |
|
||||
| `server/native-settings.mjs` | Allowed per-model settings, their bounds, MCP configuration validation and credential masking. |
|
||||
| `server/native-gateway.mjs` | Forwards Chat Completions, Anthropic and Responses requests to the selected instance. |
|
||||
| `server/chat-images.mjs` | Validation of attached pictures. |
|
||||
| `src/lib/native.ts` | Shared native state and polling. |
|
||||
| `src/components/NativeModels.tsx` | Model setup, device assignment, version management and load. |
|
||||
| `src/components/NativeSettings.tsx`, `NativeMcpEditor.tsx` | Per-model parameters, JSON and tool-server configuration. |
|
||||
| `src/components/NativeMonitor.tsx` | Rates per instance, per-instance detail and host measurements. |
|
||||
|
||||
Contracts that still hold:
|
||||
|
||||
- Each loaded instance runs `serve/server.py` on its own private loopback port with a random API key, from a config copy in the app's data folder.
|
||||
The one public URL shown in Serving forwards to it; the public model id is an alias in that config, and unknown or ambiguous ids fail before streaming.
|
||||
- A source checkout finds a Strata installation through `STRATA_NATIVE_REPO` (it must contain `setup.py`, `tools/strata_mcp.py` and `serve/server.py`) and a Python through
|
||||
`STRATA_PYTHON` or `STRATA_NATIVE_PYTHON` (`server/python-resolver.mjs`). The installed app uses the Control that ships beside it instead.
|
||||
- Saving settings validates first, keeps unrelated keys and non-Strata environment values, writes a `.bak` and replaces the config atomically. New settings apply on the next load.
|
||||
|
||||
## Moved to history
|
||||
|
||||
Pointer: [history/README.md](../../../history/README.md). The files, unchanged:
|
||||
|
||||
| Was | Now |
|
||||
| --- | --- |
|
||||
| `audit-fixes/` (next to this folder) | [history/apps/host/audit-fixes](../../../history/apps/host/audit-fixes/README.md) |
|
||||
| `README-historical.md` | [history/apps/host/docs/README-historical.md](../../../history/apps/host/docs/README-historical.md) |
|
||||
| `native-engine-handover.md` | [history/apps/host/docs/native-engine-handover.md](../../../history/apps/host/docs/native-engine-handover.md) |
|
||||
| `annotations-19-2026-10-09.md`, `annotations-32-2026-10-09.md`, `annotations-followup-2026-10-09.md` | [history/apps/host/docs/](../../../history/apps/host/docs) |
|
||||
| `chat-shell-2026-10-06.md`, `ui-followup-2026-10-06.md` | [history/apps/host/docs/](../../../history/apps/host/docs) |
|
||||
| `design-improvements-2026-10-05.md`, `dsh-recon.md`, `engine-models-onboarding-2026-10-05.md` | [history/apps/host/docs/](../../../history/apps/host/docs) |
|
||||
| `user-feedback-2026-10-05.md`, `user-feedback-2026-10-06.md` | [history/apps/host/docs/](../../../history/apps/host/docs) |
|
||||
| `persona-review-2026-10-05/` | [history/apps/host/docs/persona-review-2026-10-05](../../../history/apps/host/docs/persona-review-2026-10-05/decision-report.md) |
|
||||
| `plans/` | [history/apps/host/docs/plans](../../../history/apps/host/docs/plans/README.md) |
|
||||
@@ -7,17 +7,13 @@ tints composited over `--st-surface` first.
|
||||
|
||||
## 1. How the app consumes the kit
|
||||
|
||||
`src/styles/kit/{tokens,components}.css` are synced verbatim from `../ui` by
|
||||
`npm run sync:kit` (the only rewrite is making the `@font-face` URLs absolute).
|
||||
`src/styles/kit/{tokens,components}.css` are the checked-in kit styles and the source of truth
|
||||
(there is no generator; the kit was first imported from a separate design workspace that is not in this repository).
|
||||
`src/styles/app.css` sits on top and contains, in order: extensions to the kit's
|
||||
classes, app shell layout, utilities, app-only components, and the first-run
|
||||
takeover. There are no gradients in it and no literal colours — every value is
|
||||
either a kit token or a layout constant.
|
||||
|
||||
```
|
||||
npm run sync:kit # re-import tokens/components/fonts/icons from ../ui
|
||||
```
|
||||
|
||||
Every component the kit defines is in use:
|
||||
|
||||
| Kit class | Where the app uses it |
|
||||
|
||||
@@ -6,7 +6,7 @@ measure` / `npm run motion` (runtime), so a violation fails a command rather tha
|
||||
a code review.
|
||||
|
||||
Companion documents: [`design-system-notes.md`](design-system-notes.md) for how
|
||||
the app maps onto the kit, [`dsh-recon.md`](dsh-recon.md) for the product this
|
||||
the app maps onto the kit, [`dsh-recon.md`](../../../history/apps/host/docs/dsh-recon.md) (archived) for the product this
|
||||
one is modelled on.
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Moved
|
||||
|
||||
The 5 October 2026 persona review (eight personas, debates, ballot, decision report) is kept in
|
||||
[history/apps/host/docs/persona-review-2026-10-05](../../../../history/apps/host/docs/persona-review-2026-10-05/decision-report.md).
|
||||
Open requirements from it are in [../BACKLOG.md](../BACKLOG.md).
|
||||
@@ -1,67 +1,4 @@
|
||||
# Strata · per-screen improvement plans
|
||||
# Moved
|
||||
|
||||
One plan per page and per scenario, written against the two rulebooks the app is
|
||||
built on — [interaction-guidelines.md](../interaction-guidelines.md) (Strata's own
|
||||
rules) and Apple's Liquid Glass HIG — and against what the code actually does
|
||||
today, not what it is meant to do.
|
||||
|
||||
Every plan follows the same shape: **what it is today** (grounded in the file and
|
||||
in measured numbers), **where it falls short**, **the plan**, **motion**, and
|
||||
**how the result is verified**. Claims about the present are checkable with the
|
||||
scripts in `scripts/`; claims about the future are written so they can be checked
|
||||
the same way once built.
|
||||
|
||||
## Screens
|
||||
|
||||
| # | Page | Route | Plan |
|
||||
|---|---|---|---|
|
||||
| 01 | Chat | `#/chat` | [01-chat.md](01-chat.md) |
|
||||
| 02 | Models | `#/models` | [02-models.md](02-models.md) |
|
||||
| 03 | Engine tuning | `#/tuning` | [03-engine-tuning.md](03-engine-tuning.md) |
|
||||
| 04 | Monitor | `#/monitor` | [04-monitor.md](04-monitor.md) |
|
||||
| 05 | Serving | `#/serving` | [05-serving.md](05-serving.md) |
|
||||
| 06 | Workspace | `#/workspace` | [06-workspace.md](06-workspace.md) |
|
||||
| 07 | Skills | `#/skills` | [07-skills.md](07-skills.md) |
|
||||
| 08 | Settings | `#/settings` | [08-settings.md](08-settings.md) |
|
||||
| 09 | First run | modal | [09-first-run.md](09-first-run.md) |
|
||||
|
||||
## Scenarios
|
||||
|
||||
| # | Scenario | Plan |
|
||||
|---|---|---|
|
||||
| 10 | Sidebar and navigation | [10-sidebar.md](10-sidebar.md) |
|
||||
| 11 | Command palette | [11-command-palette.md](11-command-palette.md) |
|
||||
| 12 | Context rail | [12-context-rail.md](12-context-rail.md) |
|
||||
| 13 | Overlays — drawers, menus, selects, popovers | [13-overlays.md](13-overlays.md) |
|
||||
| 14 | Empty, loading and error states | [14-states.md](14-states.md) |
|
||||
| 15 | Motion and fluidity | [15-motion.md](15-motion.md) |
|
||||
| 16 | Accessibility and long sessions | [16-accessibility.md](16-accessibility.md) |
|
||||
|
||||
## The shared vocabulary these plans assume
|
||||
|
||||
Fixed by the design system, so plans do not re-litigate them:
|
||||
|
||||
- **Light is the reference theme.** Every change is designed light-first and
|
||||
verified in dark, never the reverse.
|
||||
- **Capsules are for controls; the 8/14/20/24 scale is for surfaces.** Audit-enforced.
|
||||
- **Glass is only for the floating control layer.** Glass never samples glass;
|
||||
`npm run luminance` fails the build if a material ends up nested in another.
|
||||
- **One motion scale** — 100/160/240/320/460 ms, one standard curve, springs only
|
||||
for direct manipulation. No layout property is transitioned directly; grid
|
||||
tracks and opacity carry the movement.
|
||||
- **Contrast is measured on rendered pairings**, compositing alpha, in every
|
||||
interactive state.
|
||||
- **Reading measure is capped** at 68 characters in the conversation, verified at
|
||||
10, 16 and 22 px.
|
||||
|
||||
## Verification harness these plans refer to
|
||||
|
||||
```
|
||||
npm run check typecheck + audit + contrast + measure + luminance
|
||||
npm run audit off-scale values, materials, elevation, capsules, icons, native selects
|
||||
npm run contrast rendered pairings, composited, per state, both themes
|
||||
npm run measure characters per line at every content size
|
||||
npm run luminance dark-mode brightness budget + material layering
|
||||
npm run motion interruptibility, frame-sampled contact sheets
|
||||
npm run flow <name> scripted screen flows; `tour` and `tour-dark` cover every route
|
||||
```
|
||||
The 18 screen plans and their checklist (October 2026) are kept in [history/apps/host/docs/plans](../../../../history/apps/host/docs/plans/README.md).
|
||||
Open requirements from them are in [../BACKLOG.md](../BACKLOG.md).
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "strata",
|
||||
"version": "0.2.11",
|
||||
"version": "0.2.13",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "strata",
|
||||
"version": "0.2.11",
|
||||
"version": "0.2.13",
|
||||
"dependencies": {
|
||||
"html-to-image": "^1.11.13",
|
||||
"katex": "^0.19.0",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "strata",
|
||||
"private": true,
|
||||
"version": "0.2.11",
|
||||
"version": "0.2.13",
|
||||
"type": "module",
|
||||
"description": "Strata \u2014 local AI, tuned to one model. Web shell + engine host.",
|
||||
"scripts": {
|
||||
@@ -12,8 +12,7 @@
|
||||
"serve": "node server/index.mjs --dist dist",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"sync:kit": "node scripts/sync-design-kit.mjs",
|
||||
"shot": "node scripts/shoot.mjs",
|
||||
"shot": "node scripts/shoot.mjs",
|
||||
"flow": "node scripts/flow.mjs",
|
||||
"audit": "node scripts/audit-ui.mjs",
|
||||
"contrast": "node scripts/contrast.mjs",
|
||||
|
||||
@@ -17,7 +17,6 @@ import { fileURLToPath } from 'node:url';
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const STYLE_DIR = join(ROOT, 'src/styles');
|
||||
const KIT_SRC = join(ROOT, '..', 'ui');
|
||||
const KIT_COPY = join(ROOT, 'src/styles/kit');
|
||||
|
||||
/** The app's own stylesheets, in cascade order: the last one wins. */
|
||||
@@ -264,24 +263,8 @@ const allAppCss = APP_SHEETS.map((sheet) => sheet.css).join('\n');
|
||||
/* ------------------------------------------------- kit sync + motion scale -- */
|
||||
|
||||
for (const file of ['tokens.css', 'components.css']) {
|
||||
const a = join(KIT_SRC, file);
|
||||
const b = join(KIT_COPY, file);
|
||||
// Some checkouts of this branch do not carry the design-kit source (apps/ui),
|
||||
// only the synced copy. There is nothing to compare against then, so the check
|
||||
// reports that instead of failing every run with a condition no one can fix.
|
||||
if (!existsSync(a)) {
|
||||
notes.push(`kit copy: ${file} has no source in this checkout — drift check skipped`);
|
||||
continue;
|
||||
}
|
||||
if (!existsSync(b)) {
|
||||
fail('kit', 0, `${file} missing from src/styles/kit — run npm run sync:kit`);
|
||||
continue;
|
||||
}
|
||||
// The sync rewrites @font-face URLs to absolute; everything else must match.
|
||||
const norm = (t) => t.replace(/url\((["']?)\/fonts\//g, 'url($1fonts/').trim();
|
||||
if (norm(readFileSync(a, 'utf8')) !== norm(readFileSync(b, 'utf8'))) {
|
||||
fail('kit', 0, `${file} has drifted from ../ui — run npm run sync:kit`);
|
||||
}
|
||||
// The checked-in files in src/styles/kit are the source of the kit styles; there is no generator.
|
||||
if (!existsSync(join(KIT_COPY, file))) fail('kit', 0, `${file} is missing from src/styles/kit (restore it from git)`);
|
||||
}
|
||||
|
||||
// Which selectors have been given the capsule radius. Checked across the whole
|
||||
@@ -328,7 +311,7 @@ for (const file of [...readdirSync(join(ROOT, 'src'), { recursive: true })].filt
|
||||
for (const m of readFileSync(full, 'utf8').matchAll(/<(?:Icon|IconBtn)\s[^>]*?\bname="([a-z-]+)"/g)) referenced.add(m[1]);
|
||||
}
|
||||
for (const name of referenced) {
|
||||
if (!symbols.has(name)) fail('src', 0, `icon "${name}" is referenced but not in the sprite — run npm run sync:kit`);
|
||||
if (!symbols.has(name)) fail('src', 0, `icon "${name}" is referenced but not in the sprite: add a <symbol id="i-NAME"> to public/icons/sprite.svg`);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------- undefined classes -- */
|
||||
|
||||
@@ -202,6 +202,24 @@ add("Drafting: off (the cards were short of memory)",
|
||||
"Previsão: desligada (faltou memória nas placas)",
|
||||
"초안 예측: 꺼짐(카드 메모리가 부족했습니다)",
|
||||
"Anticipation : désactivée (les cartes manquaient de mémoire)")
|
||||
add("On your Mac, DFlash2 was faster than MTP on this model in our tests.",
|
||||
"在您的 Mac 上,我们的测试中 DFlash2 在此模型上比 MTP 更快。",
|
||||
"お使いの Mac では、私たちのテストでこのモデルにおいて DFlash2 は MTP より速くなりました。",
|
||||
"Auf Ihrem Mac war DFlash2 bei diesem Modell in unseren Tests schneller als MTP.",
|
||||
"En su Mac, en nuestras pruebas DFlash2 fue más rápido que MTP con este modelo.",
|
||||
"На вашем Mac в наших тестах DFlash2 с этой моделью был быстрее MTP.",
|
||||
"No seu Mac, nos nossos testes o DFlash2 foi mais rápido que o MTP com este modelo.",
|
||||
"고객님의 Mac에서 저희 테스트 결과 이 모델에서는 DFlash2가 MTP보다 빨랐습니다.",
|
||||
"Sur votre Mac, DFlash2 a été plus rapide que MTP avec ce modèle lors de nos tests.")
|
||||
add("DFlash2 is off by default on this graphics card for now; choose it in Drafting to use it.",
|
||||
"目前在这张显卡上 DFlash2 默认关闭;如需使用,请在“预测草稿”中选择它。",
|
||||
"現在、このグラフィックスカードでは DFlash2 は既定でオフです。使うには「先読み」で選んでください。",
|
||||
"DFlash2 ist auf dieser Grafikkarte vorerst standardmäßig aus; wählen Sie es unter „Vorausraten“ aus, um es zu nutzen.",
|
||||
"DFlash2 está desactivado de forma predeterminada en esta tarjeta gráfica por ahora; elíjalo en Predicción para usarlo.",
|
||||
"На этой видеокарте DFlash2 пока выключен по умолчанию; чтобы использовать его, выберите его в разделе «Предсказание».",
|
||||
"O DFlash2 está desligado por predefinição nesta placa de vídeo por agora; escolha-o em Previsão para o usar.",
|
||||
"현재 이 그래픽 카드에서는 DFlash2가 기본적으로 꺼져 있습니다. 사용하려면 ‘초안 예측’에서 선택하세요.",
|
||||
"DFlash2 est désactivé par défaut sur cette carte graphique pour le moment ; choisissez-le dans Anticipation pour l'utiliser.")
|
||||
here = Path(__file__).resolve().parent.parent / "src" / "locales"
|
||||
NBSP = " "
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
/**
|
||||
* Sync the Strata UI kit (../ui) into the app's public/ folder.
|
||||
*
|
||||
* node scripts/sync-design-kit.mjs
|
||||
*
|
||||
* Copies fonts, and rebuilds a single clean icon sprite at public/icons/sprite.svg.
|
||||
* The kit's own SVG files carry C2PA manifest metadata (~12 KB each, base64);
|
||||
* we strip <metadata> so the shipped sprite stays small.
|
||||
*
|
||||
* Also mirrors tokens.css / components.css into src/styles/kit/ so the app has a
|
||||
* versioned copy that can be diffed against the kit.
|
||||
*/
|
||||
import { readFileSync, writeFileSync, mkdirSync, readdirSync, copyFileSync, existsSync } from 'node:fs';
|
||||
import { join, dirname, basename } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const root = join(here, '..');
|
||||
const kit = join(root, '..', 'ui');
|
||||
const pub = join(root, 'public');
|
||||
const styles = join(root, 'src', 'styles', 'kit');
|
||||
|
||||
if (!existsSync(kit)) {
|
||||
console.error(`Design kit not found at ${kit}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
mkdirSync(join(pub, 'fonts'), { recursive: true });
|
||||
mkdirSync(join(pub, 'icons'), { recursive: true });
|
||||
mkdirSync(styles, { recursive: true });
|
||||
|
||||
// ---- fonts -----------------------------------------------------------------
|
||||
let fonts = 0;
|
||||
for (const f of readdirSync(join(kit, 'fonts'))) {
|
||||
if (!f.endsWith('.woff2')) continue;
|
||||
copyFileSync(join(kit, 'fonts', f), join(pub, 'fonts', f));
|
||||
fonts++;
|
||||
}
|
||||
|
||||
// ---- icon sprite -----------------------------------------------------------
|
||||
const strip = (svg) =>
|
||||
svg
|
||||
.replace(/<metadata>[\s\S]*?<\/metadata>/g, '')
|
||||
.replace(/<svg[^>]*>/, '')
|
||||
.replace(/<\/svg>\s*$/, '')
|
||||
.replace(/\s+xmlns(:\w+)?="[^"]*"/g, '')
|
||||
.trim();
|
||||
|
||||
const iconDir = join(kit, 'icons');
|
||||
const sources = new Map(readdirSync(iconDir)
|
||||
.filter((f) => f.endsWith('.svg') && f !== 'sprite.svg')
|
||||
.map((f) => [basename(f, '.svg'), join(iconDir, f)]));
|
||||
const appIconDir = join(root, 'src', 'assets', 'icons');
|
||||
if (existsSync(appIconDir)) {
|
||||
for (const file of readdirSync(appIconDir).filter(f => f.endsWith('.svg'))) {
|
||||
sources.set(basename(file, '.svg'), join(appIconDir, file));
|
||||
}
|
||||
}
|
||||
const names = [...sources.keys()].sort();
|
||||
|
||||
const symbols = names.map((name) => {
|
||||
const raw = readFileSync(sources.get(name), 'utf8');
|
||||
const body = strip(raw);
|
||||
const viewBox = /viewBox="([^"]+)"/.exec(raw)?.[1] ?? '0 0 24 24';
|
||||
// The root <svg> carries fill="none"; without it every stroked path inherits
|
||||
// the default black fill and the icons render as solid blobs.
|
||||
const rootFill = /<svg[^>]*\sfill="([^"]+)"/.exec(raw)?.[1] ?? 'none';
|
||||
return ` <symbol id="i-${name}" viewBox="${viewBox}" fill="${rootFill}">\n ${body}\n </symbol>`;
|
||||
});
|
||||
|
||||
const sprite = `<svg xmlns="http://www.w3.org/2000/svg" style="display:none" aria-hidden="true">\n${symbols.join('\n')}\n</svg>\n`;
|
||||
writeFileSync(join(pub, 'icons', 'sprite.svg'), sprite);
|
||||
|
||||
// ---- styles ----------------------------------------------------------------
|
||||
// The kit references fonts relatively (fonts/x.woff2). In the app the fonts are
|
||||
// served from public/fonts, so rewrite the URLs to be absolute — otherwise the
|
||||
// browser resolves them against the CSS file and 404s in both dev and build.
|
||||
for (const f of ['tokens.css', 'components.css']) {
|
||||
const css = readFileSync(join(kit, f), 'utf8').replace(/url\((["']?)fonts\//g, 'url($1/fonts/');
|
||||
writeFileSync(join(styles, f), css);
|
||||
}
|
||||
|
||||
const kb = (n) => `${(n / 1024).toFixed(1)} KB`;
|
||||
console.log(`synced ${fonts} fonts -> public/fonts/`);
|
||||
console.log(`sprite: ${names.length} icons -> public/icons/sprite.svg (${kb(sprite.length)})`);
|
||||
console.log(`styles: tokens.css, components.css -> src/styles/kit/`);
|
||||
console.log(`icons: ${names.join(', ')}`);
|
||||
@@ -11,6 +11,8 @@ import {tmpdir} from 'node:os';
|
||||
import {join} from 'node:path';
|
||||
const scratch=mkdtempSync(join(tmpdir(),'strata-host-suite-profile-'));
|
||||
const env={...process.env,STRATA_SETTINGS_FILE:join(scratch,'app-settings.json'),STRATA_COMPONENTS_DIR:join(scratch,'components'),LOCALAPPDATA:join(scratch,'Local'),APPDATA:join(scratch,'Roaming'),XDG_DATA_HOME:join(scratch,'xdg-data'),XDG_STATE_HOME:join(scratch,'xdg-state'),XDG_CONFIG_HOME:join(scratch,'xdg-config')};
|
||||
const child=spawn(process.execPath,['--test',...files],{cwd:root,stdio:'inherit',env});
|
||||
/* A test that never finishes fails after STRATA_TEST_TIMEOUT_MS (default 10 minutes) instead of holding the whole run for ever; extra arguments (a reporter, --test-name-pattern) go to node --test. */
|
||||
const timeout=Number(process.env.STRATA_TEST_TIMEOUT_MS)||600000;
|
||||
const child=spawn(process.execPath,['--test',`--test-timeout=${timeout}`,...process.argv.slice(2),...files],{cwd:root,stdio:'inherit',env});
|
||||
child.on('error',error=>{console.error(error.message);process.exitCode=1;});
|
||||
child.on('exit',(code,signal)=>{process.exitCode=code??(signal?1:0);});
|
||||
@@ -6,7 +6,7 @@
|
||||
* per saved API connection, so that each chat can pick its own model. This module writes the same two
|
||||
* Cordis entries (`agent-default-model`, `llm-pi-ai`) for the current set.
|
||||
*
|
||||
* Rules carried over from the integration notes (docs/work/IMPLEMENTATION_STATUS.md section 1.8):
|
||||
* Rules carried over from the integration notes (history/docs/work/IMPLEMENTATION_STATUS.md section 1.8):
|
||||
* - `llm-pi-ai` config is `{ providers: { "<route>": {...} } }`; a route key at the top level is
|
||||
* silently ignored.
|
||||
* - the output budget must fit the whole request, so `maxTokens` is derived from the real context.
|
||||
|
||||
@@ -22,7 +22,7 @@ export function refusedFolder(dir) {
|
||||
const win = (process.env.SystemRoot ?? 'C:\\Windows').toLowerCase();
|
||||
const q = p.toLowerCase();
|
||||
if (q === win || q.startsWith(win + sep) || /^[a-z]:\\(program files( \(x86\))?|programdata)(\\|$)/.test(q)) return 'That is a system folder. Choose one of your own folders.';
|
||||
} else if (/^\/(bin|boot|dev|etc|lib|lib64|proc|root|run|sbin|sys|usr|var)(\/|$)/.test(p)) return 'That is a system folder. Choose one of your own folders.';
|
||||
} else if (/^\/(bin|boot|dev|etc|lib|lib64|proc|root|run|sbin|sys|usr|var)(\/|$)/.test(p) && !/^\/run\/media\/[^/]+\/./.test(p)) return 'That is a system folder. Choose one of your own folders.';
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -288,18 +288,18 @@ export function createControl({ control = findControl(), env = process.env, run
|
||||
throw Object.assign(failure(result, 'The partial copies could not be removed.'), { code: result.json?.error?.code ?? 'leave_failed', details: result.json?.error?.details ?? '' });
|
||||
},
|
||||
/**
|
||||
* The drafter for a model's run config and the drafting choice (`strata drafting plan`): { needed, gb, line, note, drafter, split, mtp_needed, mtp_gb }. Control decides
|
||||
* The drafter for a model's run config and the drafting choice (`strata drafting plan`): { needed, tuned_needed, gb, drafter_gb, tuned, file, line, note, drafter, split, mtp_needed, mtp_gb }. `model` (with `family`) names a model that has no run config yet. Control decides
|
||||
* (the table of strata_control/drafting.py and the all-cards split engine's rule, strata_control/split_engine.py: the same answer as the Terminal's); the app never reads
|
||||
* it. Nothing is downloaded. `noSplit` asks as if the split engine will not take the start (only after the draft layer for the split could not be downloaded).
|
||||
*/
|
||||
async draftingPlan({ configPath = null, mode = null, family = null, dataDir = null, noSplit = false, unloading = false } = {}) {
|
||||
const result = await call(['drafting', 'plan', ...(configPath ? ['--config', configPath] : []), ...(mode ? ['--mode', mode] : []), ...(family ? ['--family', family] : []), ...(dataDir ? ['--data-dir', dataDir] : []), ...(noSplit ? ['--no-split'] : []), ...(unloading ? ['--engines-unloading'] : []), '--output', 'json'], { timeoutMs: 30_000 });
|
||||
async draftingPlan({ configPath = null, mode = null, family = null, model = null, dataDir = null, noSplit = false, unloading = false } = {}) {
|
||||
const result = await call(['drafting', 'plan', ...(configPath ? ['--config', configPath] : []), ...(mode ? ['--mode', mode] : []), ...(family ? ['--family', family] : []), ...(model && !configPath ? ['--model', model] : []), ...(dataDir ? ['--data-dir', dataDir] : []), ...(noSplit ? ['--no-split'] : []), ...(unloading ? ['--engines-unloading'] : []), '--output', 'json'], { timeoutMs: 30_000 });
|
||||
if (result.json?.ok) return result.json;
|
||||
throw failure(result, 'The drafter could not be checked.');
|
||||
},
|
||||
/** Fetch and pack the drafter when the choice would use it and it is not here (`strata drafting fetch --if-needed`); `onEvent` gets {step, done, total}. */
|
||||
async draftingFetch({ configPath, mode = null, noSplit = false, unloading = false, onEvent = null, signal = null } = {}) {
|
||||
const args = ['drafting', 'fetch', '--config', configPath, ...(mode ? ['--mode', mode] : []), ...(noSplit ? ['--no-split'] : []), ...(unloading ? ['--engines-unloading'] : []), '--if-needed', '--output', 'jsonl'];
|
||||
async draftingFetch({ configPath, mode = null, noSplit = false, unloading = false, throttle = false, onEvent = null, signal = null } = {}) {
|
||||
const args = ['drafting', 'fetch', '--config', configPath, ...(mode ? ['--mode', mode] : []), ...(noSplit ? ['--no-split'] : []), ...(unloading ? ['--engines-unloading'] : []), '--if-needed', ...(throttle ? ['--throttle'] : []), '--output', 'jsonl'];
|
||||
const result = await call(args, { timeoutMs: 3 * 3600_000, acquire: true, onLine: onEvent ?? (() => {}), signal });
|
||||
if (result.json?.ok) return result.json;
|
||||
throw Object.assign(new ControlError(result.json?.error?.message ?? (result.stderr.split(/\r?\n/).filter(Boolean).pop() || 'The DFlash2 drafter could not be downloaded.'), { exitCode: result.code, output: result.json }), { code: 'drafting_failed' });
|
||||
|
||||
@@ -1098,7 +1098,7 @@ async function api(req, res, path, method, url) {
|
||||
if(nativeInstance&&method==='POST')return json(res,200,nativeInstance[2]==='unload'?await native.unload(nativeInstance[1]):native.setDefault(nativeInstance[1]));
|
||||
const nativeSettings=/^\/api\/native\/models\/([a-z0-9_.-]+)\/settings$/.exec(path);
|
||||
/* Expert mode shows what will actually run: the saved tuning's values come with the settings (null when none applies or unreadable). */
|
||||
if(nativeSettings&&method==='GET'){const settings=native.settings(nativeSettings[1]);let tuned=null;try{const t=await native.tuneEffective({model:nativeSettings[1],resourceProfile:url.searchParams.get('resourceProfile')||undefined,vision:url.searchParams.get('vision')||undefined});tuned=t.applied?t:null;}catch{tuned=null;}return json(res,200,{...settings,tuned});}
|
||||
if(nativeSettings&&method==='GET'){const settings=native.settings(nativeSettings[1]);let tuned=null;try{const t=await native.tuneEffective({model:nativeSettings[1],resourceProfile:url.searchParams.get('resourceProfile')||undefined,vision:url.searchParams.get('vision')||undefined});tuned=t.applied?t:null;}catch{tuned=null;}return json(res,200,{...settings,tuned,hints:await native.settingsHints?.(nativeSettings[1])});}
|
||||
if(nativeSettings&&method==='PATCH')return json(res,200,native.save(nativeSettings[1],await readBody(req)));
|
||||
const nativeSettingsFile=/^\/api\/native\/models\/([a-z0-9_.-]+)\/settings-file$/.exec(path);
|
||||
if(nativeSettingsFile&&method==='GET')return json(res,200,native.settingsFile(nativeSettingsFile[1]));
|
||||
|
||||
@@ -7,7 +7,7 @@ import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, realpa
|
||||
import { join, resolve, dirname, basename, sep, relative, isAbsolute } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createServer } from 'node:net';
|
||||
import { NATIVE_FIELDS, APP_OWNED_FIELDS, withoutDraftLayer, resolveDrafter, validateNativeSettings, validateMcpServers, publicMcpServers, publicNativeEnv, restoreNativeEnv, speedSwitchRows } from './native-settings.mjs';
|
||||
import { draftingHint, NATIVE_FIELDS, APP_OWNED_FIELDS, withoutDraftLayer, resolveDrafter, validateNativeSettings, validateMcpServers, publicMcpServers, publicNativeEnv, restoreNativeEnv, speedSwitchRows } from './native-settings.mjs';
|
||||
import { resourceSettings, saferStart, reserveChoice } from './native-resources.mjs';
|
||||
import { USER_KEY, capture as captureUserArgs, merge as mergeUserArgs, setFlag as setUserFlag, stamped as stampedUserArgs } from './native-user-args.mjs';
|
||||
import { resolvePython, venvPython } from './python-resolver.mjs';
|
||||
@@ -164,11 +164,13 @@ export function createNativeControl({directory,requests=()=>[],changed=()=>{},sp
|
||||
/* A mistake in the file holds the start back with the line, instead of an engine that dies on it. Warnings (an unknown key) never do. */
|
||||
function checkedSettingsConfig(text){const check=checkAndParseRunConfig(text);if(!check.ok)throw new Error(`The settings file for this model has a mistake, so the model was not started. ${describeIssues(check.issues,'error').join(' ')}`);return check.config;}
|
||||
function settings(model){const {entry,config}=record(model);const values=Object.fromEntries(NATIVE_FIELDS.map(f=>[f.id,config[f.id]??null]));if(typeof values.aliases==='string')values.aliases=values.aliases.split(',').map(v=>v.trim()).filter(Boolean);values.sampling=config.sampling??{};values.args=config.args??[];values.env=publicNativeEnv(config.env??{});return {model,values,mcp:publicMcpServers(config.mcp_servers??config.mcpServers??{}),restartNeeded:true,capabilities:{dsExperts:ownEngine(config)&&dsExpertsSupported(config)},speedSwitches:speedSwitchRows(config.env??{},{deepseek:ownEngine(config),arch:config.arch??null,backend:config.backend??entry?.backend??snapshot.installed?.engine?.backend??null,gpus:snapshot.hardware?.gpus??[]})};}
|
||||
/* The Drafting hint of this model's settings: the size of the drafter DFlash2 would fetch for it (Control's plan with the choice DFlash2); the generic hint when Control does not say. */
|
||||
async function settingsHints(model){try{if(!packaged||typeof control.draftingPlan!=='function')return {};const {path}=recordFile(model),d=await control.draftingPlan({configPath:path,mode:'dflash2'});return Number(d?.drafter_gb)>0?{drafting:draftingHint(d.drafter_gb)}:{};}catch{return {};}}
|
||||
function save(model,{values,mcp}){if([...jobs.values()].some(j=>j.state==='running'))throw new Error('Finish the current operation before saving native settings.');const {path,config}=record(model);if(busy(requests().filter(r=>instances.get(r.instanceId)?.model===model)))throw new Error('Finish this model’s requests before saving its settings.');const patch=validateNativeSettings(values??{}),next={...config};if(patch.ds_experts!=null&&!dsExpertsSupported(config))throw new Error('The Speed choice needs the newer DeepSeek engine. Update the engine in Settings, then choose it.');for(const field of nativeFields())if(field.supported===false&&patch[field.id]!=null&&JSON.stringify(patch[field.id])!==JSON.stringify(config[field.id]))throw new Error(`${field.label} needs a newer native Strata installation. Update it in Settings → About.`);for(const [key,value] of Object.entries(patch)){if(key==='env')next.env=restoreNativeEnv(value,config.env??{});else if(value===null)delete next[key];else next[key]=value;}if('vram_reserve_mib' in patch){const base=captureUserArgs(next);const mine=setUserFlag(base,'--vram-reserve-mib',patch.vram_reserve_mib);if(mine.length)next[USER_KEY]=mine;else delete next[USER_KEY];next.args=mine.length&&patch.vram_reserve_mib!==null?mergeUserArgs(next.args??[],mine):setUserFlag(next.args??[],'--vram-reserve-mib',null);delete next.ui_reserve_applied;if(Array.isArray(next.args_written))next.args_written=[...next.args];}if(mcp!==undefined){next.mcp_servers=validateMcpServers(mcp,config.mcp_servers??config.mcpServers??{});delete next.mcpServers;}copyFileSync(path,path+'.bak');chmodSync(path+'.bak',0o600);const temp=path+'.ui-tmp';writeFileSync(temp,JSON.stringify(next,null,2)+'\n',{mode:0o600});renameSync(temp,path);return settings(model);}
|
||||
function previousEngine(){if(packaged)return !!snapshot.engine?.previous;try{const data=snapshot.installed?.data_dir??snapshot.installed?.data_folder;return [join(preferences.repo,'engine/.previous'),data&&join(data,'engine/.previous')].filter(Boolean).some(existsSync)&&readFileSync(join(preferences.repo,'setup.py'),'utf8').includes('--rollback-engine');}catch{return false;}}
|
||||
async function plan(args){await refresh();if(!snapshot.available)throw new Error(snapshot.error);const result=await tool('strata_install',installArgs(args,false));
|
||||
/* A chosen drafter that is not on this computer yet is part of the download: its size is listed beside the model's (Control decides; nothing is fetched here). */
|
||||
const choice=args.drafting??(dflash2Served===true?'auto':undefined);if(packaged&&typeof choice==='string'&&typeof control.draftingPlan==='function'){try{const size=snapshot.catalog?.families?.find(f=>f.family===args.family)?.sizes.find(s=>s.model===args.model),here=snapshot.installed?.models?.find(m=>m.model===size?.id);const d=await control.draftingPlan({mode:choice,family:args.family??null,dataDir:here?.runDataFolder??snapshot.installed?.data_folder??null,...(unloadsRunning(args)?{unloading:true}:{})});if(d.needed||d.mtp_needed)return {...result,drafting:{needed:true,drafter:d.needed?'dflash2':'mtp',gb:d.needed?d.gb:d.mtp_gb,line:d.line}};}catch{/* the model plan still stands */}}
|
||||
const choice=args.drafting??(dflash2Served===true?'auto':undefined);if(packaged&&typeof choice==='string'&&typeof control.draftingPlan==='function'){try{const size=snapshot.catalog?.families?.find(f=>f.family===args.family)?.sizes.find(s=>s.model===args.model),here=snapshot.installed?.models?.find(m=>m.model===size?.id);const d=await control.draftingPlan({mode:choice,family:args.family??null,model:args.model??null,dataDir:here?.runDataFolder??snapshot.installed?.data_folder??null,...(unloadsRunning(args)?{unloading:true}:{})});if(d.needed||d.mtp_needed)return {...result,drafting:{needed:true,drafter:d.needed?'dflash2':'mtp',gb:d.needed?d.gb:d.mtp_gb,line:d.line,...(d.needed&&d.tuned===true?{tuned:true}:{})}};}catch{/* the model plan still stands */}}
|
||||
return result;}
|
||||
/* The folder the person picked in the first-run chooser can be the one that holds their GGUF files (or a folder inside it). It is theirs, not a Strata data folder: packs are never made in it, and a Strata folder around it (<Strata>/models/<tag>) is used instead (#3507, #3424). */
|
||||
function ownFilesDataFolder(candidate,args){if(!candidate||args.data_dir!==undefined||!args.gguf_dir)return candidate;const same=(a,b)=>{const x=resolve(a),y=resolve(b);return process.platform==='win32'?x.toLowerCase()===y.toLowerCase():x===y;};const inside=(a,b)=>{const rel=relative(resolve(b),resolve(a));return rel===''||(!!rel&&!rel.startsWith('..')&&!isAbsolute(rel));};if(!same(candidate,args.gguf_dir)&&!inside(candidate,args.gguf_dir))return candidate;const models=dirname(resolve(args.gguf_dir)),top=dirname(models);return basename(models).toLowerCase()==='models'&&/strata/i.test(basename(top))?top:undefined;}
|
||||
@@ -326,12 +328,31 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
async function draftPlan(args,configFile,{noSplit=false}={}){
|
||||
if(!packaged||typeof control.draftingPlan!=='function')return null;
|
||||
const asked=args.values?.drafting;
|
||||
/* The plan is for the cards this Load uses: the screen's own card choice goes over the ones the saved settings name (the all-cards split engine needs two or more). */
|
||||
try{return await withPlanFile(args,configFile,planFile=>control.draftingPlan({configPath:planFile,...(asked===undefined?{}:{mode:asked===null?'auto':asked}),...(noSplit?{noSplit:true}:{}),...(unloadsRunning(args)?{unloading:true}:{})}));}catch{return null;}
|
||||
}
|
||||
/* The plan is for the cards this Load uses: the screen's own card choice goes over the ones the saved settings name (the all-cards split engine needs two or more). `use` gets the file to hand to Control; a temporary one is removed after. */
|
||||
async function withPlanFile(args,configFile,use){
|
||||
let planFile=configFile,temporary=null;
|
||||
if(Array.isArray(args.gpus)&&args.gpus.length&&args.gpus.every(Number.isInteger)){try{const work=join(directory,'native-instances');mkdirSync(work,{recursive:true});temporary=join(work,'drafting-plan-'+randomUUID()+'.json');writeFileSync(temporary,JSON.stringify({...readJson(configFile),gpu:args.gpus.length===1?args.gpus[0]:args.gpus}),{mode:0o600});planFile=temporary;}catch{temporary=null;planFile=configFile;}}
|
||||
try{return await control.draftingPlan({configPath:planFile,...(asked===undefined?{}:{mode:asked===null?'auto':asked}),...(noSplit?{noSplit:true}:{}),...(unloadsRunning(args)?{unloading:true}:{})});}catch{return null;}
|
||||
try{return await use(planFile);}
|
||||
finally{if(temporary){try{rmSync(temporary,{force:true});}catch{/* best effort */}}}
|
||||
}
|
||||
/* Auto and its drafter (the tuned one where the set has one, else the standard DFlash2 drafter: whatever Control's Auto plan says is `needed`), on every path that starts a model: a plain load, a restart of the stopped models and a tuning run fetch it as the Models screen's own Load does (Control: pinned, checked, marked, licence and notice), but they were not asked for it, so
|
||||
the 12 hour rule of the Terminal's launch applies (--throttle: a feed that had nothing a short while ago is not asked again, a feed that does not answer is not an error). A choice the person named or saved (MTP, DFlash2, off) wins: this never runs for it. */
|
||||
function autoChoice(value){return value==null||String(value).toLowerCase()==='auto';}
|
||||
async function fetchAutoDrafter(job,args,configFile,drafts,{noSplit=false,after}={}){
|
||||
if(!packaged||typeof control.draftingFetch!=='function'||drafts?.needed!==true||!autoChoice(readJson(configFile).drafting))return false;
|
||||
job.engineAbort??=new AbortController();
|
||||
try{
|
||||
const got=await withPlanFile(args,configFile,planFile=>control.draftingFetch({configPath:planFile,mode:'auto',throttle:true,...(noSplit?{noSplit:true}:{}),...(unloadsRunning(args)?{unloading:true}:{}),signal:job.engineAbort.signal,onEvent:event=>{if(event.event==='progress'&&job.state==='running'){job.summary=event.step;job.details=event.total>1&&event.done<event.total?`${(event.done/1e9).toFixed(2)} of ${(event.total/1e9).toFixed(2)} GB`:undefined;emit();}}}));
|
||||
if(job.state==='running'&&after){job.summary=after;job.details=undefined;emit();}
|
||||
return got?.pack!=null&&got?.skipped!==true;
|
||||
}catch(error){if(job.state==='running'){job.drafterNote=error.message;if(after){job.summary=after;job.details=undefined;}emit();}return false;}
|
||||
}
|
||||
/* The same for a path that has no Load of its own around it (a restart of stopped models, a tuning run): plan the saved settings file, then fetch. */
|
||||
async function autoDrafterFor(job,model,after){
|
||||
try{const {path}=recordFile(model),args={model,multiple:true},drafts=await draftPlan(args,path);return await fetchAutoDrafter(job,args,path,drafts,{after});}catch{return false;}
|
||||
}
|
||||
/* The choice changed to the MTP and its layer is not here yet: it is fetched once, by the installer (finished steps are skipped), in this Load. Only the Models screen's own Load carries the choice; a restart or a tuning run never fetches. */
|
||||
async function installMtpLayer(job,args,config){
|
||||
const fam=snapshot.catalog?.families?.find(f=>f.sizes.some(s=>s.id===args.model)),size=fam?.sizes.find(s=>s.id===args.model);
|
||||
@@ -434,6 +455,8 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
if(drafts?.mtp_needed===true&&(args.values?.drafting!==undefined||drafts.split===true&&(config.drafting==null||String(config.drafting).toLowerCase()==='auto'))){/* the Models screen's own Load carries the choice; a plain Auto load fetches the layer too when Control says the all-cards split engine will take the start (it runs the MTP and nothing else): the same as the Terminal */await installMtpLayer(job,args,config);if(job.state!=='running')return;await refresh(true);config=checkedSettingsConfig(readFileSync(recordFile(args.model).path,'utf8'));drafts=await draftPlan(args,configFile);
|
||||
/* The draft layer for the split could not be had (no internet, disk): the load goes on as the 0.2.10 app did, the normal layer split with the plan DFlash2's again (the Models screen's own Load fetches that drafter below). One plain line says so. */
|
||||
if(drafts?.split===true&&drafts.mtp_needed===true){noSplit=true;job.drafterNote=job.drafterNote??'The MTP drafter could not be downloaded.';job.summary='Loading the model…';emit();drafts=await draftPlan(args,configFile,{noSplit:true});}}
|
||||
/* A plain load (the Models screen's own Load carries its choice and fetches below): Auto on a set that has the tuned drafter fetches it now, so the start uses it. Not on this computer and not reachable: the MTP or the stock drafter runs as before. */
|
||||
if(args.values?.drafting===undefined&&drafts?.needed===true){const got=await fetchAutoDrafter(job,args,configFile,drafts,{noSplit,after:'Loading the model…'});if(job.state!=='running')return;if(got)drafts=await draftPlan(args,configFile,{noSplit});}
|
||||
const {cfg,resources,visionPreferences,parallelChoice,parallelNote:chosenNote,dsNote}=buildConfig(args,config,drafts?.choice==='dflash2'&&drafts.needed===true&&args.values?.drafting===undefined?'mtp':drafts?.choice??null);/* (a drafter that is not here and will not be fetched by this load: the MTP runs, and the request slots stay) */
|
||||
let parallelNote=chosenNote;
|
||||
await prepareDrafter(job,args,cfg,configFile,drafts,{noSplit});if(job.state!=='running')return;
|
||||
@@ -607,6 +630,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
for(const old of offer.instances){
|
||||
const instance={...old,status:'loading',phase:'loading',error:null,metrics:null,loadedAt:new Date().toISOString()};
|
||||
instances.set(instance.id,instance);if(!defaultId||!instances.has(defaultId))defaultId=instance.id;job.summary=`Starting ${instance.label}. Loading can take a few minutes.`;emit();
|
||||
await autoDrafterFor(job,instance.model,`Starting ${instance.label}. Loading can take a few minutes.`);
|
||||
let wait=960;try{const {config,entry}=record(instance.model);wait=startWaitFor(config,entry,instance.model);}catch{/* the usual wait */}
|
||||
try{
|
||||
await control.start(instance.profile,{waitSeconds:wait});
|
||||
@@ -661,6 +685,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
const {path}=tuneConfig(args);
|
||||
if(externalRunning().length)throw new Error('Stop the native server running outside this UI before tuning.');
|
||||
if(busy(requests().filter(r=>instances.has(r.instanceId))))throw new Error('Finish requests before tuning.');
|
||||
await autoDrafterFor(job,args.model,'Finding the fastest settings for your PC…');if(job.state!=='running')return;
|
||||
for(const id of [...instances.keys()])await unload(id);
|
||||
if(job.state!=='running')return;
|
||||
job.summary='Finding the fastest settings for your PC…';emit();
|
||||
@@ -767,7 +792,7 @@ return {summary:result.summary??'Deleted',removed:result.plan?.gb??null};}
|
||||
// Remove nonserializable process references from public jobs.
|
||||
const publicState=()=>{const value=state();value.jobs=value.jobs.map(({process,engineAbort,rawResult,...job})=>job);return value;};
|
||||
async function localPreview(path){if(!packaged)throw new Error('Checking local model files is part of the Strata app.');if(typeof path!=='string'||!path.trim()||[...path].some(c=>c.charCodeAt(0)<32))throw new Error('Choose a folder.');return control.importPlan(path.trim());}
|
||||
return {state:publicState,tuneStatus,tuneEffective,tuneOff,tunePresets:{list:()=>autotuner.list(),action:(kind,id,extra)=>autotuner.action(kind,id,extra),undoFor:async args=>autotuner.undoFor(tuneConfig(args).path)},refresh,localPreview,configure,setDownloadDirectory,planModelsFolder,startModelsFolder,interruptedMove,finishInterruptedMove,leaveInterruptedMove,plan,settings,save,settingsFile,openSettingsFile,operation,dismissRestart,cancel,unload,resolveInstance,poll,logs:()=>logs.slice(),benchmarkReport:()=>readJson(join(directory,'native-benchmark.json'),null),setDefault(id){defaultId=resolveInstance(id).id;emit();return publicState();},hasLive:()=>[...instances.values()].some(i=>i.status==='ready'),readyInstances:()=>[...instances.values()].filter(i=>i.status==='ready'),defaultInstanceId:()=>defaultId,close};
|
||||
return {state:publicState,settingsHints,tuneStatus,tuneEffective,tuneOff,tunePresets:{list:()=>autotuner.list(),action:(kind,id,extra)=>autotuner.action(kind,id,extra),undoFor:async args=>autotuner.undoFor(tuneConfig(args).path)},refresh,localPreview,configure,setDownloadDirectory,planModelsFolder,startModelsFolder,interruptedMove,finishInterruptedMove,leaveInterruptedMove,plan,settings,save,settingsFile,openSettingsFile,operation,dismissRestart,cancel,unload,resolveInstance,poll,logs:()=>logs.slice(),benchmarkReport:()=>readJson(join(directory,'native-benchmark.json'),null),setDefault(id){defaultId=resolveInstance(id).id;emit();return publicState();},hasLive:()=>[...instances.values()].some(i=>i.status==='ready'),readyInstances:()=>[...instances.values()].filter(i=>i.status==='ready'),defaultInstanceId:()=>defaultId,close};
|
||||
}
|
||||
/* A DeepSeek config on AMD written by setup 0.2.8 or earlier names ONE card and marks it "asked", though setup picked it itself (strata_control/gpu_pick.py): it must not stop the app's recommended cards from applying. A card setup named for itself is the first of the usable cards, largest memory first; another card number was the person's. Roles and a "gpus_by" marker are always honoured. */
|
||||
export function setupPickedOneCard(config,hardware){
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Preserve native protocol events and tool payloads; the gateway owns identity and cancellation. */
|
||||
import { once } from 'node:events';
|
||||
export async function nativeGateway({req,res,path,body,control,serving,fetcher=fetch}){
|
||||
let instance;try{instance=control.resolveInstance(body.model);}catch(e){res.writeHead(400,{'content-type':'application/json'});res.end(JSON.stringify({error:{message:e.message,type:'invalid_request_error',param:'model',code:null}}));return;}
|
||||
let instance;try{instance=control.resolveInstance(body.model);}catch(e){/* #5312: an Anthropic client sends a model name of its own (Claude Desktop: claude-5-5-sonnet) and cannot change it: with exactly one model loaded the route answers with it */if(path==='/v1/messages'&&body.model){try{instance=control.resolveInstance(undefined);}catch{}}if(!instance){res.writeHead(400,{'content-type':'application/json'});res.end(JSON.stringify({error:{message:e.message,type:'invalid_request_error',param:'model',code:null}}));return;}}
|
||||
const abort=new AbortController(),started=Date.now();let sawDone=false;/* #4690: a client that hangs up after `data: [DONE]` (the OpenAI SDK does) has the whole answer: that is not a cancel. */res.on('close',()=>{if(!res.writableEnded&&!sawDone)abort.abort();});
|
||||
const entry=serving.open({client:req.headers['user-agent']?.slice(0,100)??'API client',endpoint:path.slice(3),model:instance.apiModelId,instanceId:instance.id,gpuIds:instance.gpus.map(String),cancel:()=>abort.abort()});
|
||||
let tokensIn=null,tokensOut=null,tps=null,promptTps=null;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { validMcpName, MCP_NAME_HELP } from './mcp-names.mjs';
|
||||
import { SPEED_SWITCH_TABLE, DRAFTING_FAMILIES as GENERATED_DRAFTING_FAMILIES, DFLASH2_MODEL_PATTERN } from './policy-generated.mjs';
|
||||
/** Editable native keys. Unknown config keys and program paths stay intact. */
|
||||
/* The Drafting hint, word for word, with the size of the drafter this model would really fetch (the tuned one is 0.34 GB, 0.52 on an Arc card; the stock one 1.2). */
|
||||
export function draftingHint(gb=1.2){const size=Number.isFinite(Number(gb))&&Number(gb)>0?Number(gb):1.2;return `Which drafter guesses words ahead to make answers faster, one at a time. Auto (recommended) is DFlash2 for Qwen3.8-Flash-Next where the engine has it, the MTP otherwise; on two or more identical graphics cards that the all-cards split engine takes, it is that engine with the MTP draft layer. DFlash2 downloads a ${size} GB drafter once, the MTP its draft layer once. With Auto, several requests at once use the MTP. If you choose DFlash2 yourself, it runs with the number of requests you set. If DFlash2 cannot run, the MTP is used alone and Details says why. Off turns the drafter off.`;}
|
||||
export const NATIVE_FIELDS=[
|
||||
{id:'reasoning_budget_tokens',label:'Thinking token budget',group:'Responses',type:'number',min:0,max:1048576,hint:'Leave blank for the model default.'},
|
||||
{id:'fit_max_tokens',label:'Fit output to remaining context',group:'Responses',type:'boolean',hint:'Shortens the requested output cap when the prompt leaves less room.'},
|
||||
@@ -9,7 +12,7 @@ export const NATIVE_FIELDS=[
|
||||
{id:'idle_unload_s',label:'Unload after idle seconds',group:'Serving',type:'number',min:0,max:86400,hint:'Zero keeps the model loaded.'},
|
||||
{id:'lazy_load',label:'Load on first request',group:'Serving',type:'boolean'},
|
||||
{id:'mtp',notForModel:'deepseek',label:'Draft layer (MTP)',group:'Serving',type:'boolean',hint:'The draft layer guesses several words ahead, which makes answers faster, and costs about 1 GB of graphics memory and some RAM. Turn it off if the model stops while it starts, or the card is small: the expert cache gets that memory back and answers are slower. Default is on.'},
|
||||
{id:'drafting',notForModel:'deepseek',label:'Drafting',group:'Serving',type:'enum',choices:['auto','mtp','dflash2','off'],hint:'Which drafter guesses words ahead to make answers faster, one at a time. Auto (recommended) is DFlash2 for Qwen3.8-Flash-Next where the engine has it, the MTP otherwise; on two or more identical graphics cards that the all-cards split engine takes, it is that engine with the MTP draft layer. DFlash2 downloads a 1.2 GB drafter once, the MTP its draft layer once. With Auto, several requests at once use the MTP. If you choose DFlash2 yourself, it runs with the number of requests you set. If DFlash2 cannot run, the MTP is used alone and Details says why. Off turns the drafter off.'},
|
||||
{id:'drafting',notForModel:'deepseek',label:'Drafting',group:'Serving',type:'enum',choices:['auto','mtp','dflash2','off'],hint:draftingHint(1.2)},
|
||||
{id:'batchMtp',notForModel:'deepseek',label:'MTP per request',group:'Serving',type:'boolean',hint:'Each concurrent request uses the draft layer. Left alone, Strata turns it on for one graphics card with 24 GB or more and several requests at once, and off otherwise; choose on or off here to decide yourself.'},
|
||||
{id:'ds_experts',label:'Speed (DeepSeek)',group:'Serving',type:'enum',choices:['5','6'],forModel:'deepseek',hint:'Faster uses 5 of 6 experts per word: a small quality change, usually not noticeable. Full quality uses all 6. Left alone, the engine picks Faster on a PC whose graphics cards total 16 GB or less.'},
|
||||
{id:'parallel',label:'Concurrent requests',group:'Serving',type:'number',min:1,max:8,hint:'More slots need more memory and can slow each response.'},
|
||||
@@ -36,22 +39,8 @@ export const ENV_FLAG=/^(?:STRATA|DS4)_[A-Z0-9_]+$/;
|
||||
* turns it on, and the STRATA_ROUTE_PRIOR=0 that 0.2.7's Off wrote is the default state now. The other two are on by default. The names are the engine switch contract (phaseA-tests/notes-027-plan.md); strata_control/speed_switches.py says the
|
||||
* same for the Terminal (tests/terminal/test_speed_switches.py compares the two).
|
||||
*/
|
||||
export const SPEED_SWITCHES=[
|
||||
{id:'faster_long_prompts',label:'Faster long prompts',group:'Speed',
|
||||
hint:'Reads very long prompts faster by approximating part of the work. Off by default: answers about long documents can miss or mix up details.',
|
||||
default:'off',env:{STRATA_KVA:'1'}},
|
||||
{id:'faster_12gb_nvidia',label:'Faster answers on 12 GB NVIDIA cards',group:'Speed',
|
||||
hint:'Prefers the parts of the model already on the graphics card, which makes answers faster on 12 GB cards. Off by default: in long agent sessions it can repeat itself and give weaker results. Turn on if you mostly chat and want the speed.',
|
||||
default:'off',env:{STRATA_ROUTE_PRIOR:'1.5'}},
|
||||
{id:'faster_radeon_9000',label:'Faster answers on Radeon RX 9000 / AI PRO R9700',group:'Speed',
|
||||
hint:'Shares the work between the graphics card and the processor in a way that makes answers faster on these cards. Turn off to go back to the previous setting, which is slower.',
|
||||
default:'on',env:{STRATA_PCIE_FRAC_DEFAULT:'old'}},
|
||||
{id:'deepseek_speedups',label:'DeepSeek speed-ups',group:'Speed',
|
||||
hint:'Reads long prompts faster and loads the parts of the model it needs faster. Turn off if DeepSeek stops working or its answers look wrong; it then runs as in version 0.2.6.',
|
||||
default:'on',env:{DS4_DEFAULTS:'026',DS4_FAST_ATTN:'0',DS4_ATTN_SLAB:'0',DS4_COPY_PIN:'0',DS4_COPY_HUGETLB:'0',DS4_STORE_SPARE:'0',DS4_STORE_PUT:'0'},
|
||||
/* an earlier test build wrote only these six for the same state: they read as the switch being set, and go with it */
|
||||
legacy:{DS4_FAST_ATTN:'0',DS4_ATTN_SLAB:'0',DS4_COPY_PIN:'0',DS4_COPY_HUGETLB:'0',DS4_STORE_SPARE:'0',DS4_STORE_PUT:'0'}},
|
||||
];
|
||||
/* The table (ids, words, defaults, environment entries) is generated from strata_control/speed_switches.py into policy-generated.mjs; this file only adds the group the screen lists them under and the rules for where a switch applies (below), which tests/speed-switches.test.mjs and tests/terminal/test_speed_switches.py run against the same cases. */
|
||||
export const SPEED_SWITCHES=SPEED_SWITCH_TABLE.map(s=>({id:s.id,label:s.label,group:'Speed',hint:s.hint,default:s.default,env:s.env,...(s.legacy?{legacy:s.legacy}:{})}));
|
||||
const GFX12=/^gfx120[01]$/i, RADEON_9000=/\b(?:RX\s*9\d{3}|AI\s*PRO\s*R97\d{2}|R97\d{2})\b/i;
|
||||
/**
|
||||
* Which switches make sense for this model on this PC: only those are shown. `backend` is the engine's (cuda, hip, sycl, metal, cpu). `arch` is the "arch"
|
||||
@@ -88,8 +77,8 @@ export function speedSwitchRows(env,context){return SPEED_SWITCHES.filter(s=>spe
|
||||
/** Settings the app itself applies when it starts a model (they are not read from the engine's source, so the engine's text never says it supports them). */
|
||||
export const APP_OWNED_FIELDS=['mtp','batchMtp','drafting','ds_experts'];
|
||||
/** The families that have a Drafting choice and the model names the DFlash2 drafter was made for (strata_control/drafting.py SUPPORTED_FAMILIES, _SUPPORTED_NAME): Qwen3.8-Flash-Next, Gyro and Unsloth's quantizations of the same weights; not Swift, Coder or NVFP4. */
|
||||
export const DRAFTING_FAMILIES=['qwen','gyro','unsloth'];
|
||||
const DFLASH2_MODEL=/^qwen3\.8-flash-next(-gyro-[a-z0-9._-]+|-unsloth-[a-z0-9._-]+|-(q|iq|tq)\d[a-z0-9_.]*)?$/i;
|
||||
export const DRAFTING_FAMILIES=GENERATED_DRAFTING_FAMILIES;
|
||||
const DFLASH2_MODEL=new RegExp(DFLASH2_MODEL_PATTERN,'i');
|
||||
/** The drafter a choice means (one per run; strata_control/drafting.py choice_of): Auto is DFlash2 where the engine has it (`served`, Control's word) and the model is one it was made for, else the MTP; off and mtp are what they say. `model` is a run config's model name or a family. */
|
||||
/** `split` (Control's plan: the all-cards split engine will take this start) makes Auto the MTP, the draft layer that engine runs; a DFlash2 the person chose by name stays. */
|
||||
export function resolveDrafter(choice,model,served,split){
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// GENERATED by `python -m strata_control.app_policy` from strata_control/speed_switches.py and strata_control/drafting.py. Do not edit it by hand:
|
||||
// change the Python and regenerate; tests/terminal/test_app_policy.py and apps/host/tests/policy-generated.test.mjs fail when this file is out of date.
|
||||
/** The speed switches of the app's Expert settings (strata_control/speed_switches.py app_switches()): id, words, default, the environment entries outside the default. */
|
||||
export const SPEED_SWITCH_TABLE = [
|
||||
{"id": "faster_long_prompts", "label": "Faster long prompts", "hint": "Reads very long prompts faster by approximating part of the work. Off by default: answers about long documents can miss or mix up details.", "default": "off", "env": {"STRATA_KVA": "1"}},
|
||||
{"id": "faster_12gb_nvidia", "label": "Faster answers on 12 GB NVIDIA cards", "hint": "Prefers the parts of the model already on the graphics card, which makes answers faster on 12 GB cards. Off by default: in long agent sessions it can repeat itself and give weaker results. Turn on if you mostly chat and want the speed.", "default": "off", "env": {"STRATA_ROUTE_PRIOR": "1.5"}},
|
||||
{"id": "faster_radeon_9000", "label": "Faster answers on Radeon RX 9000 / AI PRO R9700", "hint": "Shares the work between the graphics card and the processor in a way that makes answers faster on these cards. Turn off to go back to the previous setting, which is slower.", "default": "on", "env": {"STRATA_PCIE_FRAC_DEFAULT": "old"}},
|
||||
{"id": "deepseek_speedups", "label": "DeepSeek speed-ups", "hint": "Reads long prompts faster and loads the parts of the model it needs faster. Turn off if DeepSeek stops working or its answers look wrong; it then runs as in version 0.2.6.", "default": "on", "env": {"DS4_DEFAULTS": "026", "DS4_FAST_ATTN": "0", "DS4_ATTN_SLAB": "0", "DS4_COPY_PIN": "0", "DS4_COPY_HUGETLB": "0", "DS4_STORE_SPARE": "0", "DS4_STORE_PUT": "0"}, "legacy": {"DS4_FAST_ATTN": "0", "DS4_ATTN_SLAB": "0", "DS4_COPY_PIN": "0", "DS4_COPY_HUGETLB": "0", "DS4_STORE_SPARE": "0", "DS4_STORE_PUT": "0"}},
|
||||
];
|
||||
/** The families that have a Drafting choice (strata_control/drafting.py SUPPORTED_FAMILIES). */
|
||||
export const DRAFTING_FAMILIES = ["qwen", "gyro", "unsloth"];
|
||||
/** The model names the DFlash2 drafter was made for (strata_control/drafting.py _SUPPORTED_NAME), a case-insensitive pattern. */
|
||||
export const DFLASH2_MODEL_PATTERN = "^qwen3\\.8-flash-next(-gyro-[a-z0-9._-]+|-unsloth-[a-z0-9._-]+|-(q|iq|tq)\\d[a-z0-9_.]*)?$";
|
||||
@@ -28,7 +28,9 @@ export function endTree(child, signal = 'SIGTERM') {
|
||||
if (child.pid) {
|
||||
try { process.kill(-child.pid, signal); return; } catch { /* no such group: it did not lead one, or it is gone */ }
|
||||
}
|
||||
child.kill(signal);
|
||||
// A child that never started has no pid. On macOS, kill() on it right after a failed spawn signals the caller's own process group (pid 0), so
|
||||
// it is left alone: there is nothing to end.
|
||||
if (child.pid) child.kill(signal);
|
||||
} catch { /* already gone */ }
|
||||
}
|
||||
|
||||
|
||||
+21
-13
@@ -1,7 +1,7 @@
|
||||
import { tr } from '../lib/i18n';
|
||||
/** App shell: routes, setup takeover, palette, toasts. */
|
||||
import { useNative } from '../lib/native';
|
||||
import { Component, useEffect, useRef, useState, type ReactNode } from 'react';
|
||||
import { Component, Suspense, useDeferredValue, useEffect, useRef, useState, type ReactNode } from 'react';
|
||||
import { useSurfaceFocus } from '../lib/focus';
|
||||
import { closeSettings, useLocation } from '../lib/router';
|
||||
import { bootOnce, bootstrap, setNavOpen, setFocus, useStore, } from '../lib/store';
|
||||
@@ -13,14 +13,16 @@ import { MOD, handleAppShortcut, runCommand } from '../lib/commands';
|
||||
import { useMedia } from '../lib/responsive';
|
||||
import { Btn, Dialog, IconBtn, Toasts } from '../components/ui';
|
||||
import { Brand } from '../components/Brand';
|
||||
import { SetupScreen } from '../screens/Setup';
|
||||
import { ChatScreen } from '../screens/Chat';
|
||||
import { ModelRunScreen } from '../screens/ModelRun';
|
||||
import { ServingScreen } from '../screens/Serving';
|
||||
import { MonitorScreen } from '../screens/Monitor';
|
||||
import { TasksScreen } from '../screens/Tasks';
|
||||
import { SkillsScreen } from '../screens/Skills';
|
||||
import { SettingsScreen } from '../screens/Settings';
|
||||
import { lazyScreen, useLoadedModule } from '../lib/lazy-screen';
|
||||
// The start screen is Chat. Every other screen loads the first time it is opened (7.1), so the first start reads and runs less code.
|
||||
const loadSetup = () => import('../screens/Setup');
|
||||
const loadSettings = () => import('../screens/Settings');
|
||||
const ModelRunScreen = lazyScreen(() => import('../screens/ModelRun'), (m) => m.ModelRunScreen);
|
||||
const ServingScreen = lazyScreen(() => import('../screens/Serving'), (m) => m.ServingScreen);
|
||||
const MonitorScreen = lazyScreen(() => import('../screens/Monitor'), (m) => m.MonitorScreen);
|
||||
const TasksScreen = lazyScreen(() => import('../screens/Tasks'), (m) => m.TasksScreen);
|
||||
const SkillsScreen = lazyScreen(() => import('../screens/Skills'), (m) => m.SkillsScreen);
|
||||
function AppShell() {
|
||||
useStore(s => s.language);
|
||||
const location = useLocation();
|
||||
@@ -49,7 +51,13 @@ function AppShell() {
|
||||
// demanding five steps before anything appears.
|
||||
const setupOpen = location.route === 'setup' || (!!setup && !setup.complete && !setup.dismissed) || firstRunOpen;
|
||||
// Hook runs here, after setupOpen exists — see src/lib/focus.ts.
|
||||
useSurfaceFocus(setupOpen, setupRef);
|
||||
// The dialogs open once their code has loaded, so they appear and take focus exactly as before.
|
||||
const setupCode = useLoadedModule(loadSetup, setupOpen);
|
||||
const setupShown = setupOpen && !!setupCode;
|
||||
useSurfaceFocus(setupShown, setupRef);
|
||||
const settingsWanted = location.query.get('settings') === '1';
|
||||
const settingsCode = useLoadedModule(loadSettings, settingsWanted);
|
||||
const routeShown = useDeferredValue(location.route);
|
||||
return (<div className="app" data-rail={compact ? !navOpen : rail} data-layout={mobile ? 'mobile' : compact ? 'compact' : 'desktop'} data-nav-open={navOpen || undefined} data-focus={focus && location.route === 'chat' || undefined}>
|
||||
<Sidebar containerRef={navRef}/>
|
||||
{tr(navOpen && compact ? <button className="nav-scrim" tabIndex={-1} aria-hidden="true" aria-label={tr("Close navigation")} onClick={() => setNavOpen(false)}/> : null)}
|
||||
@@ -62,16 +70,16 @@ function AppShell() {
|
||||
</div>
|
||||
<ConnectionBar />
|
||||
{tr(focus && location.route === 'chat' ? <FocusBar /> : null)}
|
||||
{tr(!ready ? <BootSkeleton /> : <RouteView route={location.route}/>)}
|
||||
{tr(!ready ? <BootSkeleton /> : <Suspense fallback={<BootSkeleton />}><RouteView route={routeShown}/></Suspense>)}
|
||||
</main>
|
||||
<CommandPalette open={paletteOpen}/>
|
||||
<Dialog open={location.query.get('settings') === '1'} title={tr("Settings")} className="settings-dialog" onClose={closeSettings}><SettingsScreen /></Dialog>
|
||||
<Dialog open={settingsWanted && !!settingsCode} title={tr("Settings")} className="settings-dialog" onClose={closeSettings}>{settingsCode ? <settingsCode.SettingsScreen /> : null}</Dialog>
|
||||
<ShortcutsDialog />
|
||||
<FolderPathDialog />
|
||||
{tr(setupOpen ? (<div className="setupmodal" role="dialog" aria-modal="true" aria-label={tr("Set up Strata")}>
|
||||
{tr(setupShown ? (<div className="setupmodal" role="dialog" aria-modal="true" aria-label={tr("Set up Strata")}>
|
||||
<div className="scrim" data-open="true" aria-hidden="true"/>
|
||||
<div className="setupmodal__surface" ref={setupRef}>
|
||||
<SetupScreen />
|
||||
{setupCode ? <setupCode.SetupScreen /> : null}
|
||||
</div>
|
||||
</div>) : null)}
|
||||
<Toasts />
|
||||
|
||||
@@ -7,7 +7,7 @@ Original SVGs supplied by Niko in `modelandweights.zip`, 10 October 2026.
|
||||
|
||||
The originals retain their metadata and vector geometry. The app sprite strips
|
||||
metadata and uses their 24 x 24 viewBox, currentColor, 1.5px strokes, and 12% fills.
|
||||
`scripts/sync-design-kit.mjs` includes these files when rebuilding the kit sprite.
|
||||
They are already in the checked-in sprite, `public/icons/sprite.svg`, which is the source: a new icon is added there as a `<symbol id="i-NAME">`.
|
||||
|
||||
Also supplied by Niko on 10 October 2026:
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Icon } from './ui';
|
||||
*/
|
||||
export function FamilySwitch({ families, selected, onPick }: { families: CatalogModel[]; selected?: string; onPick: (family: CatalogModel) => void }) {
|
||||
const shown = families.filter(f => f.sizes.length);
|
||||
const missing = families.filter(f => !f.sizes.length && f.available === false); // a model of the update feed this engine does not run: said, never offered
|
||||
const trigger = useRef<HTMLButtonElement>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
if (!shown.length) return null;
|
||||
@@ -19,12 +20,20 @@ export function FamilySwitch({ families, selected, onPick }: { families: Catalog
|
||||
<Icon name="models" size={20}/><strong>{tr(current.title)}</strong><Icon name="chevron" size={16} className="family-trigger__chev"/>
|
||||
</button>
|
||||
{tr(open ? <FloatingMenu anchor={trigger.current} title={tr("Model family")} width={320} onClose={() => setOpen(false)}>
|
||||
<div className="family-menu">
|
||||
{shown.map(f => <button key={f.family} type="button" role="menuitemradio" aria-checked={f.family === current.family} className="family-menu__item" onClick={() => { setOpen(false); if (f.family !== current.family) onPick(f); }}>
|
||||
<Icon name="models" size={18}/><span><strong>{tr(f.title)}</strong>{f.experimental ? <small>{tr("Experimental")}</small> : null}</span>
|
||||
{f.family === current.family ? <Icon name="check" size={16} className="family-menu__check"/> : null}
|
||||
</button>)}
|
||||
</div>
|
||||
<FamilyMenuList shown={shown} missing={missing} current={current} onPick={f => { setOpen(false); if (f.family !== current.family) onPick(f); }}/>
|
||||
</FloatingMenu> : null)}
|
||||
</>;
|
||||
}
|
||||
|
||||
/** The rows of the family menu: the families to pick, then (as notes, not choices) the models of the update feed this engine cannot use, each with the plain reason. */
|
||||
export function FamilyMenuList({ shown, missing, current, onPick }: { shown: CatalogModel[]; missing: CatalogModel[]; current: CatalogModel; onPick: (family: CatalogModel) => void }) {
|
||||
return (
|
||||
<div className="family-menu">
|
||||
{shown.map(f => <button key={f.family} type="button" role="menuitemradio" aria-checked={f.family === current.family} className="family-menu__item" onClick={() => onPick(f)}>
|
||||
<Icon name="models" size={18}/><span><strong>{tr(f.title)}</strong>{f.experimental ? <small>{tr("Experimental")}</small> : null}{f.available === false && f.unavailable_reason ? <small>{f.unavailable_reason}</small> : null}</span>
|
||||
{f.family === current.family ? <Icon name="check" size={16} className="family-menu__check"/> : null}
|
||||
</button>)}
|
||||
{missing.map(f => <div key={f.family} className="family-menu__item" role="note" aria-disabled="true"><Icon name="models" size={18}/><span><strong>{f.title}</strong><small>{f.unavailable_reason}</small></span></div>)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -129,7 +129,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
const cardsGpus: number[] = roleCards.length > 1 ? roleOrder(effectiveRoles, roleCards) : [];
|
||||
const chosenCards = cardsGpus;
|
||||
const fitHardware = { ram_gb: pcRam ? Math.max(0, pcRam - keptRam) : pcRam, vram_gb: Math.max(0, (roleCards.length > 1 ? vramOn(effectiveRoles, roleCards) : pcVram) - keptVramOn(chosenCards)) };
|
||||
// How the chosen version sits on this computer (the same rule as the version list): the page says a word only for "SSD streaming" and "Doesn't fit".
|
||||
// How the chosen version sits on this computer (the same rule as the version list): the page says a word only for "may run slower".
|
||||
const fitState = selectedSize ? versionFit(selectedSize, fitHardware, { context, kv }) : 'unknown';
|
||||
// A layer split written for another number of cards no longer fits: back to automatic.
|
||||
useEffect(() => { if (cardsGpus.length <= 1 && split !== 'auto') setSplit('auto'); }, [cardsGpus.length]);
|
||||
@@ -251,7 +251,7 @@ export function NativeModels({ onOtherModels, onReady, onImport, onConnectEngine
|
||||
<div className="native-choice__head"><span className="native-choice__label">{tr("Version")}</span><span className="native-choice__tools"><NativePresets values={values} onApply={v => { const f = families.find(f => f.family === v.family || f.sizes.some(s => s.sourceFamily === v.family)); if (!f)
|
||||
return; presetPending.current = true; setFamily(f.family); setSize(f.sizes.find(s => s.model === v.model && (s.sourceFamily === v.family || f.family === v.family))?.id ?? ''); setContext(v.context ?? 32768); setVision(v.vision ?? 'no'); setKv(v.kv ?? 'int8'); setLowRam(v.low_ram ?? 'auto'); }}/><Btn icon="models" size="sm" variant="ghost" onClick={() => setManage(true)}>{tr("Manage models")}</Btn></span></div>
|
||||
<div className="native-version"><VersionPicker pick={native?.catalog?.recommendation} versions={selectedFamily?.sizes ?? []} details={expert} selectedId={selectedSize.id} hardware={fitHardware} cache={{ context, kv, drafter: fitDrafter }} downloaded={readyIds} onSelect={id => { const f = families.find(f => f.sizes.some(s => s.id === id)); if (f) { setFamily(f.family); setSize(id); setError(''); } }} onOwnGguf={() => (onImport ?? onOtherModels)()}/></div>
|
||||
{tr(fitState === 'ssd' ? <p className="small muted native-spill-note"><Icon name="disk" size={16}/>{tr("SSD streaming · will run slower")}</p> : fitState === 'no' ? <p className="small muted native-spill-note"><Icon name="warning" size={16}/>{tr("Doesn't fit")}</p> : null)}
|
||||
{tr(fitState === 'ssd' ? <p className="small muted native-spill-note"><Icon name="disk" size={16}/>{tr("may run slower")}</p> : null)}
|
||||
{tr(selectedSize?.multi_file && deepseekContextNote(context) ? <p className="small muted native-spill-note"><Icon name="info" size={16}/>{tr(deepseekContextNote(context))}</p> : null)}
|
||||
<div className="native-choice__row">
|
||||
<div className="native-choice__col"><span className="native-choice__label">{tr("Conversation context")}</span><Select ariaLabel={tr("Conversation context")} value={String(context)} options={contextChoices(contextList, context).map(n => ({ value: String(n), label: n % 1024 === 0 ? `${n / 1024}K` : `${n.toLocaleString()} tokens` }))} onChange={n => { setContextPicked(true); setContext(Number(n)); }}/></div>
|
||||
|
||||
@@ -28,6 +28,7 @@ export function NativeSettings({ model }: {
|
||||
}) {
|
||||
const native = useNative();
|
||||
const [revision,setRevision]=useState(0);
|
||||
const [hints, setHints] = useState<Record<string, string>>({}); // a field's hint worded for this model (the Drafting size)
|
||||
const [switches, setSwitches] = useState<SpeedSwitch[]>([]); // the speed switches that apply to this model on this PC
|
||||
const [dsExperts, setDsExperts] = useState(false); // the installed DeepSeek engine knows --experts (0.1.50 or newer)
|
||||
const [tuned, setTuned] = useState<{ preset: { name: string } | null; changed: TunedChange[] } | null>(null);
|
||||
@@ -44,6 +45,7 @@ export function NativeSettings({ model }: {
|
||||
values: Record<string, any>;
|
||||
mcp: Record<string, any>;
|
||||
tuned?: { preset: { name: string } | null; changed: TunedChange[] } | null;
|
||||
hints?: Record<string, string>;
|
||||
capabilities?: { dsExperts?: boolean };
|
||||
speedSwitches?: SpeedSwitch[];
|
||||
}>(`/api/native/models/${model}/settings`).then(input => {
|
||||
@@ -52,6 +54,7 @@ export function NativeSettings({ model }: {
|
||||
setDsExperts(!!input.capabilities?.dsExperts);
|
||||
setSwitches(input.speedSwitches ?? []);
|
||||
setTuned(input.tuned ?? null);
|
||||
setHints(input.hints ?? {});
|
||||
const value = nativeSettingsResponse(input);
|
||||
setValues(value.values);
|
||||
setText(JSON.stringify(value.values, null, 2));
|
||||
@@ -120,7 +123,7 @@ export function NativeSettings({ model }: {
|
||||
const rows = (native?.fields ?? []).filter(f => (!f.forModel || (model.toLowerCase().includes(f.forModel) && (f.id !== 'ds_experts' || dsExperts))) && (!f.notForModel || !model.toLowerCase().includes(f.notForModel)) && (f.id !== 'drafting' || draftingOffered) && `${tr(f.label)} ${f.id} ${tr(f.group)}`.toLowerCase().includes(query.toLowerCase()));
|
||||
const switchRows = switches.filter(sw => `${tr(sw.label)} ${sw.id} ${tr('Speed')}`.toLowerCase().includes(query.toLowerCase()));
|
||||
return <details className="native-settings"><summary>{tr("Native engine settings")}</summary>{tr(tuned?.changed?.length ? <div className="native-tuned"><p className="small"><strong>{tr("Your tuning '{0}' sets", [tuned.preset?.name ?? ''])}</strong></p><ul>{tuned.changed.map(c => { const d = describeChange(c); return <li key={c.flag}><span>{d.name}</span><b>{d.now || tr('off')}</b>{d.was !== null ? <small className="muted">{tr("was {0}", [d.was])}</small> : null}</li>; })}</ul></div> : null)}<p className="small muted">{tr(!original ? 'Reading saved settings… ' : '')}{tr("Settings for this model. Blank values use Strata\u2019s defaults.")}</p><div className="editor-tabs" role="tablist" aria-label={tr("Native settings editor")}>{tr(['fields', 'json', 'tools'].map(t => <button key={t} role="tab" aria-selected={tab === t} onClick={() => chooseTab(t)}>{tr(t === 'fields' ? 'Parameters' : t === 'json' ? 'JSON' : 'MCP tools')}</button>))}</div>
|
||||
{tr(tab === 'fields' ? <><input className="st-input" type="search" aria-label={tr("Search native parameters")} placeholder={tr("Search parameters…")} value={query} onChange={e => setQuery(e.target.value)}/>{tr(['Responses', 'GPU & memory', 'Serving', 'Image reading', 'Tools', 'Recovery'].map(group => rows.some(f => f.group === group) ? <section className="native-settings-group" key={group}><h3 className="h4">{tr(group)}</h3>{tr(rows.filter(f => f.group === group).map(f => <label className="native-setting-row" key={f.id}><span><strong>{tr(f.label)}</strong>{tr(f.supported === false ? <small>{tr("Requires a newer Strata installation.")}</small> : f.hint ? <small>{tr(f.hint)}</small> : null)}</span><NativeFieldInput field={f} model={model} value={values[f.id]} onChange={v => change(f.id, v)}/></label>))}</section> : null))}{tr(switchRows.length ? <section className="native-settings-group"><h3 className="h4">{tr("Speed")}</h3>{tr(switchRows.map(sw => <label className="native-setting-row" key={sw.id}><span><strong>{tr(sw.label)}</strong><small>{tr(sw.hint)}</small></span><Select ariaLabel={tr(sw.label)} value={switchIsOn(sw, values.env) ? 'on' : 'off'} options={sw.default === 'on' ? [{ value: 'on', label: 'On (recommended)' }, { value: 'off', label: 'Off' }] : [{ value: 'off', label: 'Off (recommended)' }, { value: 'on', label: 'On' }]} onChange={v => changeSwitch(sw, v === 'on')}/></label>))}</section> : null)}<section className="native-settings-group"><h3 className="h4">{tr("Sampling")}</h3>{tr([['temperature', 'Temperature'], ['top_p', 'Top-p'], ['top_k', 'Top-k'], ['min_p', 'Min-p'], ['repetition_penalty', 'Repetition penalty'], ['presence_penalty', 'Presence penalty'], ['frequency_penalty', 'Frequency penalty'], ['penalty_last_n', 'Penalty window'], ['seed', 'Seed']].map(([id, label]) => <label className="native-setting-row" key={id}><span><strong>{tr(label)}</strong></span><DecimalInput ariaLabel={tr("Native {0}", [label])} value={values.sampling?.[id] ?? null} onChange={n => { const sampling = { ...values.sampling }; if (n === null) delete sampling[id]; else sampling[id] = n; change('sampling', sampling); }}/></label>))}</section></> : tab === 'json' ? <><p className="small muted">{tr("Engine arguments and STRATA environment flags are editable here. Program paths and network access remain managed by the host.")}</p><textarea className="st-input native-json" aria-label={tr("Native configuration JSON")} spellCheck={false} value={text} onChange={e => { setText(e.target.value); setNotice(''); }}/></> : <><p className="small muted">{tr("Add local programs or HTTP MCP servers. Connected tools can read or change the resources you give them access to. Enable tools in chat when you want the model to use them.")}</p><NativeMcpEditor text={mcp} onChange={value => { setMcp(value); setNotice(''); }}/><details><summary className="small">{tr("Configuration example")}</summary><pre className="native-example">{'{\n "files": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-filesystem", "/your/folder"],\n "disabled": false\n }\n}'}</pre><p className="xs muted">{tr("Saved credentials are masked. Keep the mask to preserve an existing value; type a new value to replace it.")}</p></details></>)}
|
||||
{tr(tab === 'fields' ? <><input className="st-input" type="search" aria-label={tr("Search native parameters")} placeholder={tr("Search parameters…")} value={query} onChange={e => setQuery(e.target.value)}/>{tr(['Responses', 'GPU & memory', 'Serving', 'Image reading', 'Tools', 'Recovery'].map(group => rows.some(f => f.group === group) ? <section className="native-settings-group" key={group}><h3 className="h4">{tr(group)}</h3>{tr(rows.filter(f => f.group === group).map(f => <label className="native-setting-row" key={f.id}><span><strong>{tr(f.label)}</strong>{tr(f.supported === false ? <small>{tr("Requires a newer Strata installation.")}</small> : f.hint ? <small>{tr(hints[f.id] ?? f.hint)}</small> : null)}</span><NativeFieldInput field={f} model={model} value={values[f.id]} onChange={v => change(f.id, v)}/></label>))}</section> : null))}{tr(switchRows.length ? <section className="native-settings-group"><h3 className="h4">{tr("Speed")}</h3>{tr(switchRows.map(sw => <label className="native-setting-row" key={sw.id}><span><strong>{tr(sw.label)}</strong><small>{tr(sw.hint)}</small></span><Select ariaLabel={tr(sw.label)} value={switchIsOn(sw, values.env) ? 'on' : 'off'} options={sw.default === 'on' ? [{ value: 'on', label: 'On (recommended)' }, { value: 'off', label: 'Off' }] : [{ value: 'off', label: 'Off (recommended)' }, { value: 'on', label: 'On' }]} onChange={v => changeSwitch(sw, v === 'on')}/></label>))}</section> : null)}<section className="native-settings-group"><h3 className="h4">{tr("Sampling")}</h3>{tr([['temperature', 'Temperature'], ['top_p', 'Top-p'], ['top_k', 'Top-k'], ['min_p', 'Min-p'], ['repetition_penalty', 'Repetition penalty'], ['presence_penalty', 'Presence penalty'], ['frequency_penalty', 'Frequency penalty'], ['penalty_last_n', 'Penalty window'], ['seed', 'Seed']].map(([id, label]) => <label className="native-setting-row" key={id}><span><strong>{tr(label)}</strong></span><DecimalInput ariaLabel={tr("Native {0}", [label])} value={values.sampling?.[id] ?? null} onChange={n => { const sampling = { ...values.sampling }; if (n === null) delete sampling[id]; else sampling[id] = n; change('sampling', sampling); }}/></label>))}</section></> : tab === 'json' ? <><p className="small muted">{tr("Engine arguments and STRATA environment flags are editable here. Program paths and network access remain managed by the host.")}</p><textarea className="st-input native-json" aria-label={tr("Native configuration JSON")} spellCheck={false} value={text} onChange={e => { setText(e.target.value); setNotice(''); }}/></> : <><p className="small muted">{tr("Add local programs or HTTP MCP servers. Connected tools can read or change the resources you give them access to. Enable tools in chat when you want the model to use them.")}</p><NativeMcpEditor text={mcp} onChange={value => { setMcp(value); setNotice(''); }}/><details><summary className="small">{tr("Configuration example")}</summary><pre className="native-example">{'{\n "files": {\n "command": "npx",\n "args": ["-y", "@modelcontextprotocol/server-filesystem", "/your/folder"],\n "disabled": false\n }\n}'}</pre><p className="xs muted">{tr("Saved credentials are masked. Keep the mask to preserve an existing value; type a new value to replace it.")}</p></details></>)}
|
||||
<div className="native-settings-actions"><Btn disabled={pending || !original || !dirty} variant="primary" onClick={() => void save()}>{tr(pending ? 'Saving…' : 'Save settings')}</Btn><Btn disabled={pending} onClick={() => { const saved = JSON.parse(original || '{"values":{},"mcp":{}}'); setValues(saved.values); setText(JSON.stringify(saved.values, null, 2)); setMcp(JSON.stringify(saved.mcp, null, 2)); setError(''); }}>{tr("Restore saved")}</Btn></div>{tr(notice ? <p role="status" className="small">{tr(notice)}</p> : null)}{tr(error ? <ErrorNotice message={error} onRetry={() => setRevision(value => value + 1)}/> : null)}
|
||||
</details>;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ function FreeMeter({ label, total, free }: { label: string; total: number; free:
|
||||
* "This computer": how much graphics memory and RAM is free right now. While a model is loaded that is all the card shows: the hardware line, the model's
|
||||
* name, "Loaded", what it takes and the SSD line are gone (owner, 11-01: the page already says what is loaded); only while the loaded model really reads
|
||||
* from the SSD, how fast it reads right now. Before anything is loaded the card says what the chosen version would take. That a version does not fit in
|
||||
* memory is said once, under the version picker ("SSD streaming · will run slower"), not again here. (The n-gram table, which always sits on the SSD, is not counted.)
|
||||
* memory is said once, under the version picker ("may run slower"), not again here. (The n-gram table, which always sits on the SSD, is not counted.)
|
||||
*/
|
||||
export function PcFitCard({ gpuName, ramGb, vramGb, fit, inUse = null, diskGb = null, live = null, loaded = [], selectedLoaded = false }: { gpuName?: string | null; ramGb: number | null; vramGb: number; fit: Fit | null; inUse?: { ramGb: number; vramGb: number } | null; diskGb?: number | null; live?: PcLive | null; loaded?: PcLoaded[]; selectedLoaded?: boolean }) {
|
||||
const showFit = !!fit?.known && !selectedLoaded;
|
||||
|
||||
@@ -88,7 +88,7 @@ export function SettingsUpdates() {
|
||||
</>;
|
||||
}
|
||||
|
||||
/** "Strata 0.2.11 · Engine 0.1.57": the app's own version first, the engine's beside it (About). */
|
||||
/** "Strata 0.2.13 · Engine 0.1.59": the app's own version first, the engine's beside it (About). */
|
||||
export function AboutVersions() {
|
||||
const native = useNative();
|
||||
const app = appVersionLabel(useStore(s => s.setup?.app.version), native?.version);
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
import { useMemo } from 'react';
|
||||
import { codeLanguage, highlightedLines } from '../lib/syntax';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { codeLanguage } from '../lib/code-language';
|
||||
import type { CodeToken } from '../lib/syntax';
|
||||
/** The Prism grammars are a separate file (src/lib/syntax.ts), fetched after the first screen is up (App.tsx asks for it a few seconds in) or by the first code block, whichever comes first. Until it is here a block shows its text uncoloured. */
|
||||
type Highlighter = typeof import('../lib/syntax');
|
||||
let loaded: Highlighter | null = null;
|
||||
let loading: Promise<Highlighter | null> | null = null;
|
||||
export function loadHighlighter(): Promise<Highlighter | null> {
|
||||
loading ??= import('../lib/syntax').then(module => (loaded = module), () => { loading = null; return null; });
|
||||
return loading;
|
||||
}
|
||||
const plainLines = (text: string): CodeToken[][] => text.split('\n').map(part => (part ? [{ text: part, classes: [] }] : []));
|
||||
export function SyntaxCode({text,language='',filename,lineNumbers=false,className=''}:{text:string;language?:string;filename?:string;lineNumbers?:boolean;className?:string}){
|
||||
const resolved=filename?codeLanguage(filename,true):codeLanguage(language);
|
||||
const lines=useMemo(()=>highlightedLines(text,resolved),[text,resolved]);
|
||||
const [highlighter,setHighlighter]=useState<Highlighter|null>(loaded);
|
||||
useEffect(()=>{if(highlighter)return;let live=true;void loadHighlighter().then(module=>{if(live&&module)setHighlighter(module);});return()=>{live=false;};},[highlighter]);
|
||||
const lines=useMemo(()=>highlighter?highlighter.highlightedLines(text,resolved):plainLines(text),[highlighter,text,resolved]);
|
||||
return <pre className={`syntax-code ${lineNumbers?'code-preview':''} ${className}`} data-language={resolved}><code>{lines.map((line,i)=><span className="syntax-line" key={i}>{lineNumbers?<span className="syntax-gutter" aria-hidden="true">{i+1}</span>:null}<span className="syntax-text">{line.map((part,j)=>part.classes.length?<span className={part.classes.map(c=>`syntax-${c}`).join(' ')} key={j}>{part.text}</span>:part.text)}{!lineNumbers&&i<lines.length-1?'\n':''}</span></span>)}</code></pre>;
|
||||
}
|
||||
@@ -26,9 +26,9 @@ const whole = (n: number) => `${Math.round(n)} GB`;
|
||||
|
||||
/**
|
||||
* Where the versions of the chosen base model are listed, each exactly once. At the top: the versions we recommend (the pick for this PC first; no "Recommended" tag since 11-02, the order says it) and the versions already downloaded (tagged "Downloaded"). Below, folded under "More
|
||||
* versions": all the others. A row is the name, small tags, a word only where one is needed ("SSD streaming · will run slower" for a version that does
|
||||
* not fully fit in memory, "Doesn't fit" where it cannot run at all; a version that fits says nothing: see `versionFit`) and the size. The maker, the
|
||||
* memory it needs and the SSD figure are behind "Show details" (open from the start in Expert mode). The other base models are not in this list: they
|
||||
* versions": all the others. A row is the name, small tags, a word only where one is needed ("may run slower" for a version that does
|
||||
* not fit in memory; a version that fits says nothing: see `versionFit`) and the size. The maker and the
|
||||
* memory it needs are behind "Show details" (open from the start in Expert mode). The other base models are not in this list: they
|
||||
* appear when picked at the top of the page.
|
||||
*/
|
||||
export function VersionPicker({ versions, selectedId, hardware, onSelect, onOwnGguf, downloaded, showRecommended = true, moreLabel, pick, cache, details: detailsOpen = false }: {
|
||||
@@ -67,21 +67,20 @@ export function VersionPicker({ versions, selectedId, hardware, onSelect, onOwnG
|
||||
const foldedOpen = moreOpen || !top.length || more.some(v => v.id === selectedId);
|
||||
|
||||
const need = (v: CatalogVersion, technical: boolean) => {
|
||||
const fit = fitOn(v, hardware, true, cache);
|
||||
if (technical) return v.multi_file && v.disk_gb ? tr("{0} RAM + VRAM, plus about {1} GB of disk", [gb(fit.memoryGb), Math.round(v.disk_gb)]) : tr("{0} RAM + VRAM", [gb(fit.memoryGb)]);
|
||||
return v.multi_file && v.disk_gb ? tr("Needs {0} of memory and about {1} GB of disk", [whole(fit.memoryGb), Math.round(v.disk_gb)]) : tr("Needs {0} of memory", [whole(fit.memoryGb)]);
|
||||
const size = typeof v.fit_gb === 'number' && Number.isFinite(v.fit_gb) ? v.fit_gb : fitOn(v, hardware, true, cache).memoryGb; // the files without the lookup table: the figure the label uses
|
||||
if (technical) return v.multi_file && v.disk_gb ? tr("{0} RAM + VRAM, plus about {1} GB of disk", [gb(size), Math.round(v.disk_gb)]) : tr("{0} RAM + VRAM", [gb(size)]);
|
||||
return v.multi_file && v.disk_gb ? tr("Needs {0} of memory and about {1} GB of disk", [whole(size), Math.round(v.disk_gb)]) : tr("Needs {0} of memory", [whole(size)]);
|
||||
};
|
||||
const row = (v: CatalogVersion) => {
|
||||
const fit = fitOn(v, hardware, true, cache);
|
||||
const on = v.id === selectedId, have = !!downloaded?.has(v.id);
|
||||
const state = versionFit(v, hardware, cache);
|
||||
const technical = [MAKERS[v.sourceFamily], need(v, true), state === 'ssd' ? tr("Uses SSD {0} GB", [Math.round(fit.spillGb)]) : ''].filter(Boolean).join(' · ');
|
||||
const technical = [MAKERS[v.sourceFamily], need(v, true)].filter(Boolean).join(' · ');
|
||||
const note = versionNote(v.model);
|
||||
return <button key={v.id} role="menuitemradio" aria-checked={on} className={cx('version-row', on && 'is-on')} onClick={() => { onSelect(v.id); setOpen(false); }}>
|
||||
<span className="version-row__check">{tr(on ? <Icon name="check" size={14}/> : null)}</span>
|
||||
<span className="version-row__body">
|
||||
<span className="version-row__title"><strong>{versionName(v)}</strong>{tr(have ? <span className="version-row__tags"><span className="version-tag version-tag--have">{tr("Downloaded")}</span></span> : null)}</span>
|
||||
{tr(state === 'ssd' || state === 'no' ? <span className="version-row__fit" data-fit={state}>{tr(state === 'ssd' ? 'SSD streaming · will run slower' : "Doesn't fit")}</span> : null)}
|
||||
{tr(state === 'ssd' ? <span className="version-row__fit" data-fit={state}>{tr('may run slower')}</span> : null)}
|
||||
{tr(details ? <span className="version-row__tech">{technical}{tr(note ? <><br/>{note}</> : null)}</span> : null)}
|
||||
</span>
|
||||
<span className="version-row__size">{have ? gb(v.download_gb) : tr("{0} download", [gb(v.download_gb)])}</span>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
/* All the interface strings of one language (de): the base file and the 13 add-on files, merged in the order the app has always used (a later file wins).
|
||||
Loaded the first time the language is used (see loadCatalog in i18n.ts); English stays in the main file because the English text is the key. */
|
||||
import base from '../locales/de.json';
|
||||
import tune from '../locales/tune-de.json';
|
||||
import tools from '../locales/tools-de.json';
|
||||
import fix from '../locales/fix-de.json';
|
||||
import gh from '../locales/gh-de.json';
|
||||
import ui from '../locales/ui-de.json';
|
||||
import engine from '../locales/engine-de.json';
|
||||
import pick from '../locales/pick-de.json';
|
||||
import df2 from '../locales/df2-de.json';
|
||||
import upd from '../locales/upd-de.json';
|
||||
import r3 from '../locales/r3-de.json';
|
||||
import pic from '../locales/pic-de.json';
|
||||
import gui from '../locales/gui-de.json';
|
||||
import sav from '../locales/sav-de.json';
|
||||
export default {...base,...tune,...tools,...fix,...gh,...ui,...engine,...pick,...df2,...upd,...r3,...pic,...gui,...sav} as Record<string,string>;
|
||||
@@ -0,0 +1,17 @@
|
||||
/* All the interface strings of one language (es): the base file and the 13 add-on files, merged in the order the app has always used (a later file wins).
|
||||
Loaded the first time the language is used (see loadCatalog in i18n.ts); English stays in the main file because the English text is the key. */
|
||||
import base from '../locales/es.json';
|
||||
import tune from '../locales/tune-es.json';
|
||||
import tools from '../locales/tools-es.json';
|
||||
import fix from '../locales/fix-es.json';
|
||||
import gh from '../locales/gh-es.json';
|
||||
import ui from '../locales/ui-es.json';
|
||||
import engine from '../locales/engine-es.json';
|
||||
import pick from '../locales/pick-es.json';
|
||||
import df2 from '../locales/df2-es.json';
|
||||
import upd from '../locales/upd-es.json';
|
||||
import r3 from '../locales/r3-es.json';
|
||||
import pic from '../locales/pic-es.json';
|
||||
import gui from '../locales/gui-es.json';
|
||||
import sav from '../locales/sav-es.json';
|
||||
export default {...base,...tune,...tools,...fix,...gh,...ui,...engine,...pick,...df2,...upd,...r3,...pic,...gui,...sav} as Record<string,string>;
|
||||
@@ -0,0 +1,17 @@
|
||||
/* All the interface strings of one language (fr): the base file and the 13 add-on files, merged in the order the app has always used (a later file wins).
|
||||
Loaded the first time the language is used (see loadCatalog in i18n.ts); English stays in the main file because the English text is the key. */
|
||||
import base from '../locales/fr.json';
|
||||
import tune from '../locales/tune-fr.json';
|
||||
import tools from '../locales/tools-fr.json';
|
||||
import fix from '../locales/fix-fr.json';
|
||||
import gh from '../locales/gh-fr.json';
|
||||
import ui from '../locales/ui-fr.json';
|
||||
import engine from '../locales/engine-fr.json';
|
||||
import pick from '../locales/pick-fr.json';
|
||||
import df2 from '../locales/df2-fr.json';
|
||||
import upd from '../locales/upd-fr.json';
|
||||
import r3 from '../locales/r3-fr.json';
|
||||
import pic from '../locales/pic-fr.json';
|
||||
import gui from '../locales/gui-fr.json';
|
||||
import sav from '../locales/sav-fr.json';
|
||||
export default {...base,...tune,...tools,...fix,...gh,...ui,...engine,...pick,...df2,...upd,...r3,...pic,...gui,...sav} as Record<string,string>;
|
||||
@@ -0,0 +1,17 @@
|
||||
/* All the interface strings of one language (ja): the base file and the 13 add-on files, merged in the order the app has always used (a later file wins).
|
||||
Loaded the first time the language is used (see loadCatalog in i18n.ts); English stays in the main file because the English text is the key. */
|
||||
import base from '../locales/ja.json';
|
||||
import tune from '../locales/tune-ja.json';
|
||||
import tools from '../locales/tools-ja.json';
|
||||
import fix from '../locales/fix-ja.json';
|
||||
import gh from '../locales/gh-ja.json';
|
||||
import ui from '../locales/ui-ja.json';
|
||||
import engine from '../locales/engine-ja.json';
|
||||
import pick from '../locales/pick-ja.json';
|
||||
import df2 from '../locales/df2-ja.json';
|
||||
import upd from '../locales/upd-ja.json';
|
||||
import r3 from '../locales/r3-ja.json';
|
||||
import pic from '../locales/pic-ja.json';
|
||||
import gui from '../locales/gui-ja.json';
|
||||
import sav from '../locales/sav-ja.json';
|
||||
export default {...base,...tune,...tools,...fix,...gh,...ui,...engine,...pick,...df2,...upd,...r3,...pic,...gui,...sav} as Record<string,string>;
|
||||
@@ -0,0 +1,17 @@
|
||||
/* All the interface strings of one language (ko): the base file and the 13 add-on files, merged in the order the app has always used (a later file wins).
|
||||
Loaded the first time the language is used (see loadCatalog in i18n.ts); English stays in the main file because the English text is the key. */
|
||||
import base from '../locales/ko.json';
|
||||
import tune from '../locales/tune-ko.json';
|
||||
import tools from '../locales/tools-ko.json';
|
||||
import fix from '../locales/fix-ko.json';
|
||||
import gh from '../locales/gh-ko.json';
|
||||
import ui from '../locales/ui-ko.json';
|
||||
import engine from '../locales/engine-ko.json';
|
||||
import pick from '../locales/pick-ko.json';
|
||||
import df2 from '../locales/df2-ko.json';
|
||||
import upd from '../locales/upd-ko.json';
|
||||
import r3 from '../locales/r3-ko.json';
|
||||
import pic from '../locales/pic-ko.json';
|
||||
import gui from '../locales/gui-ko.json';
|
||||
import sav from '../locales/sav-ko.json';
|
||||
export default {...base,...tune,...tools,...fix,...gh,...ui,...engine,...pick,...df2,...upd,...r3,...pic,...gui,...sav} as Record<string,string>;
|
||||
@@ -0,0 +1,17 @@
|
||||
/* All the interface strings of one language (pt): the base file and the 13 add-on files, merged in the order the app has always used (a later file wins).
|
||||
Loaded the first time the language is used (see loadCatalog in i18n.ts); English stays in the main file because the English text is the key. */
|
||||
import base from '../locales/pt.json';
|
||||
import tune from '../locales/tune-pt.json';
|
||||
import tools from '../locales/tools-pt.json';
|
||||
import fix from '../locales/fix-pt.json';
|
||||
import gh from '../locales/gh-pt.json';
|
||||
import ui from '../locales/ui-pt.json';
|
||||
import engine from '../locales/engine-pt.json';
|
||||
import pick from '../locales/pick-pt.json';
|
||||
import df2 from '../locales/df2-pt.json';
|
||||
import upd from '../locales/upd-pt.json';
|
||||
import r3 from '../locales/r3-pt.json';
|
||||
import pic from '../locales/pic-pt.json';
|
||||
import gui from '../locales/gui-pt.json';
|
||||
import sav from '../locales/sav-pt.json';
|
||||
export default {...base,...tune,...tools,...fix,...gh,...ui,...engine,...pick,...df2,...upd,...r3,...pic,...gui,...sav} as Record<string,string>;
|
||||
@@ -0,0 +1,17 @@
|
||||
/* All the interface strings of one language (ru): the base file and the 13 add-on files, merged in the order the app has always used (a later file wins).
|
||||
Loaded the first time the language is used (see loadCatalog in i18n.ts); English stays in the main file because the English text is the key. */
|
||||
import base from '../locales/ru.json';
|
||||
import tune from '../locales/tune-ru.json';
|
||||
import tools from '../locales/tools-ru.json';
|
||||
import fix from '../locales/fix-ru.json';
|
||||
import gh from '../locales/gh-ru.json';
|
||||
import ui from '../locales/ui-ru.json';
|
||||
import engine from '../locales/engine-ru.json';
|
||||
import pick from '../locales/pick-ru.json';
|
||||
import df2 from '../locales/df2-ru.json';
|
||||
import upd from '../locales/upd-ru.json';
|
||||
import r3 from '../locales/r3-ru.json';
|
||||
import pic from '../locales/pic-ru.json';
|
||||
import gui from '../locales/gui-ru.json';
|
||||
import sav from '../locales/sav-ru.json';
|
||||
export default {...base,...tune,...tools,...fix,...gh,...ui,...engine,...pick,...df2,...upd,...r3,...pic,...gui,...sav} as Record<string,string>;
|
||||
@@ -0,0 +1,17 @@
|
||||
/* All the interface strings of one language (zh): the base file and the 13 add-on files, merged in the order the app has always used (a later file wins).
|
||||
Loaded the first time the language is used (see loadCatalog in i18n.ts); English stays in the main file because the English text is the key. */
|
||||
import base from '../locales/zh.json';
|
||||
import tune from '../locales/tune-zh.json';
|
||||
import tools from '../locales/tools-zh.json';
|
||||
import fix from '../locales/fix-zh.json';
|
||||
import gh from '../locales/gh-zh.json';
|
||||
import ui from '../locales/ui-zh.json';
|
||||
import engine from '../locales/engine-zh.json';
|
||||
import pick from '../locales/pick-zh.json';
|
||||
import df2 from '../locales/df2-zh.json';
|
||||
import upd from '../locales/upd-zh.json';
|
||||
import r3 from '../locales/r3-zh.json';
|
||||
import pic from '../locales/pic-zh.json';
|
||||
import gui from '../locales/gui-zh.json';
|
||||
import sav from '../locales/sav-zh.json';
|
||||
export default {...base,...tune,...tools,...fix,...gh,...ui,...engine,...pick,...df2,...upd,...r3,...pic,...gui,...sav} as Record<string,string>;
|
||||
@@ -0,0 +1,6 @@
|
||||
/** The language name of a code fence or a file name. Kept apart from the highlighter (syntax.ts, which carries the Prism grammars) so a code block can name its language before the grammars have loaded. */
|
||||
const aliases:Record<string,string>={ts:'typescript',tsx:'tsx',mts:'typescript',cts:'typescript',js:'javascript',jsx:'jsx',mjs:'javascript',cjs:'javascript',py:'python',pyw:'python',md:'markdown',mdx:'markdown',markdown:'markdown',html:'markup',htm:'markup',xml:'markup',svg:'markup',json:'json',css:'css',sh:'bash',shell:'bash',bash:'bash',zsh:'bash',yml:'yaml',yaml:'yaml',rs:'rust',go:'go',java:'java',c:'c',h:'c',cc:'cpp',cpp:'cpp',cxx:'cpp',hpp:'cpp',cs:'csharp',csharp:'csharp',sql:'sql',typescript:'typescript',javascript:'javascript',python:'python',rust:'rust'};
|
||||
export function codeLanguage(value:string,filename=false){
|
||||
const input=filename?(value.split(/[?#]/)[0].split('.').at(-1)??''):value.trim().split(/\s/)[0].toLowerCase();
|
||||
return aliases[input.toLowerCase()]??'plain';
|
||||
}
|
||||
+18
-115
@@ -1,138 +1,26 @@
|
||||
import baseEnglish from '../locales/en.json';
|
||||
import tune_en from '../locales/tune-en.json';
|
||||
import tune_zh from '../locales/tune-zh.json';
|
||||
import tune_ja from '../locales/tune-ja.json';
|
||||
import tune_de from '../locales/tune-de.json';
|
||||
import tune_es from '../locales/tune-es.json';
|
||||
import tune_ru from '../locales/tune-ru.json';
|
||||
import tune_pt from '../locales/tune-pt.json';
|
||||
import tune_ko from '../locales/tune-ko.json';
|
||||
import tune_fr from '../locales/tune-fr.json';
|
||||
import tools_en from '../locales/tools-en.json';
|
||||
import tools_zh from '../locales/tools-zh.json';
|
||||
import tools_ja from '../locales/tools-ja.json';
|
||||
import tools_de from '../locales/tools-de.json';
|
||||
import tools_es from '../locales/tools-es.json';
|
||||
import tools_ru from '../locales/tools-ru.json';
|
||||
import tools_pt from '../locales/tools-pt.json';
|
||||
import tools_ko from '../locales/tools-ko.json';
|
||||
import tools_fr from '../locales/tools-fr.json';
|
||||
import fix_en from '../locales/fix-en.json';
|
||||
import fix_zh from '../locales/fix-zh.json';
|
||||
import fix_ja from '../locales/fix-ja.json';
|
||||
import fix_de from '../locales/fix-de.json';
|
||||
import fix_es from '../locales/fix-es.json';
|
||||
import fix_ru from '../locales/fix-ru.json';
|
||||
import fix_pt from '../locales/fix-pt.json';
|
||||
import fix_ko from '../locales/fix-ko.json';
|
||||
import fix_fr from '../locales/fix-fr.json';
|
||||
/* The strings of the 0.2.7 GitHub items (gh-<lang>.json, scripts/make-gh-locales.py). */
|
||||
import gh_en from '../locales/gh-en.json';
|
||||
import gh_zh from '../locales/gh-zh.json';
|
||||
import gh_ja from '../locales/gh-ja.json';
|
||||
import gh_de from '../locales/gh-de.json';
|
||||
import gh_es from '../locales/gh-es.json';
|
||||
import gh_ru from '../locales/gh-ru.json';
|
||||
import gh_pt from '../locales/gh-pt.json';
|
||||
import gh_ko from '../locales/gh-ko.json';
|
||||
import gh_fr from '../locales/gh-fr.json';
|
||||
/* The strings of the 0.2.10 interface changes (ui-<lang>.json, scripts/make-ui-locales.py). */
|
||||
import ui_en from '../locales/ui-en.json';
|
||||
import ui_zh from '../locales/ui-zh.json';
|
||||
import ui_ja from '../locales/ui-ja.json';
|
||||
import ui_de from '../locales/ui-de.json';
|
||||
import ui_es from '../locales/ui-es.json';
|
||||
import ui_ru from '../locales/ui-ru.json';
|
||||
import ui_pt from '../locales/ui-pt.json';
|
||||
import ui_ko from '../locales/ui-ko.json';
|
||||
import ui_fr from '../locales/ui-fr.json';
|
||||
/* The strings of Settings > Engine (engine-<lang>.json, scripts/make-engine-locales.py). */
|
||||
import engine_en from '../locales/engine-en.json';
|
||||
import engine_zh from '../locales/engine-zh.json';
|
||||
import engine_ja from '../locales/engine-ja.json';
|
||||
import engine_de from '../locales/engine-de.json';
|
||||
import engine_es from '../locales/engine-es.json';
|
||||
import engine_ru from '../locales/engine-ru.json';
|
||||
import engine_pt from '../locales/engine-pt.json';
|
||||
import engine_ko from '../locales/engine-ko.json';
|
||||
import engine_fr from '../locales/engine-fr.json';
|
||||
/* The strings of the model version list (pick-<lang>.json, scripts/make-pick-locales.py). */
|
||||
import pick_en from '../locales/pick-en.json';
|
||||
import pick_zh from '../locales/pick-zh.json';
|
||||
import pick_ja from '../locales/pick-ja.json';
|
||||
import pick_de from '../locales/pick-de.json';
|
||||
import pick_es from '../locales/pick-es.json';
|
||||
import pick_ru from '../locales/pick-ru.json';
|
||||
import pick_pt from '../locales/pick-pt.json';
|
||||
import pick_ko from '../locales/pick-ko.json';
|
||||
import pick_fr from '../locales/pick-fr.json';
|
||||
/* The strings of the Drafting choice and the SSD line (df2-<lang>.json, scripts/make-df2-locales.py). */
|
||||
import df2_en from '../locales/df2-en.json';
|
||||
import df2_zh from '../locales/df2-zh.json';
|
||||
import df2_ja from '../locales/df2-ja.json';
|
||||
import df2_de from '../locales/df2-de.json';
|
||||
import df2_es from '../locales/df2-es.json';
|
||||
import df2_ru from '../locales/df2-ru.json';
|
||||
import df2_pt from '../locales/df2-pt.json';
|
||||
import df2_ko from '../locales/df2-ko.json';
|
||||
import df2_fr from '../locales/df2-fr.json';
|
||||
/* The strings of the engine update beside a loaded model (upd-<lang>.json, scripts/make-update-locales.py). */
|
||||
import upd_en from '../locales/upd-en.json';
|
||||
import upd_zh from '../locales/upd-zh.json';
|
||||
import upd_ja from '../locales/upd-ja.json';
|
||||
import upd_de from '../locales/upd-de.json';
|
||||
import upd_es from '../locales/upd-es.json';
|
||||
import upd_ru from '../locales/upd-ru.json';
|
||||
import upd_pt from '../locales/upd-pt.json';
|
||||
import upd_ko from '../locales/upd-ko.json';
|
||||
import upd_fr from '../locales/upd-fr.json';
|
||||
import r3_en from '../locales/r3-en.json';
|
||||
import r3_zh from '../locales/r3-zh.json';
|
||||
import r3_ja from '../locales/r3-ja.json';
|
||||
import r3_de from '../locales/r3-de.json';
|
||||
import r3_es from '../locales/r3-es.json';
|
||||
import r3_ru from '../locales/r3-ru.json';
|
||||
import r3_pt from '../locales/r3-pt.json';
|
||||
import r3_ko from '../locales/r3-ko.json';
|
||||
import r3_fr from '../locales/r3-fr.json';
|
||||
/* The picture files Load fetches by itself (pic-<lang>.json; the host words these, strata_control/pictures.py's sentences are the same). */
|
||||
import pic_en from '../locales/pic-en.json';
|
||||
import pic_zh from '../locales/pic-zh.json';
|
||||
import pic_ja from '../locales/pic-ja.json';
|
||||
import pic_de from '../locales/pic-de.json';
|
||||
import pic_es from '../locales/pic-es.json';
|
||||
import pic_ru from '../locales/pic-ru.json';
|
||||
import pic_pt from '../locales/pic-pt.json';
|
||||
import pic_ko from '../locales/pic-ko.json';
|
||||
import pic_fr from '../locales/pic-fr.json';
|
||||
/* The GUI feedback round 2 strings (gui-<lang>.json, scripts/make-gui-locales.py). */
|
||||
import gui_en from '../locales/gui-en.json';
|
||||
import gui_zh from '../locales/gui-zh.json';
|
||||
import gui_ja from '../locales/gui-ja.json';
|
||||
import gui_de from '../locales/gui-de.json';
|
||||
import gui_es from '../locales/gui-es.json';
|
||||
import gui_ru from '../locales/gui-ru.json';
|
||||
import gui_pt from '../locales/gui-pt.json';
|
||||
import gui_ko from '../locales/gui-ko.json';
|
||||
import gui_fr from '../locales/gui-fr.json';
|
||||
/* The notice for chats the host could not write (sav-<lang>.json, scripts/make-chatsave-locales.py). */
|
||||
import sav_en from '../locales/sav-en.json';
|
||||
import sav_zh from '../locales/sav-zh.json';
|
||||
import sav_ja from '../locales/sav-ja.json';
|
||||
import sav_de from '../locales/sav-de.json';
|
||||
import sav_es from '../locales/sav-es.json';
|
||||
import sav_ru from '../locales/sav-ru.json';
|
||||
import sav_pt from '../locales/sav-pt.json';
|
||||
import sav_ko from '../locales/sav-ko.json';
|
||||
import sav_fr from '../locales/sav-fr.json';
|
||||
import base_zh from '../locales/zh.json';
|
||||
import base_ja from '../locales/ja.json';
|
||||
import base_de from '../locales/de.json';
|
||||
import base_es from '../locales/es.json';
|
||||
import base_ru from '../locales/ru.json';
|
||||
import base_pt from '../locales/pt.json';
|
||||
import base_ko from '../locales/ko.json';
|
||||
import base_fr from '../locales/fr.json';
|
||||
|
||||
export const LANGUAGES=[{value:'en',label:'English'},{value:'zh',label:'中文(简体)'},{value:'ja',label:'日本語'},{value:'de',label:'Deutsch'},{value:'fr',label:'Français'},{value:'es',label:'Español'},{value:'ru',label:'Русский'},{value:'pt',label:'Português'},{value:'ko',label:'한국어'}] as const;
|
||||
export type Language=typeof LANGUAGES[number]['value'];
|
||||
@@ -140,8 +28,20 @@ export type Language=typeof LANGUAGES[number]['value'];
|
||||
/* The tool-server, picture and agent-result strings of the tools and images fixes live in tools-<lang>.json for the same reason. */
|
||||
/* The strings that had no key in the files above (#3479, #3825) and the 0.2.4.3 fixes live in fix-<lang>.json (scripts/make-fix-locales.py). */
|
||||
const english:Record<string,string>={...baseEnglish,...tune_en,...tools_en,...fix_en,...gh_en,...ui_en,...engine_en,...pick_en,...df2_en,...upd_en,...r3_en,...pic_en,...gui_en,...sav_en};
|
||||
const [zh,ja,de,es,ru,pt,ko,fr]=[{...base_zh,...tune_zh,...tools_zh,...fix_zh,...gh_zh,...ui_zh,...engine_zh,...pick_zh,...df2_zh,...upd_zh,...r3_zh,...pic_zh,...gui_zh,...sav_zh},{...base_ja,...tune_ja,...tools_ja,...fix_ja,...gh_ja,...ui_ja,...engine_ja,...pick_ja,...df2_ja,...upd_ja,...r3_ja,...pic_ja,...gui_ja,...sav_ja},{...base_de,...tune_de,...tools_de,...fix_de,...gh_de,...ui_de,...engine_de,...pick_de,...df2_de,...upd_de,...r3_de,...pic_de,...gui_de,...sav_de},{...base_es,...tune_es,...tools_es,...fix_es,...gh_es,...ui_es,...engine_es,...pick_es,...df2_es,...upd_es,...r3_es,...pic_es,...gui_es,...sav_es},{...base_ru,...tune_ru,...tools_ru,...fix_ru,...gh_ru,...ui_ru,...engine_ru,...pick_ru,...df2_ru,...upd_ru,...r3_ru,...pic_ru,...gui_ru,...sav_ru},{...base_pt,...tune_pt,...tools_pt,...fix_pt,...gh_pt,...ui_pt,...engine_pt,...pick_pt,...df2_pt,...upd_pt,...r3_pt,...pic_pt,...gui_pt,...sav_pt},{...base_ko,...tune_ko,...tools_ko,...fix_ko,...gh_ko,...ui_ko,...engine_ko,...pick_ko,...df2_ko,...upd_ko,...r3_ko,...pic_ko,...gui_ko,...sav_ko},{...base_fr,...tune_fr,...tools_fr,...fix_fr,...gh_fr,...ui_fr,...engine_fr,...pick_fr,...df2_fr,...upd_fr,...r3_fr,...pic_fr,...gui_fr,...sav_fr}] as Record<string,string>[];
|
||||
const catalogs:Record<Language,Record<string,string>>={en:english,zh,ja,de,fr,es,ru,pt,ko};
|
||||
/* English is the key of every string, so it stays in the main file. The other eight languages are one file each, fetched the first time a language is used
|
||||
(the saved language before the first screen is drawn, a language picked later when it is picked): most people read one of the nine. */
|
||||
type Catalog=Record<string,string>;
|
||||
const loaders:Record<Exclude<Language,'en'>,()=>Promise<{default:Catalog}>>={zh:()=>import('./catalog-zh'),ja:()=>import('./catalog-ja'),de:()=>import('./catalog-de'),fr:()=>import('./catalog-fr'),es:()=>import('./catalog-es'),ru:()=>import('./catalog-ru'),pt:()=>import('./catalog-pt'),ko:()=>import('./catalog-ko')};
|
||||
const catalogs:Partial<Record<Language,Catalog>>={en:english};
|
||||
const loading=new Map<Language,Promise<void>>();
|
||||
export const catalogLoaded=(value:Language)=>!!catalogs[value];
|
||||
/** Fetch the strings of a language (once). Resolves when they are in; a failed fetch also resolves, the text then stays English and the next call tries again. */
|
||||
export function loadCatalog(value:Language):Promise<void>{
|
||||
if(catalogs[value])return Promise.resolve();
|
||||
let request=loading.get(value);
|
||||
if(!request){request=loaders[value as Exclude<Language,'en'>]().then(module=>{catalogs[value]=module.default;},()=>undefined).finally(()=>{loading.delete(value);});loading.set(value,request);}
|
||||
return request;
|
||||
}
|
||||
const entities:Record<string,string>={amp:'&',lt:'<',gt:'>',quot:'"',apos:"'",nbsp:'\u00a0'};
|
||||
const decode=(text:string)=>text.replace(/&(#x[\da-f]+|#\d+|amp|lt|gt|quot|apos|nbsp);/gi,(whole,key:string)=>key[0]==='#'?String.fromCodePoint(key[1].toLowerCase()==='x'?parseInt(key.slice(2),16):parseInt(key.slice(1),10)):entities[key]??whole);
|
||||
const ids=new Map(Object.entries(english).map(([id,value])=>[decode(value),id]));
|
||||
@@ -149,7 +49,10 @@ let language:Language='en';
|
||||
export const validLanguage=(value:unknown):value is Language=>LANGUAGES.some(item=>item.value===value);
|
||||
export const currentLanguage=()=>language;
|
||||
export const localeTag=()=>language==='zh'?'zh-CN':language;
|
||||
export function setTranslationLanguage(value:Language){language=value;}
|
||||
/** Switch the language `tr` answers in. The strings of a language that is not loaded yet come in a moment later (English until then): wait on the result before drawing, or use `loadCatalog` first. */
|
||||
export function setTranslationLanguage(value:Language):Promise<void>{language=value;return loadCatalog(value);}
|
||||
/** Resolves when the current language's strings are in. */
|
||||
export const translationsReady=()=>loadCatalog(language);
|
||||
const escape=(value:string)=>value.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
|
||||
const patterns=Object.entries(english).filter(([,value])=>/\{\d+\}/.test(value)).map(([id,value])=>{
|
||||
const slots:number[]=[];let expression='',at=0;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Screens that are not needed to show the start screen load the first time they are used.
|
||||
*
|
||||
* `lazyScreen(load, pick)` is React.lazy over a dynamic import, for the routed screens.
|
||||
* `useLoadedModule(load, wanted)` is for surfaces that stay mounted and animate open (the Settings and set-up
|
||||
* dialogs): it returns the module once its code is here (null until then), and the caller opens the surface at
|
||||
* that point, so it appears exactly as before (same first frame, same focus target) instead of opening empty and
|
||||
* filling in. If a chunk cannot be fetched the error is thrown into render, and reaches the app's boundary as for
|
||||
* any other screen that fails to show.
|
||||
*/
|
||||
import { lazy, useEffect, useState, type ComponentType } from 'react';
|
||||
|
||||
export function lazyScreen<M, P extends object>(load: () => Promise<M>, pick: (module: M) => ComponentType<P>) {
|
||||
return lazy(() => load().then((module) => ({ default: pick(module) })));
|
||||
}
|
||||
|
||||
const loaded = new WeakMap<object, unknown>();
|
||||
|
||||
export function useLoadedModule<M>(load: () => Promise<M>, wanted: boolean): M | null {
|
||||
const [module, setModule] = useState<M | null>(() => (loaded.get(load) as M | undefined) ?? null);
|
||||
const [failure, setFailure] = useState<unknown>(null);
|
||||
useEffect(() => {
|
||||
if (!wanted || module) return;
|
||||
let live = true;
|
||||
load().then((value) => { loaded.set(load, value); if (live) setModule(value); }, (error) => { if (live) setFailure(error ?? new Error('chunk failed')); });
|
||||
return () => { live = false; };
|
||||
}, [wanted, module, load]);
|
||||
if (failure) throw failure;
|
||||
return module;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ export function nativeCatalogModels(families:NativeFamily[]):CatalogModel[]{
|
||||
if(!families.length)return[];
|
||||
const groups:CatalogModel[]=[{family:'qwen3.8',title:'Qwen3.8-Flash-Next',about:'',experimental:false,images:false,sizes:[]},{family:'qwen3.6',title:'Qwen3.6-35B-A3B',about:'',experimental:false,images:false,sizes:[]}];
|
||||
for(const family of families){
|
||||
if(family.feed){groups.push({family:family.family,title:family.title,about:family.about??'',experimental:false,images:family.images,available:family.available,unavailable_reason:family.unavailable_reason,feed:true,sizes:family.sizes.map(size=>({...size,sourceFamily:family.family,edition:'',images:family.images}))});continue;}
|
||||
if(family.family==='deepseek41'){const sizes=family.sizes.filter(size=>!/^UD-Q8/i.test(size.model));if(sizes.length)groups.push({family:'deepseek41',title:family.title||'DeepSeek-V4.1-Flash',about:family.about??'',experimental:false,images:sizes.some(size=>size.images!==false),sizes:sizes.map(size=>({...size,sourceFamily:family.family,edition:'',images:size.images!==false}))});continue;}
|
||||
const target=/qwen3[._-]?6|35b[-_]?a3b/i.test(family.title+' '+family.family)?groups[1]:['qwen','swift','coder','unsloth','gyro'].includes(family.family)||/qwen3[._-]?8|flash.next/i.test(family.title)?groups[0]:undefined;
|
||||
if(!target)continue;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* about 10 GB left for the system; whatever is left is read from the SSD. The same rules setup uses to choose its low-RAM mode
|
||||
* (setup.py: low_ram_gpu_gb, LOW_RAM_HEADROOM_GB), so the page and the engine agree.
|
||||
*/
|
||||
export interface FitInput { ram_needed_gb?: number; experts_gb?: number | null; memory_gb?: number; mem_total?: boolean }
|
||||
export interface FitInput { ram_needed_gb?: number; experts_gb?: number | null; memory_gb?: number; mem_total?: boolean; /** the weight files without the lookup table, when the catalog states them (DeepSeek: `memory_gb` is the CPU's share) */ fit_gb?: number | null }
|
||||
export interface FitHardware { ram_gb?: number | null; vram_gb?: number | null }
|
||||
export interface Fit {
|
||||
/** RAM + VRAM the version needs (the n-gram table not counted). */
|
||||
@@ -127,26 +127,19 @@ export function versionGroups<V extends { id: string; download_gb: number }>(ver
|
||||
}
|
||||
|
||||
/**
|
||||
* What a version is on this computer. The list writes a word only for the two cases that need one: "SSD streaming · will run slower" (part of the
|
||||
* version is read from the SSD) and "Doesn't fit". A version that fits in memory says nothing.
|
||||
* What a version is on this computer (owner's one rule, 2026-11-08): its files, WITHOUT the n-gram / PLE / Engram lookup table's file (the download size minus
|
||||
* that file: `fit_gb`, else `memory_gb`), against the graphics memory plus the RAM, nothing taken off either side. They fit: 'fits', and the list says nothing.
|
||||
* They do not: 'ssd', and the list says "may run slower" (part is read from the SSD while it answers). Nothing here refuses anything: no card, or a Mac with
|
||||
* little memory, gets the same two words. (Not enough free disk is a different question; the download says it.)
|
||||
*
|
||||
* The figures are the app's own (`fitOn`: graphics memory, then RAM, then the SSD), the same ones the This computer card uses. The catalog's
|
||||
* `on_this_pc` is only a RAM comparison for the plain families (it ignores the graphics card and the SSD): it said "does not fit" for NVFP4 on a
|
||||
* 12 GB card with 63 GB of RAM, which runs with the rest read from the SSD. So the engine's "does not fit" is believed in exactly two cases:
|
||||
* - the version needs a graphics card and this computer has none that counts (DeepSeek-V4.1-Flash: "does not fit: it needs one or more graphics cards");
|
||||
* - this computer has less memory than anything has ever been run with (MIN_RUN_GB) and the engine says it does not fit.
|
||||
* Everything else that does not fit in memory is "ssd": it runs, slower. (Not enough free disk is a different question; the download says it.)
|
||||
* `fitOn` (above) still places the experts for the This computer card; `on_this_pc` no longer decides anything here.
|
||||
*/
|
||||
export type VersionFit = 'fits' | 'ssd' | 'no' | 'unknown';
|
||||
export const MIN_RUN_GB = 16;
|
||||
export type VersionFit = 'fits' | 'ssd' | 'unknown';
|
||||
export function versionFit(v: FitInput & { on_this_pc?: string | null }, hw: FitHardware | null | undefined, cache?: CacheChoice | null): VersionFit {
|
||||
const words = String(v.on_this_pc ?? '').trim().toLowerCase();
|
||||
const engineNo = words.startsWith('does not fit');
|
||||
if (/needs? (one or more|a) graphics cards?/.test(words)) return 'no';
|
||||
const fit = fitOn(v, hw, true, cache);
|
||||
if (!fit.known) return engineNo ? 'no' : 'unknown';
|
||||
if (engineNo && Number(hw?.ram_gb) < MIN_RUN_GB) return 'no';
|
||||
return fit.spillGb >= 1 ? 'ssd' : 'fits';
|
||||
if (!fit.known) return 'unknown';
|
||||
const size = typeof v.fit_gb === 'number' && Number.isFinite(v.fit_gb) ? v.fit_gb : fit.memoryGb;
|
||||
return size <= Number(hw?.ram_gb) + (Number(hw?.vram_gb) || 0) ? 'fits' : 'ssd';
|
||||
}
|
||||
|
||||
/** A card as the installer's hardware report lists it. */
|
||||
|
||||
@@ -3,8 +3,8 @@ import { api } from './client';
|
||||
import type { Hardware, RuntimeModels } from './types';
|
||||
export interface GpuChoiceOption {key:string;cards:number[];label:string;verdict:'fits'|'tight'|'spills'|'unknown';words:string;notes:string[];recommended:boolean}
|
||||
export interface GpuChoice {options:GpuChoiceOption[];recommended:string|null;ignored:{name?:string;why_ignored:string}[];cards:number}
|
||||
export interface NativeSize {/** The first engine that reads this size's files (the catalog), and whether the installed engine is older: the screen then says "needs a newer engine" and offers the update. */engine_min?:string|null;needsNewerEngine?:boolean;id:string;model:string;about:string;download_gb:number;ram_needed_gb:number;experts_gb:number;on_this_pc:string|null;installed:boolean;/** DeepSeek: the engine gives 5 experts here on RDNA4 cards (one card, or several holding half of the experts). */rdna4_faster?:boolean|null;gpu_options?:GpuChoice|null;images?:boolean;imagesCapable?:boolean;/** DeepSeek-V4.1-Flash and the like: several big files and an engine of their own. */multi_file?:boolean;/** DeepSeek: the free disk the set needs (its download plus the pack and room to work), from the set itself. */disk_gb?:number}
|
||||
export interface NativeFamily {family:string;title:string;about:string;experimental:boolean;images:boolean;sizes:NativeSize[]}
|
||||
export interface NativeSize {/** The first engine that reads this size's files (the catalog), and whether the installed engine is older: the screen then says "needs a newer engine" and offers the update. */engine_min?:string|null;needsNewerEngine?:boolean;id:string;model:string;about:string;download_gb:number;ram_needed_gb:number;experts_gb:number;on_this_pc:string|null;/** The files without the lookup table (the download minus the n-gram / PLE / Engram file): what the fit label and the "Needs" figure use. */fit_gb?:number|null;installed:boolean;/** DeepSeek: the engine gives 5 experts here on RDNA4 cards (one card, or several holding half of the experts). */rdna4_faster?:boolean|null;gpu_options?:GpuChoice|null;images?:boolean;imagesCapable?:boolean;/** DeepSeek-V4.1-Flash and the like: several big files and an engine of their own. */multi_file?:boolean;/** DeepSeek: the free disk the set needs (its download plus the pack and room to work), from the set itself. */disk_gb?:number}
|
||||
export interface NativeFamily {family:string;title:string;about:string;experimental:boolean;images:boolean;sizes:NativeSize[];/** A model of the signed update feed (shown after the built-in ones); `available:false` = this engine does not run it, `unavailable_reason` says so in plain words. */feed?:boolean;available?:boolean;unavailable_reason?:string}
|
||||
export interface NativeField {id:string;label:string;group:string;/** Not shown for a model whose id contains this (DeepSeek has no MTP). */notForModel?:string;type:'number'|'boolean'|'string'|'enum'|'array';min?:number;max?:number;choices?:string[];hint?:string;supported?:boolean;forModel?:string}
|
||||
export interface NativeJob {id:string;kind:string;guided?:boolean;model?:string;family?:string;state:'running'|'done'|'error'|'cancelled';startedAt:string;summary:string;error:string|null;details?:string;result:Record<string,any>|null;progress?:Record<string,any>|null;/** An engine update beside a loaded model: which step it is on and the models it concerns (the app words these itself). */step?:'getting-ready'|'stopping'|'stopped';models?:string[]}
|
||||
export interface NativeInstance {id:string;apiModelId:string;model:string;label:string;aliases?:string[];vision?:boolean;resourceProfile?:'work'|'performance'|null;context?:number;port:number;gpus:number[];status:'loading'|'ready'|'unloading'|'error';phase:string;loadedAt:string;error:string|null;/** One short line when the model was loaded without picture reading although it was on (no internet for the picture files, no room on the disk). */picturesNote?:string;metrics:Record<string,any>|null}
|
||||
|
||||
@@ -30,7 +30,7 @@ import { postWithFolderFallback } from './folder-pick';
|
||||
import { createSessionWriter, exportSessions, readBrowserSessions, HOST_FLAG_KEY, PENDING_KEY, type SaveResult, type StorageLike } from './session-persist';
|
||||
import { createHttpTransport, hydrateChats, keepLastMomentCopy, WINDOW_ID } from './chat-sync';
|
||||
import { copyText } from './clipboard';
|
||||
import { setTranslationLanguage, tr, validLanguage, type Language } from './i18n';
|
||||
import { catalogLoaded, loadCatalog, setTranslationLanguage, tr, validLanguage, type Language } from './i18n';
|
||||
/**
|
||||
* Strata app store.
|
||||
*
|
||||
@@ -280,8 +280,8 @@ function loadPrefs(): Prefs {
|
||||
}
|
||||
|
||||
const prefs = loadPrefs();
|
||||
// Use the saved language for the very first render, before host bootstrap.
|
||||
setTranslationLanguage(prefs.language);
|
||||
// Use the saved language for the very first render, before host bootstrap (main.tsx waits for its strings before it draws).
|
||||
void setTranslationLanguage(prefs.language);
|
||||
|
||||
/** The last approval question a notification was shown for, so one question is announced once. */
|
||||
let lastApprovalNotice = '';
|
||||
@@ -494,7 +494,7 @@ export function dismissToast(id: string) {
|
||||
*/
|
||||
function applyPrefs() {
|
||||
const root = document.documentElement;
|
||||
setTranslationLanguage(state.language);
|
||||
void setTranslationLanguage(state.language);
|
||||
root.lang = state.language === 'zh' ? 'zh-CN' : state.language;
|
||||
if(state.fontFamily)root.style.setProperty('--st-font',`${JSON.stringify(state.fontFamily)},system-ui,-apple-system,"Segoe UI",sans-serif`);
|
||||
else root.style.removeProperty('--st-font');
|
||||
@@ -560,7 +560,14 @@ export function toggleRail() {
|
||||
set({ rail: !state.rail });
|
||||
savePrefs();
|
||||
}
|
||||
export function setLanguage(language:Language){if(!validLanguage(language))return;setTranslationLanguage(language);set({language});savePrefs();rememberShellLanguage(language);}
|
||||
/** A language whose strings are already here switches at once; the first use of another one waits for its file (a moment), then switches everything together. The last pick wins. */
|
||||
let languageRequest=0;
|
||||
export function setLanguage(language:Language){
|
||||
if(!validLanguage(language))return;
|
||||
const request=++languageRequest;
|
||||
const apply=()=>{if(request!==languageRequest)return;setTranslationLanguage(language);set({language});savePrefs();rememberShellLanguage(language);};
|
||||
if(catalogLoaded(language))apply();else void loadCatalog(language).then(apply);
|
||||
}
|
||||
let shellAnnounced = false;
|
||||
/** The desktop shell's tray menu follows the app's language; it reads it from a small file the host keeps (no effect in a browser). */
|
||||
function rememberShellLanguage(language:Language){api.post('/api/shell/prefs',{language}).catch(()=>undefined);}
|
||||
|
||||
@@ -16,11 +16,8 @@ import 'prismjs/components/prism-cpp';
|
||||
import 'prismjs/components/prism-csharp';
|
||||
import 'prismjs/components/prism-sql';
|
||||
Prism.manual = true;
|
||||
const aliases:Record<string,string>={ts:'typescript',tsx:'tsx',mts:'typescript',cts:'typescript',js:'javascript',jsx:'jsx',mjs:'javascript',cjs:'javascript',py:'python',pyw:'python',md:'markdown',mdx:'markdown',markdown:'markdown',html:'markup',htm:'markup',xml:'markup',svg:'markup',json:'json',css:'css',sh:'bash',shell:'bash',bash:'bash',zsh:'bash',yml:'yaml',yaml:'yaml',rs:'rust',go:'go',java:'java',c:'c',h:'c',cc:'cpp',cpp:'cpp',cxx:'cpp',hpp:'cpp',cs:'csharp',csharp:'csharp',sql:'sql',typescript:'typescript',javascript:'javascript',python:'python',rust:'rust'};
|
||||
export function codeLanguage(value:string,filename=false){
|
||||
const input=filename?(value.split(/[?#]/)[0].split('.').at(-1)??''):value.trim().split(/\s/)[0].toLowerCase();
|
||||
return aliases[input.toLowerCase()]??'plain';
|
||||
}
|
||||
import { codeLanguage } from './code-language';
|
||||
export { codeLanguage };
|
||||
export interface CodeToken {text:string;classes:string[]}
|
||||
export function highlightedLines(text:string,language:string):CodeToken[][]{
|
||||
const lines:CodeToken[][]=[[]];
|
||||
|
||||
@@ -18,5 +18,7 @@
|
||||
"df17": "Vorausraten: MTP (DFlash2 ist nicht gestartet)",
|
||||
"df18": "Automatisch nutzt die Alle-Karten-Split-Engine mit der MTP-Entwurfsschicht: etwa {0} GB, einmalig herunterzuladen.",
|
||||
"df19": "Die Alle-Karten-Split-Engine konnte nicht starten, deshalb läuft die normale Schichtaufteilung.",
|
||||
"df20": "Vorausraten: aus (den Karten fehlte Speicher)"
|
||||
"df20": "Vorausraten: aus (den Karten fehlte Speicher)",
|
||||
"df21": "Auf Ihrem Mac war DFlash2 bei diesem Modell in unseren Tests schneller als MTP.",
|
||||
"df22": "DFlash2 ist auf dieser Grafikkarte vorerst standardmäßig aus; wählen Sie es unter „Vorausraten“ aus, um es zu nutzen."
|
||||
}
|
||||
@@ -18,5 +18,7 @@
|
||||
"df17": "Drafting: MTP (DFlash2 didn't start)",
|
||||
"df18": "Auto uses the all-cards split engine with the MTP draft layer: about {0} GB to download, once.",
|
||||
"df19": "The all-cards split engine could not start, so the normal layer split runs.",
|
||||
"df20": "Drafting: off (the cards were short of memory)"
|
||||
"df20": "Drafting: off (the cards were short of memory)",
|
||||
"df21": "On your Mac, DFlash2 was faster than MTP on this model in our tests.",
|
||||
"df22": "DFlash2 is off by default on this graphics card for now; choose it in Drafting to use it."
|
||||
}
|
||||
@@ -18,5 +18,7 @@
|
||||
"df17": "Predicción: MTP (DFlash2 no arrancó)",
|
||||
"df18": "Automático usa el motor de reparto entre todas las tarjetas con la capa de borrador MTP: unos {0} GB, una sola vez.",
|
||||
"df19": "El motor de reparto entre todas las tarjetas no pudo arrancar, así que se usa el reparto normal por capas.",
|
||||
"df20": "Predicción: desactivada (a las tarjetas les faltaba memoria)"
|
||||
"df20": "Predicción: desactivada (a las tarjetas les faltaba memoria)",
|
||||
"df21": "En su Mac, en nuestras pruebas DFlash2 fue más rápido que MTP con este modelo.",
|
||||
"df22": "DFlash2 está desactivado de forma predeterminada en esta tarjeta gráfica por ahora; elíjalo en Predicción para usarlo."
|
||||
}
|
||||
@@ -18,5 +18,7 @@
|
||||
"df17": "Anticipation : MTP (DFlash2 n’a pas démarré)",
|
||||
"df18": "Auto utilise le moteur réparti sur toutes les cartes avec la couche de brouillon MTP : environ {0} Go à télécharger, une seule fois.",
|
||||
"df19": "Le moteur réparti sur toutes les cartes n’a pas pu démarrer, la répartition normale par couches est donc utilisée.",
|
||||
"df20": "Anticipation : désactivée (les cartes manquaient de mémoire)"
|
||||
"df20": "Anticipation : désactivée (les cartes manquaient de mémoire)",
|
||||
"df21": "Sur votre Mac, DFlash2 a été plus rapide que MTP avec ce modèle lors de nos tests.",
|
||||
"df22": "DFlash2 est désactivé par défaut sur cette carte graphique pour le moment ; choisissez-le dans Anticipation pour l’utiliser."
|
||||
}
|
||||
@@ -18,5 +18,7 @@
|
||||
"df17": "先読み:MTP(DFlash2 は起動しませんでした)",
|
||||
"df18": "自動では、全カードのスプリットエンジンと MTP ドラフト層を使います。約 {0} GB を一度だけダウンロードします。",
|
||||
"df19": "全カードのスプリットエンジンを起動できなかったため、通常のレイヤー分割で動作しています。",
|
||||
"df20": "先読み:オフ(グラフィックスカードのメモリが不足していました)"
|
||||
"df20": "先読み:オフ(グラフィックスカードのメモリが不足していました)",
|
||||
"df21": "お使いの Mac では、私たちのテストでこのモデルにおいて DFlash2 は MTP より速くなりました。",
|
||||
"df22": "現在、このグラフィックスカードでは DFlash2 は既定でオフです。使うには「先読み」で選んでください。"
|
||||
}
|
||||
@@ -18,5 +18,7 @@
|
||||
"df17": "초안 예측: MTP(DFlash2가 시작되지 않았습니다)",
|
||||
"df18": "자동은 모든 카드를 쓰는 분할 엔진과 MTP 초안 층을 사용합니다(약 {0}GB, 한 번만 내려받음).",
|
||||
"df19": "모든 카드를 쓰는 분할 엔진을 시작하지 못해 일반 레이어 분할로 실행합니다.",
|
||||
"df20": "초안 예측: 꺼짐(카드 메모리가 부족했습니다)"
|
||||
"df20": "초안 예측: 꺼짐(카드 메모리가 부족했습니다)",
|
||||
"df21": "고객님의 Mac에서 저희 테스트 결과 이 모델에서는 DFlash2가 MTP보다 빨랐습니다.",
|
||||
"df22": "현재 이 그래픽 카드에서는 DFlash2가 기본적으로 꺼져 있습니다. 사용하려면 ‘초안 예측’에서 선택하세요."
|
||||
}
|
||||
@@ -18,5 +18,7 @@
|
||||
"df17": "Previsão: MTP (o DFlash2 não iniciou)",
|
||||
"df18": "O automático usa o motor de divisão entre todas as placas com a camada de rascunho MTP: cerca de {0} GB, uma vez.",
|
||||
"df19": "O motor de divisão entre todas as placas não conseguiu iniciar, então a divisão normal por camadas está em uso.",
|
||||
"df20": "Previsão: desligada (faltou memória nas placas)"
|
||||
"df20": "Previsão: desligada (faltou memória nas placas)",
|
||||
"df21": "No seu Mac, nos nossos testes o DFlash2 foi mais rápido que o MTP com este modelo.",
|
||||
"df22": "O DFlash2 está desligado por predefinição nesta placa de vídeo por agora; escolha-o em Previsão para o usar."
|
||||
}
|
||||
@@ -18,5 +18,7 @@
|
||||
"df17": "Предсказание: MTP (DFlash2 не запустился)",
|
||||
"df18": "Авто использует движок с разделением по всем картам и черновой слой MTP: около {0} ГБ, скачивается один раз.",
|
||||
"df19": "Движок с разделением по всем картам не запустился, поэтому работает обычное послойное разделение.",
|
||||
"df20": "Предсказание: выключено (на картах не хватило памяти)"
|
||||
"df20": "Предсказание: выключено (на картах не хватило памяти)",
|
||||
"df21": "На вашем Mac в наших тестах DFlash2 с этой моделью был быстрее MTP.",
|
||||
"df22": "На этой видеокарте DFlash2 пока выключен по умолчанию; чтобы использовать его, выберите его в разделе «Предсказание»."
|
||||
}
|
||||
@@ -18,5 +18,7 @@
|
||||
"df17": "预测草稿:MTP(DFlash2 未能启动)",
|
||||
"df18": "自动模式使用全部显卡的拆分引擎和 MTP 草稿层:约 {0} GB,只下载一次。",
|
||||
"df19": "全显卡拆分引擎未能启动,因此改用常规的分层拆分。",
|
||||
"df20": "预测草稿:关闭(显卡内存不足)"
|
||||
"df20": "预测草稿:关闭(显卡内存不足)",
|
||||
"df21": "在您的 Mac 上,我们的测试中 DFlash2 在此模型上比 MTP 更快。",
|
||||
"df22": "目前在这张显卡上 DFlash2 默认关闭;如需使用,请在“预测草稿”中选择它。"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"r3_01": "Braucht ein Engine-Update",
|
||||
"r3_02": "SSD-Streaming · läuft langsamer",
|
||||
"r3_02": "kann langsamer laufen",
|
||||
"r3_03": "So viel Kontext belegt etwa {0} GB Grafikspeicher und lässt auf dieser Karte wenig Platz für das Modell. Strata versucht es; startet es nicht, wähle einen kürzeren Kontext.",
|
||||
"r3_04": "In Arbeit · {0}",
|
||||
"r3_05": "{0} Chats in Arbeit",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"r3_01": "Needs an engine update",
|
||||
"r3_02": "SSD streaming · will run slower",
|
||||
"r3_02": "may run slower",
|
||||
"r3_03": "This much context takes about {0} GB of graphics memory and leaves little room for the model on this card. Strata will try it; if it does not start, choose a shorter context.",
|
||||
"r3_04": "Working · {0}",
|
||||
"r3_05": "{0} chats working",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"r3_01": "Necesita actualizar el motor",
|
||||
"r3_02": "Streaming desde el SSD · irá más lento",
|
||||
"r3_02": "puede ir más lento",
|
||||
"r3_03": "Este contexto ocupa unos {0} GB de memoria gráfica y deja poco espacio para el modelo en esta tarjeta. Strata lo intentará; si no arranca, elige un contexto más corto.",
|
||||
"r3_04": "Trabajando · {0}",
|
||||
"r3_05": "{0} chats trabajando",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"r3_01": "Nécessite une mise à jour du moteur",
|
||||
"r3_02": "Streaming depuis le SSD · plus lent",
|
||||
"r3_02": "peut être plus lent",
|
||||
"r3_03": "Ce contexte occupe environ {0} Go de mémoire graphique et laisse peu de place au modèle sur cette carte. Strata va essayer ; s’il ne démarre pas, choisissez un contexte plus court.",
|
||||
"r3_04": "En cours de travail · {0}",
|
||||
"r3_05": "{0} conversations en cours",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"r3_01": "エンジンの更新が必要です",
|
||||
"r3_02": "SSD ストリーミング · 動作は遅くなります",
|
||||
"r3_02": "動作が遅くなる可能性があります",
|
||||
"r3_03": "このコンテキストはグラフィックスメモリーを約 {0} GB 使い、このカードではモデルに残る余裕がわずかです。Strata は試しますが、起動しない場合はより短いコンテキストを選んでください。",
|
||||
"r3_04": "作業中 · {0}",
|
||||
"r3_05": "{0} 件のチャットが作業中",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"r3_01": "엔진 업데이트가 필요합니다",
|
||||
"r3_02": "SSD 스트리밍 · 더 느리게 실행됩니다",
|
||||
"r3_02": "더 느리게 실행될 수 있습니다",
|
||||
"r3_03": "이 정도의 컨텍스트는 그래픽 메모리를 약 {0} GB 사용하며 이 카드에서는 모델이 쓸 공간이 거의 남지 않습니다. Strata가 시도하지만 시작되지 않으면 더 짧은 컨텍스트를 선택하세요.",
|
||||
"r3_04": "작업 중 · {0}",
|
||||
"r3_05": "대화 {0}개 작업 중",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"r3_01": "Precisa de uma atualização do motor",
|
||||
"r3_02": "Streaming do SSD · vai rodar mais devagar",
|
||||
"r3_02": "pode rodar mais devagar",
|
||||
"r3_03": "Esse contexto ocupa cerca de {0} GB de memória gráfica e deixa pouco espaço para o modelo nesta placa. O Strata vai tentar; se não iniciar, escolha um contexto menor.",
|
||||
"r3_04": "Trabalhando · {0}",
|
||||
"r3_05": "{0} chats trabalhando",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"r3_01": "Нужно обновить движок",
|
||||
"r3_02": "Потоковая подгрузка с SSD · будет работать медленнее",
|
||||
"r3_02": "может работать медленнее",
|
||||
"r3_03": "Такой контекст занимает около {0} ГБ видеопамяти и оставляет модели мало места на этой карте. Strata попробует запустить; если не получится, выберите контекст короче.",
|
||||
"r3_04": "В работе · {0}",
|
||||
"r3_05": "В работе чатов: {0}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"r3_01": "需要更新引擎",
|
||||
"r3_02": "SSD 流式读取 · 运行会更慢",
|
||||
"r3_02": "运行可能会更慢",
|
||||
"r3_03": "这么长的上下文大约占用 {0} GB 显存,留给模型的空间很少。Strata 会尝试运行;如果无法启动,请选择更短的上下文。",
|
||||
"r3_04": "进行中 · {0}",
|
||||
"r3_05": "{0} 个对话进行中",
|
||||
|
||||
+17
-5
@@ -6,6 +6,8 @@ import './styles/app.css';
|
||||
import { App } from './app/App';
|
||||
import { installDropGuard } from './components/DropZone';
|
||||
import { startRecovery } from './lib/recovery';
|
||||
import { translationsReady } from './lib/i18n';
|
||||
import { loadHighlighter } from './components/SyntaxCode';
|
||||
|
||||
const el = document.getElementById('root');
|
||||
if (!el) throw new Error('#root missing');
|
||||
@@ -13,11 +15,21 @@ if (!el) throw new Error('#root missing');
|
||||
installDropGuard();
|
||||
startRecovery();
|
||||
|
||||
createRoot(el).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
// The saved language's strings are fetched while the rest of the code loads; the first screen is drawn once they are in (English needs no wait).
|
||||
// Code colouring (the Prism grammars, 130 kB) is not needed to show the first screen: it is fetched a few seconds later, when the browser is idle,
|
||||
// so the first code block of the session is already coloured when it appears. A block that appears sooner fetches it itself.
|
||||
window.setTimeout(() => {
|
||||
const idle: (run: () => void) => void = 'requestIdleCallback' in window ? (run) => window.requestIdleCallback(run, { timeout: 4000 }) : (run) => window.setTimeout(run, 0);
|
||||
idle(() => { void loadHighlighter(); void import('./components/TimeSkipPanel'); void import('./components/TimeSkipWorking'); });
|
||||
}, 3000);
|
||||
|
||||
void translationsReady().then(() => {
|
||||
createRoot(el).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
});
|
||||
|
||||
import './styles/annotation-fixes.css';
|
||||
import './styles/monitor.css';
|
||||
|
||||
@@ -2,9 +2,8 @@ import {localeTag} from '../lib/i18n';
|
||||
import { tr } from '../lib/i18n';
|
||||
import { UsageDashboard } from '../components/UsageDashboard';
|
||||
import { FloatingMenu } from '../components/FloatingMenu';
|
||||
import { TimeSkipPanel } from '../components/TimeSkipPanel';
|
||||
import { lazyScreen } from '../lib/lazy-screen';
|
||||
import { timeSkipPrompt, timeSkipTabId, type TimeSkipRun } from '../lib/time-skip';
|
||||
import { TimeSkipWorking } from '../components/TimeSkipWorking';
|
||||
import { ChatToolbar } from '../components/ChatToolbar';
|
||||
import { ChatTelemetry } from '../components/TelemetryPopover';
|
||||
import { ArtifactCard } from '../components/ArtifactCard';
|
||||
@@ -40,7 +39,7 @@ import { openExternal } from '../lib/open-external';
|
||||
import { busyPlan } from '../lib/while-busy';
|
||||
import { composeQuoted, parseQuoted, type Quote } from '../lib/quotes';
|
||||
/** Chat — talk to the one loaded model. */
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Markdown } from '../components/Markdown';
|
||||
import { Btn, Icon, IconBtn, ErrorNotice, type IconName } from '../components/ui';
|
||||
import { composerCap, scrollBehavior } from '../lib/responsive';
|
||||
@@ -52,6 +51,9 @@ import { getSlashCommands } from '../lib/commands';
|
||||
import { useLocation } from '../lib/router';
|
||||
import { composerSends } from '../lib/shortcut-bindings';
|
||||
import type { ChatMessage, Session, ToolCall } from '../lib/types';
|
||||
// The Time-skip panel and its progress card are only drawn in a chat that has a Time-skip run: their code loads then (and a few seconds after start, see main.tsx).
|
||||
const TimeSkipPanel = lazyScreen(() => import('../components/TimeSkipPanel'), (m) => m.TimeSkipPanel);
|
||||
const TimeSkipWorking = lazyScreen(() => import('../components/TimeSkipWorking'), (m) => m.TimeSkipWorking);
|
||||
export function ChatScreen() {
|
||||
const location = useLocation();
|
||||
const sessions = useStore((s) => s.sessions);
|
||||
@@ -535,7 +537,7 @@ export function ChatScreen() {
|
||||
<Icon name="chevron" size={18}/></button>) : null)}</div>
|
||||
<Composer draft={draft} setDraft={updateDraft} loaded={!!loaded} streaming={streaming} onSubmit={submit} onKey={onComposerKey} session={session} onPreview={openPreview} textareaRef={textarea}/>
|
||||
</DropZone>
|
||||
{tr(!setupVisible && selectedRun && session ? <TimeSkipPanel key={selectedRun.id} run={selectedRun} sessionId={session.id} inline={inlinePreview} expanded={expanded} onCompare={setExpanded} onPreview={openPreview} onClose={() => { setSelectedPreview(null); setExpanded(false); }}/> : null)}
|
||||
{tr(!setupVisible && selectedRun && session ? <Suspense fallback={null}><TimeSkipPanel key={selectedRun.id} run={selectedRun} sessionId={session.id} inline={inlinePreview} expanded={expanded} onCompare={setExpanded} onPreview={openPreview} onClose={() => { setSelectedPreview(null); setExpanded(false); }}/></Suspense> : null)}
|
||||
{tr(!setupVisible && planShown ? <PlanPanel chatId={session?.id}/> : null)}
|
||||
<ArtifactPanel items={previews} onCloseTab={closeTab} selected={setupVisible || planShown ? null : selectedPreview} onSelect={selectPreview} onClose={() => setSelectedPreview(null)} inline={inlinePreview}/>
|
||||
{tr(resizeHandle)}
|
||||
@@ -781,7 +783,7 @@ function MessageView({ message: m, isLast, onEditPrompt, onPreview, onClosePrevi
|
||||
{tr(m.deliverables?.length && !m.streaming ? <div className="deliverables">{m.deliverables.length >= COMPACT_FILES_FROM ? <FilesSummary files={m.deliverables} answer={m.content} sessionId={activeSession ?? ''} onPreview={onPreview}/> : <>{m.deliverables.map(f => <FileCard key={f.path} file={f} sessionId={activeSession ?? ''} onPreview={onPreview}/>)}<EditedFiles files={m.deliverables} sessionId={activeSession ?? ''} onPreview={onPreview}/></>}</div> : null)}
|
||||
{tr(m.streaming && isLast && !m.timeSkipId ? <StatusLine sessionId={activeSession} startedAt={Date.parse(m.at) || Date.now()} tokens={m.tokens}/> : null)}
|
||||
|
||||
{tr(m.timeSkipId && timeSkipReal && !timeSkipDone && timeSkipRun ? <TimeSkipWorking run={timeSkipRun} sessionId={activeSession ?? ''}/> : m.timeSkipId && (!timeSkipReal || (timeSkipRun?.job?.files?.length ?? 0) > 0) ? <button className="timeskip-chat-card" onClick={() => onOpenTimeSkip(m.timeSkipId!)}><Icon name="branch" size={20}/><span><strong>{tr("Time-skip")}</strong><span>{tr(timeSkipDone ? 'Result ready · compare versions' : 'Revising · follow progress')}</span></span><Icon name="chevron" size={16} className="timeskip-card-arrow"/></button> : null)}
|
||||
{tr(m.timeSkipId && timeSkipReal && !timeSkipDone && timeSkipRun ? <Suspense fallback={null}><TimeSkipWorking run={timeSkipRun} sessionId={activeSession ?? ''}/></Suspense> : m.timeSkipId && (!timeSkipReal || (timeSkipRun?.job?.files?.length ?? 0) > 0) ? <button className="timeskip-chat-card" onClick={() => onOpenTimeSkip(m.timeSkipId!)}><Icon name="branch" size={20}/><span><strong>{tr("Time-skip")}</strong><span>{tr(timeSkipDone ? 'Result ready · compare versions' : 'Revising · follow progress')}</span></span><Icon name="chevron" size={16} className="timeskip-card-arrow"/></button> : null)}
|
||||
{tr(m.media?.length ? <ChatMedia items={m.media} onPreview={onPreview}/> : null)}
|
||||
{tr(m.artifacts?.length ? <div className="artifact-cards">{tr(m.artifacts.map(a => <ArtifactCard key={a.id} artifact={a} onPreview={onPreview}/>))}</div> : null)}
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// The chat's own folder and opening its files by path.
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, symlinkSync, existsSync, readFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, symlinkSync, existsSync, readFileSync, realpathSync } from 'node:fs';
|
||||
import { tmpdir as osTmpdir } from 'node:os';
|
||||
// the canonical scratch root: on a Mac the temp folder is /var/folders/..., which the folder policy refuses as a system folder and which
|
||||
// is /private/var/... once resolved; the tests make their folders under the resolved name (the policy itself is unchanged)
|
||||
const tmpdir = () => realpathSync(osTmpdir());
|
||||
import { join } from 'node:path';
|
||||
import { createChatFolders, openFile, openCommand, resolveChatFile, refusedFolder } from '../server/chat-files.mjs';
|
||||
import { startScriptedWorker } from './helpers/scripted-worker.mjs';
|
||||
@@ -32,6 +35,11 @@ test('a chat folder is remembered per chat, must exist, and is never a system fo
|
||||
} finally { rmSync(dir, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test('a drive the desktop mounted (/run/media/<user>/<drive>) is a fine chat folder; the rest of /run is still system', { skip: process.platform === 'win32' && 'POSIX paths' }, () => {
|
||||
for (const p of ['/run/media/ann/Backup', '/run/media/ann/Backup/projects/pagoda', '/media/ann/Backup/x', '/mnt/data/x']) assert.equal(refusedFolder(p), null, p);
|
||||
for (const p of ['/run', '/run/media', '/run/media/ann', '/run/user/1000/x', '/run/systemd', '/usr/share', '/etc', '/var/lib/x']) assert.match(refusedFolder(p), /system folder/, p);
|
||||
});
|
||||
|
||||
test('files open by path from inside the folder only; the browser gets a file:// address', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'strata-cf2-'));
|
||||
try {
|
||||
|
||||
@@ -35,7 +35,7 @@ test('the notice exists in all nine languages, with the placeholder kept', () =>
|
||||
|
||||
test('the catalog finds the notice: the German page shows it in German, with the number filled in', async () => {
|
||||
const i18n = await bundle('../src/lib/i18n.ts');
|
||||
i18n.setTranslationLanguage('de');
|
||||
await i18n.setTranslationLanguage('de');
|
||||
assert.equal(i18n.tr(TITLE), 'Einige Chats konnten nicht gespeichert werden');
|
||||
assert.equal(i18n.tr(BUTTON), 'Nicht gespeicherte Chats kopieren');
|
||||
assert.match(i18n.tr(BODY, [3]), /^Der Chat-Speicher von Strata ist voll, daher gibt es die neuesten Änderungen an 3 Chat\(s\) nur in diesem Fenster/);
|
||||
|
||||
@@ -68,7 +68,7 @@ test('STRATA_CLOUDFLARED picks exactly one file; STRATA_CLOUDFLARED_SEARCH=own k
|
||||
assert.deepEqual(own.map(c => c.source), ['strata', 'component']);
|
||||
});
|
||||
|
||||
test('a real executable on PATH is found and run (a user-local copy, never a system install)', { skip: process.platform === 'win32' }, async () => {
|
||||
test('a real executable on PATH is found and run (a user-local copy, never a system install)', { skip: process.platform === 'win32' && 'needs a POSIX executable file to run as cloudflared (not on Windows)' }, async () => {
|
||||
const dir = await mkdtemp(join(tmpdir(), 'strata-cf-path-'));
|
||||
try {
|
||||
const bin = join(dir, 'bin'); await mkdir(bin);
|
||||
|
||||
@@ -313,7 +313,8 @@ test('packaged: an app update that ends the app is recorded as finished by the n
|
||||
});
|
||||
|
||||
test('packaged: an update job an OLDER host left running is finished when the app program file is newer than the job', async () => {
|
||||
const exe = new URL('../../strata-desktop.exe', import.meta.url); // <install>/strata-desktop.exe as seen from <install>/host
|
||||
// <install>/strata-desktop(.exe) as seen from <install>/host; native-control.mjs looks for the name of the platform it runs on
|
||||
const exe = new URL(process.platform === 'win32' ? '../../strata-desktop.exe' : '../../strata-desktop', import.meta.url);
|
||||
const f = fixture({ withApp: true });
|
||||
try {
|
||||
const { fileURLToPath } = await import('node:url');
|
||||
|
||||
@@ -22,10 +22,22 @@ test('corrupt or unverified downloads cannot become executable', () => {
|
||||
assert.throws(()=>verifyConnector(bytes,null),/could not be verified/);
|
||||
});
|
||||
|
||||
test('failed concurrent setup shares one download, cleans up and can be retried', async () => {
|
||||
/** Waits until the fake download service has been asked; fails (instead of spinning for ever) when it never is. */
|
||||
async function untilAsked(isAsked){
|
||||
const deadline=Date.now()+10000;
|
||||
while(!isAsked()){
|
||||
if(Date.now()>deadline)throw new Error('the installer never asked the download service (it found a connector, or stopped before the request)');
|
||||
await new Promise(resolve=>setTimeout(resolve,5));
|
||||
}
|
||||
}
|
||||
|
||||
// The installer first looks for a connector that is already on the computer. The look is pinned to Strata's own empty folder
|
||||
// (search:false, an empty environment): a cloudflared installed on the machine that runs the test (PATH, /usr/local/bin, ~/.local/bin)
|
||||
// would otherwise be used, nothing would be downloaded, and the test would wait for ever for a request that never comes.
|
||||
test('failed concurrent setup shares one download, cleans up and can be retried', {timeout:30000}, async () => {
|
||||
const directory=await mkdtemp(join(tmpdir(),'strata-connector-test-'));
|
||||
let requests=0, release;
|
||||
const installer=createConnectorInstaller({directory,platform:'linux',arch:'x64',fetchImpl:async()=>{
|
||||
const installer=createConnectorInstaller({directory,platform:'linux',arch:'x64',env:{},home:directory,search:false,fetchImpl:async()=>{
|
||||
requests++;
|
||||
await new Promise(resolve=>{release=resolve;});
|
||||
return {ok:false};
|
||||
@@ -33,7 +45,7 @@ test('failed concurrent setup shares one download, cleans up and can be retried'
|
||||
try {
|
||||
const first=installer.ensure(),second=installer.ensure();
|
||||
assert.equal(first,second);
|
||||
while(!release)await new Promise(resolve=>setImmediate(resolve));
|
||||
await untilAsked(()=>release);
|
||||
release();
|
||||
const results=await Promise.allSettled([first,second]);
|
||||
assert.ok(results.every(result=>result.status==='rejected'));
|
||||
@@ -41,7 +53,7 @@ test('failed concurrent setup shares one download, cleans up and can be retried'
|
||||
assert.deepEqual(await readdir(directory),[]);
|
||||
release=null;
|
||||
const retry=installer.ensure();
|
||||
while(!release)await new Promise(resolve=>setImmediate(resolve));
|
||||
await untilAsked(()=>release);
|
||||
release();
|
||||
await assert.rejects(retry,/download service/);
|
||||
assert.equal(requests,2);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -9,11 +9,12 @@ import { join } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
|
||||
const read = rel => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), 'utf8').replace(/\r\n/g, '\n');
|
||||
const stdin = { contents: "import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { PcFitCard } from './src/components/PcFitCard'; export { setTranslationLanguage } from './src/lib/i18n'; export const render = props => renderToStaticMarkup(createElement(PcFitCard, props));", resolveDir: fileURLToPath(new URL('..', import.meta.url)), loader: 'ts' };
|
||||
const stdin = { contents: "import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { PcFitCard } from './src/components/PcFitCard'; export { setTranslationLanguage, loadCatalog } from './src/lib/i18n'; export const render = props => renderToStaticMarkup(createElement(PcFitCard, props));", resolveDir: fileURLToPath(new URL('..', import.meta.url)), loader: 'ts' };
|
||||
const built = await build({ stdin, bundle: true, write: false, format: 'esm', platform: 'node', loader: { '.json': 'json', '.tsx': 'tsx' }, jsx: 'automatic', logLevel: 'silent', banner: { js: "import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);" } });
|
||||
const dir = mkdtempSync(join(tmpdir(), 'strata-card-test-'));
|
||||
writeFileSync(join(dir, 'card.mjs'), built.outputFiles[0].text); // a file, not a data: URL: the bundled react-dom/server needs require()
|
||||
const { render, setTranslationLanguage } = await import(pathToFileURL(join(dir, 'card.mjs')).href);
|
||||
const { render, setTranslationLanguage, loadCatalog } = await import(pathToFileURL(join(dir, 'card.mjs')).href);
|
||||
for (const l of ['zh', 'ja', 'de', 'es', 'ru', 'pt', 'ko', 'fr']) await loadCatalog(l); // the non-English catalogs load on first use (src/lib/i18n.ts loadCatalog); load them before the first tr()
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
const fit = (spillGb, known = true) => ({ memoryGb: 250, vramUseGb: 10, ramUseGb: 240, spillGb, known });
|
||||
const card = props => render({ gpuName: 'RTX 5070', ramGb: 64, vramGb: 12, ...props });
|
||||
@@ -67,7 +68,7 @@ test('the Models screen passes the set\'s disk to the card, and the version line
|
||||
const screen = read('../src/components/NativeModels.tsx');
|
||||
assert.match(screen, /<PcComputer [\s\S]*?diskGb=\{selectedSize\?\.multi_file \? selectedSize\.disk_gb \?\? selectedSize\.download_gb : null\}/);
|
||||
const picker = read('../src/components/VersionPicker.tsx');
|
||||
assert.match(picker, /v\.multi_file && v\.disk_gb \? tr\("\{0\} RAM \+ VRAM, plus about \{1\} GB of disk", \[gb\(fit\.memoryGb\), Math\.round\(v\.disk_gb\)\]\)/, 'Details: the disk of the set for DeepSeek');
|
||||
assert.match(picker, /v\.multi_file && v\.disk_gb \? tr\("\{0\} RAM \+ VRAM, plus about \{1\} GB of disk", \[gb\(size\), Math\.round\(v\.disk_gb\)\]\)/, 'Details: the disk of the set for DeepSeek');
|
||||
assert.match(picker, /v\.multi_file && v\.disk_gb \? tr\("Needs \{0\} of memory and about \{1\} GB of disk"/, 'plain: the disk for DeepSeek only');
|
||||
assert.match(picker, /: tr\("Needs \{0\} of memory", \[whole\(fit\.memoryGb\)\]\)/, 'every other model keeps its line');
|
||||
assert.match(picker, /: tr\("Needs \{0\} of memory", \[whole\(size\)\]\)/, 'every other model keeps its line');
|
||||
});
|
||||
@@ -0,0 +1,260 @@
|
||||
/**
|
||||
* 0.2.13: the app fetches the tuned DFlash2 drafter on every path where Auto resolves to it (a plain load, a restart of the stopped models, a tuning run), like the Models screen
|
||||
* own Load does, with the 12 hour rule (--throttle); a drafter the person named or saved wins; the plan and the Settings hint carry the size of the drafter that is really fetched.
|
||||
* The decision is Control (strata drafting plan / fetch; tests/terminal/test_df2ux_0213.py); this file drives the host with a fake Control.
|
||||
*/
|
||||
import './helpers/isolate-profile.mjs'; // a test run never writes the owner's real Strata profile
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { createNativeControl } from '../server/native-control.mjs';
|
||||
import { createControl } from '../server/control-runtime.mjs';
|
||||
import { resolveDrafter as hostResolve } from '../server/native-settings.mjs';
|
||||
|
||||
const SPLIT_PLAN = { ok: true, split: true, choice: 'mtp', drafter: 'mtp', needed: false, gb: 0, line: 'Drafting: MTP (the all-cards split engine uses the MTP draft layer, 5.2 GB)', serves: true };
|
||||
const DFLASH2_PLAN = { ok: true, split: false, choice: 'dflash2', drafter: 'dflash2', needed: true, gb: 1.2, mtp_needed: false, mtp_gb: 0, line: 'Drafting: DFlash2', serves: true };
|
||||
|
||||
/**
|
||||
* A packaged install on two 48 GB cards. `plan(options, state)` answers `drafting plan` (state.installs says how many layer installs ran), `split(options, files)` answers `split-engine apply`
|
||||
* (it may write options.outPath), `installFails` makes the installer's MCP tool fail, `startFails` fails the first N engine starts.
|
||||
*/
|
||||
function fixture({ fetchResult = { ok: true, event: 'result', pack: 'C:/d/dflash2/x.pack', skipped: false }, plan, split = null, installFails = false, startFails = 0, metrics = {}, config = {} } = {}) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'strata-splitapp-test-'));
|
||||
const install = join(root, 'install'), data = join(root, 'host-data'), home = join(root, 'home'), modelsDir = join(home, 'models');
|
||||
for (const d of [install, data, modelsDir]) mkdirSync(d, { recursive: true });
|
||||
const earlier = process.env.STRATA_INSTALL_DIR;
|
||||
process.env.STRATA_INSTALL_DIR = install;
|
||||
const dataFolder = join(root, 'Strata-data'), modelFolder = join(dataFolder, 'models', 'UD-Q4_K_XL');
|
||||
const gguf = join(modelFolder, 'Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00002.gguf');
|
||||
mkdirSync(modelFolder, { recursive: true });
|
||||
writeFileSync(gguf, 'GGUF'); writeFileSync(gguf.replace('00001', '00002'), 'GGUF');
|
||||
const pack = join(dataFolder, 'packs', 'ud_q4_k_xl'), mtp = join(dataFolder, 'mtp', 'rt');
|
||||
mkdirSync(join(pack, 'tokenizer'), { recursive: true });
|
||||
writeFileSync(join(pack, 'native_experts.txt'), 'x');
|
||||
const configPath = join(install, 'strata-ud_q4_k_xl.json');
|
||||
const args = ['--pack', pack, '--native', gguf, '--ple-gguf', gguf.replace('00001', '00002'), '--mtp', mtp, '--max-context', '131072', '--kv', 'int8'];
|
||||
writeFileSync(configPath, JSON.stringify({ exe: 'component:strata-engine', tokenizer: join(pack, 'tokenizer'), args, model_name: 'qwen3.8-flash-next-ud_q4_k_xl', port: 8080, gpu: [0, 1], ...config }));
|
||||
writeFileSync(process.env.STRATA_SETTINGS_FILE, JSON.stringify({ data_dir: join(root, 'host-owned', 'Strata-data'), data_dir_at: 1700000000 }));
|
||||
const state = { installs: 0, planCalls: [], fetchCalls: [], splitCalls: [], saved: [], starts: 0, rpc: [] };
|
||||
const control = {
|
||||
control: { dir: join(root, 'control'), python: join(root, 'control', 'python', 'python') }, version: '0.2.10',
|
||||
async installedModels() {
|
||||
return { install_dir: install, models: [{ id: 'ud_q4_k_xl', name: 'qwen3.8-flash-next-ud_q4_k_xl', source_config: configPath, context_tokens: 131072, backend: 'cuda', component_status: 'present', missing: [], issue_codes: [] }] };
|
||||
},
|
||||
async engine() { return { usable: true, backend: 'cuda', components_dir: join(root, 'c'), engine: { version: '0.1.56', platform: 'linux-x86_64', backend: 'cuda', exe: join(root, 'engine', 'strata'), root: join(root, 'engine') } }; },
|
||||
async draftingPlan(options) { state.planCalls.push(options); return typeof plan === 'function' ? plan(options, state) : plan; },
|
||||
async draftingFetch(options) { state.fetchCalls.push(options); if (fetchResult instanceof Error) throw fetchResult; return fetchResult; },
|
||||
async splitEngine(options) {
|
||||
state.splitCalls.push({ ...options, input: JSON.parse(readFileSync(options.configPath, 'utf8')) });
|
||||
if (!split) return { ok: true, split: false, line: '', warn: false, reasons: [], written: null };
|
||||
return split(options, state);
|
||||
},
|
||||
async saveProfile(name, path) { state.saved.push(JSON.parse(readFileSync(path, 'utf8'))); return {}; },
|
||||
async start() { state.starts += 1; if (state.starts <= startFails) throw new Error('The engine stopped: out of memory'); return { ok: true, state: 'ready' }; },
|
||||
async stop() { return { ok: true }; },
|
||||
};
|
||||
const catalog = { families: [{ family: 'qwen', title: 'Qwen3.8-Flash-Next', about: 'x', images: false, experimental: false, sizes: [
|
||||
{ id: 'ud_q4_k_xl', model: 'UD-Q4_K_XL', download_gb: 22, ram_needed_gb: 60, about: 'x', on_this_pc: 'fits', installed: true, images: false, experimental: false }] }],
|
||||
contexts: [32768, 131072], recommendation: { family: 'qwen', model: 'UD-Q4_K_XL', context: 131072 }, context_note: '' };
|
||||
const hardware = { ram_gb: 256, cpu: { name: 'cpu', cores: 32 }, engine_vendor: 'nvidia', gpus: [0, 1].map(index => ({ index, name: 'NVIDIA RTX A6000', vendor: 'nvidia', vram_gb: 48, arch: 86, usable: true })) };
|
||||
const spawnProcess = (command, argv) => {
|
||||
if (!String(argv[0]).endsWith('strata_mcp.py')) throw new Error('no process but the installer may be started in packaged mode: ' + command + ' ' + argv.join(' '));
|
||||
const child = new EventEmitter(); child.stdout = new EventEmitter(); child.stderr = new EventEmitter(); child.exitCode = null;
|
||||
child.kill = () => { child.exitCode = 0; queueMicrotask(() => child.emit('exit', 0)); return true; };
|
||||
child.stdin = { write(line) {
|
||||
const message = JSON.parse(line); if (!message.id) return;
|
||||
if (message.method === 'tools/call') state.rpc.push(message.params);
|
||||
let result;
|
||||
if (message.method === 'initialize') result = { protocolVersion: '2024-11-05' };
|
||||
else if (message.method === 'tools/list') result = { tools: ['strata_models', 'strata_status', 'strata_install', 'strata_remove'].map(name => ({ name })) };
|
||||
else if (message.params.name === 'strata_models') result = { structuredContent: catalog };
|
||||
else if (message.params.name === 'strata_status') result = { structuredContent: { hardware, recommendation: catalog.recommendation, install: null, running: [], summary: 'x' } };
|
||||
else if (Object.keys(message.params.arguments ?? {}).length === 0) result = { structuredContent: { summary: 'Install finished', install: { running: false, exit_code: 0 } } };
|
||||
else if (message.params.arguments?.confirm) {
|
||||
if (installFails) result = { isError: true, structuredContent: { error: 'The download did not finish.' } };
|
||||
else { state.installs += 1; mkdirSync(mtp, { recursive: true }); writeFileSync(join(mtp, 'dense.bin'), 'x'); result = { structuredContent: { summary: 'Install started', plan: {}, install: { running: true, step: '1/7' } } }; }
|
||||
} else result = { structuredContent: { summary: 'Plan', plan: { title: 'Qwen3.8-Flash-Next UD-Q4_K_XL', download_gb: 5.2, disk_short: null } } };
|
||||
queueMicrotask(() => child.stdout.emit('data', Buffer.from(JSON.stringify({ jsonrpc: '2.0', id: message.id, result }) + String.fromCharCode(10))));
|
||||
} };
|
||||
return child;
|
||||
};
|
||||
const fetcher = async url => String(url).endsWith('/metrics') ? Response.json({ engine: metrics }) : Response.json({ status: 'ok' });
|
||||
const native = createNativeControl({ control, directory: data, spawnProcess, fetcher, requests: () => [], modelsDirectory: () => modelsDir, hardwareProbe: () => [0, 1].map(index => ({ index, vram_gb: 48 })) });
|
||||
return { native, state, root, gguf, modelFolder, mtp, configPath, args,
|
||||
cleanup: () => { if (earlier === undefined) delete process.env.STRATA_INSTALL_DIR; else process.env.STRATA_INSTALL_DIR = earlier; rmSync(root, { recursive: true, force: true }); } };
|
||||
}
|
||||
const finish = async (native, id) => { for (let i = 0; i < 1500; i++) { const job = native.state().jobs.find(j => j.id === id); if (job.state !== 'running') return job; await new Promise(r => setTimeout(r, 10)); } throw new Error('job did not finish'); };
|
||||
const load = (f, args = {}) => f.native.operation('load', { model: 'ud_q4_k_xl', ...args });
|
||||
|
||||
/** The split engine's answer: the layer config with the split's engine and a lane count (Control's split_config, in miniature), written where the host asked. */
|
||||
const splitAnswer = (options) => {
|
||||
const cfg = JSON.parse(readFileSync(options.configPath, 'utf8'));
|
||||
const lanes = Math.min(8, cfg.parallel ?? 6);
|
||||
writeFileSync(options.outPath, JSON.stringify({ ...cfg, exe: '/e/strata-split-2-sm86', args: ['--native', cfg.args[cfg.args.indexOf('--native') + 1], '--batch', String(lanes)], drafting: 'mtp', split_fallback: { exe: cfg.exe, args: cfg.args, parallel: cfg.parallel }, split_engine_note: 'Using the all-cards split engine on 2 cards.' }));
|
||||
return { ok: true, split: true, line: 'Using the all-cards split engine on 2 cards.', warn: false, reasons: [], cards: 2, written: options.outPath };
|
||||
};
|
||||
/** Before the layer is fetched Control says split + mtp_needed; after it, split with the layer here. */
|
||||
const splitPlan = (_options, state) => state.installs === 0 ? { ...SPLIT_PLAN, mtp_needed: true, mtp_gb: 5.2 } : { ...SPLIT_PLAN, mtp_needed: false, mtp_gb: 0 };
|
||||
|
||||
|
||||
const TUNED_PLAN = { ok: true, split: false, choice: 'dflash2', drafter: 'dflash2', needed: true, tuned_needed: true, tuned: true, gb: 0.34, drafter_gb: 0.34, mtp_needed: false, mtp_gb: 0, line: 'Drafting: DFlash2', serves: true };
|
||||
const READY_PLAN = { ...TUNED_PLAN, needed: false, tuned_needed: false, gb: 0 };
|
||||
const planAfterFetch = (_o, state) => (state.fetchCalls.length ? READY_PLAN : TUNED_PLAN);
|
||||
|
||||
test('a plain load fetches the tuned drafter with the 12 hour rule, then plans again', async () => {
|
||||
const f = fixture({ plan: planAfterFetch });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const job = await finish(f.native, load(f).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
assert.equal(f.state.fetchCalls.length, 1);
|
||||
assert.equal(f.state.fetchCalls[0].mode, 'auto');
|
||||
assert.equal(f.state.fetchCalls[0].throttle, true, 'nobody asked for it: the Terminal 12 hour rule');
|
||||
assert.ok(f.state.planCalls.length >= 2, 'planned again after the fetch');
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
/* CLEAN-1A (owner 11-07): the plain Load follows Auto everywhere. Where Auto's drafter is the standard DFlash2 pack (Intel Arc, UD-Q4_K_XL on NVIDIA 72 GB / 12 GB and the Spark), Control says `needed` without `tuned_needed`; the app fetches it just the same, asking Control. */
|
||||
const STOCK_PLAN = { ...TUNED_PLAN, tuned_needed: false, tuned: false, gb: 1.2, drafter_gb: 1.2 };
|
||||
const stockAfterFetch = (_o, state) => (state.fetchCalls.length ? { ...STOCK_PLAN, needed: false, gb: 0 } : STOCK_PLAN);
|
||||
|
||||
test('a plain load fetches the standard DFlash2 drafter too when that is what Auto runs', async () => {
|
||||
const f = fixture({ plan: stockAfterFetch });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const job = await finish(f.native, load(f).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
assert.equal(f.state.fetchCalls.length, 1);
|
||||
assert.equal(f.state.fetchCalls[0].mode, 'auto');
|
||||
assert.equal(f.state.fetchCalls[0].throttle, true);
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
test('a plain load fetches nothing when Auto needs no download', async () => {
|
||||
const f = fixture({ plan: () => ({ ...STOCK_PLAN, needed: false, gb: 0 }) });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const job = await finish(f.native, load(f).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
assert.equal(f.state.fetchCalls.length, 0);
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
test('the Models screen own Load keeps its un-throttled fetch and does not fetch twice', async () => {
|
||||
const f = fixture({ plan: planAfterFetch });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const job = await finish(f.native, load(f, { values: { drafting: 'auto' } }).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
assert.equal(f.state.fetchCalls.length, 1);
|
||||
assert.ok(f.state.fetchCalls[0].throttle !== true);
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
test('a drafter the person saved wins: MTP, off and DFlash2 are never fetched by a plain load', async () => {
|
||||
for (const drafting of ['mtp', 'off', 'dflash2']) {
|
||||
const f = fixture({ plan: TUNED_PLAN, config: { drafting } });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const job = await finish(f.native, load(f).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
assert.equal(f.state.fetchCalls.length, 0, drafting);
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
}
|
||||
});
|
||||
|
||||
test('nothing needed (the tuned drafter is here, or Control says MTP): a plain load fetches nothing', async () => {
|
||||
const f = fixture({ plan: READY_PLAN });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const job = await finish(f.native, load(f).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
assert.equal(f.state.fetchCalls.length, 0);
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
test('the feed has nothing (skipped): the load goes on exactly as before, no note', async () => {
|
||||
const f = fixture({ plan: TUNED_PLAN, fetchResult: { ok: true, event: 'result', pack: null, skipped: true } });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const job = await finish(f.native, load(f).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
assert.equal(f.state.fetchCalls.length, 1);
|
||||
assert.ok(!job.drafterNote);
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
test('a fetch that fails for a real reason never stops the load and the job says why', async () => {
|
||||
const f = fixture({ plan: TUNED_PLAN, fetchResult: new Error('The tuned DFlash2 drafter did not pass the safety check, so it was deleted.') });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const job = await finish(f.native, load(f).id);
|
||||
assert.equal(job.state, 'done', job.error);
|
||||
assert.match(job.drafterNote ?? '', /safety check/);
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
test('a tuning run fetches it first, and not for a saved MTP', async () => {
|
||||
for (const [drafting, calls] of [[undefined, 1], ['mtp', 0]]) {
|
||||
const f = fixture({ plan: TUNED_PLAN, config: drafting ? { drafting } : {} });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const id = f.native.operation('autotune', { model: 'ud_q4_k_xl' }).id;
|
||||
await finish(f.native, id);
|
||||
assert.equal(f.state.fetchCalls.length, calls, String(drafting));
|
||||
if (calls) assert.equal(f.state.fetchCalls[0].throttle, true);
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
}
|
||||
});
|
||||
|
||||
test('the install plan asks Control about this model and lists the tuned size, and says nothing for MTP', async () => {
|
||||
const f = fixture({ plan: (o) => (o.mode === 'mtp' ? { ...READY_PLAN, choice: 'mtp', drafter: 'mtp' } : TUNED_PLAN) });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
const got = await f.native.plan({ family: 'qwen', model: 'UD-Q4_K_XL', drafting: 'auto' });
|
||||
assert.equal(f.state.planCalls.find(c => c.model)?.model, 'UD-Q4_K_XL');
|
||||
assert.equal(got.drafting.gb, 0.34);
|
||||
assert.equal(got.drafting.tuned, true);
|
||||
const none = await f.native.plan({ family: 'qwen', model: 'UD-Q4_K_XL', drafting: 'mtp' });
|
||||
assert.equal(none.drafting, undefined, 'MTP chosen: no drafter is listed');
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
test('the Settings hint keeps its wording and changes only the number', async () => {
|
||||
const { draftingHint, NATIVE_FIELDS } = await import('../server/native-settings.mjs');
|
||||
assert.equal(NATIVE_FIELDS.find(x => x.id === 'drafting').hint, draftingHint(1.2));
|
||||
assert.equal(draftingHint(0.34), draftingHint(1.2).replace('1.2 GB', '0.34 GB'));
|
||||
assert.match(draftingHint(0.52), /DFlash2 downloads a 0\.52 GB drafter once, the MTP its draft layer once\./);
|
||||
assert.equal(draftingHint(0), draftingHint(1.2), 'no number from Control: the generic hint');
|
||||
const f = fixture({ plan: TUNED_PLAN });
|
||||
try {
|
||||
await f.native.refresh(true);
|
||||
assert.deepEqual(await f.native.settingsHints('ud_q4_k_xl'), { drafting: draftingHint(0.34) });
|
||||
assert.equal(f.state.planCalls.at(-1).mode, 'dflash2');
|
||||
} finally { await f.native.close(); f.cleanup(); }
|
||||
});
|
||||
|
||||
test('control passes the model, the family and --throttle to Control and not --model beside a run config', async () => {
|
||||
const calls = [];
|
||||
const control = createControl({ control: { dir: 'C:/c', python: 'py', cli: 'strata.py' }, run: async (args) => { calls.push(args); return { code: 0, json: { ok: true }, text: '', stderr: '' }; } });
|
||||
await control.draftingPlan({ mode: 'auto', family: 'unsloth', model: 'UD-Q4_K_XL', dataDir: 'C:/d' });
|
||||
assert.deepEqual(calls[0], ['drafting', 'plan', '--mode', 'auto', '--family', 'unsloth', '--model', 'UD-Q4_K_XL', '--data-dir', 'C:/d', '--output', 'json']);
|
||||
await control.draftingPlan({ configPath: 'C:/w/c.json', model: 'UD-Q4_K_XL' });
|
||||
assert.ok(!calls[1].includes('--model'));
|
||||
await control.draftingFetch({ configPath: 'C:/w/c.json', mode: 'auto', throttle: true });
|
||||
assert.deepEqual(calls[2], ['drafting', 'fetch', '--config', 'C:/w/c.json', '--mode', 'auto', '--if-needed', '--throttle', '--output', 'jsonl']);
|
||||
});
|
||||
|
||||
test('a restart of the stopped models fetches it before the start, and so does every path through the one helper', async () => {
|
||||
const { readFileSync: read } = await import('node:fs');
|
||||
const source = read(new URL('../server/native-control.mjs', import.meta.url), 'utf8');
|
||||
const restart = source.slice(source.indexOf('async function restartStopped'), source.indexOf('function dismissRestart'));
|
||||
assert.match(restart, /await autoDrafterFor\(job,instance\.model,[^)]*\);[\s\S]*await control\.start\(instance\.profile/, 'before the engine starts again');
|
||||
const tune = source.slice(source.indexOf('async function autotune'), source.indexOf('function operation'));
|
||||
assert.match(tune, /await autoDrafterFor\(job,args\.model,/);
|
||||
assert.match(source, /if\(args\.values\?\.drafting===undefined&&drafts\?\.needed===true\)\{const got=await fetchAutoDrafter\(/, 'a plain load');
|
||||
assert.match(source, /!autoChoice\(readJson\(configFile\)\.drafting\)\)return false/, 'a saved choice wins');
|
||||
});
|
||||
@@ -95,7 +95,7 @@ test('every sentence of the Drafting choice is in the catalog in all nine langua
|
||||
// the DFlash2 option (df2-<lang>.json): the paragraph under the dropdown, the lines about the card, the two new notes
|
||||
"Drafting: MTP (DFlash2 isn't available on this graphics card)", "There is no internet connection, so the DFlash2 drafter can't be downloaded now.",
|
||||
'On graphics cards like yours, DFlash2 was faster than MTP on some kinds of text and slower on others in our tests.',
|
||||
'On graphics cards like yours, DFlash2 was slower than MTP in our tests.', 'DFlash2 has not been tested on your graphics card yet.', 'SSD: not needed'];
|
||||
'On graphics cards like yours, DFlash2 was slower than MTP in our tests.', 'DFlash2 has not been tested on your graphics card yet.', 'On your Mac, DFlash2 was faster than MTP on this model in our tests.', 'DFlash2 is off by default on this graphics card for now; choose it in Drafting to use it.', 'SSD: not needed'];
|
||||
const slots = t => (t.match(/\{\d+\}/g) ?? []).sort().join();
|
||||
for (const text of sentences) {
|
||||
const id = idOf(text);
|
||||
|
||||
@@ -215,6 +215,8 @@ test('all nine languages carry the dialog\'s words with the same slots', () => {
|
||||
if (l !== 'en') assert.notEqual(catalogs[l][k], catalogs.en[k], `${l} ${k} is translated`);
|
||||
}
|
||||
}
|
||||
// English is in the main catalog (src/lib/i18n.ts); each other language is one file (src/lib/catalog-<lang>.ts) that the app fetches when the language is used.
|
||||
const i18n = src('src/lib/i18n.ts');
|
||||
for (const l of langs) assert.match(i18n, new RegExp(`\\.\\.\\.upd_${l}\\b`));
|
||||
assert.match(i18n, /\.\.\.upd_en\b/);
|
||||
for (const l of langs.filter(x => x !== 'en')) assert.match(src(`src/lib/catalog-${l}.ts`), new RegExp(`from '\\.\\./locales/upd-${l}\\.json'`));
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {resolve} from 'node:path';
|
||||
import {createRequire} from 'node:module';
|
||||
import {build} from 'esbuild';
|
||||
|
||||
/* The family menu rows for the update feed's models: a model this engine cannot use is a note with its reason, not a choice; one that is allowed shows the reason beside its name. */
|
||||
const root=resolve(import.meta.dirname,'..');
|
||||
const virtual={'./ui':"export const Icon=()=>null;",'./FloatingMenu':"export const FloatingMenu=()=>null;"};
|
||||
const stubs={name:'stubs',setup(b){b.onResolve({filter:/^\.\/(ui|FloatingMenu)$/},a=>({path:a.path,namespace:'stub'}));b.onLoad({filter:/.*/,namespace:'stub'},a=>({contents:virtual[a.path],loader:'js'}));}};
|
||||
const out=await build({entryPoints:[resolve(root,'src/components/FamilySwitch.tsx')],bundle:true,write:false,platform:'node',format:'cjs',jsx:'automatic',plugins:[stubs],external:['react','react-dom','react/jsx-runtime'],loader:{'.json':'json'}});
|
||||
const require=createRequire(resolve(root,'package.json'));
|
||||
const mod={exports:{}};new Function('module','exports','require',out.outputFiles[0].text)(mod,mod.exports,require);
|
||||
const {renderToStaticMarkup}=require('react-dom/server');
|
||||
const React=require('react');
|
||||
const {FamilyMenuList}=mod.exports;
|
||||
const fam=(family,title,extra={})=>({family,title,about:'',experimental:false,images:true,sizes:[{id:family+'-x'}],...extra});
|
||||
test('a model the engine cannot use is a note with its reason; an allowed one shows the reason beside its name',()=>{
|
||||
const reason='needs a newer Strata engine: update Strata to use it';
|
||||
const shown=[fam('qwen3.8','Qwen3.8-Flash-Next'),fam('qwen4-test','Qwen4 Test',{feed:true,available:false,unavailable_reason:reason})];
|
||||
const missing=[fam('later-one','Later One',{sizes:[],feed:true,available:false,unavailable_reason:"this Strata version doesn't know this model yet: update Strata to use it"})];
|
||||
const html=renderToStaticMarkup(React.createElement(FamilyMenuList,{shown,missing,current:shown[0],onPick(){}}));
|
||||
assert.equal((html.match(/role="menuitemradio"/g)||[]).length,2);
|
||||
assert.equal((html.match(/role="note"/g)||[]).length,1);
|
||||
assert(html.includes('Qwen4 Test</strong><small>'+reason+'</small>'));
|
||||
assert(html.includes('Later One</strong><small>this Strata version doesn't know this model yet: update Strata to use it</small>'));
|
||||
const plain=renderToStaticMarkup(React.createElement(FamilyMenuList,{shown:[shown[0]],missing:[],current:shown[0],onPick(){}}));
|
||||
assert(!plain.includes('<small>'));assert(!plain.includes('role="note"'));
|
||||
});
|
||||
Loaded 100 of 900 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user