fix(ui/workflows): rework editor layout, free property panel, slim route chunk

This commit is contained in:
matevip 2026-05-13 17:53:18 +08:00
parent 32507d2157
commit 4aad813f2d
12 changed files with 521 additions and 117 deletions

6
.gitignore vendored
View File

@ -29,6 +29,12 @@ nbbuild/
nbdist/
.nb-gradle/
### frontend build artifacts ###
# Vite's primary output goes to mateclaw-server/.../static; the only thing
# that lands here is rollup-plugin-visualizer's stats.html when running
# ANALYZE=1 pnpm build.
mateclaw-ui/dist/
### maven ###
target/
*.war

View File

@ -18,6 +18,11 @@ RUN pnpm install --frozen-lockfile
COPY mateclaw-ui/ ./
# Override outDir: vite.config.ts writes to ../mateclaw-server/…/static which
# is outside this container; call vite directly to control --outDir.
# The earlier escape hatch (NODE_OPTIONS=--max-old-space-size=6144) was
# retired after the workflow route adopted precise Monaco imports + async
# component loading + named manualChunks; the single-chunk peak now sits
# below ~1 GB so the default Node heap is enough. Re-add the flag if a
# future feature reintroduces a 4 MB+ chunk and the build OOMs.
# Skipping vue-tsc here is intentional — type errors are caught in CI, not in
# the production Docker image build.
RUN pnpm exec vite build --outDir /static --emptyOutDir

View File

@ -6,7 +6,7 @@
"description": "MateClaw - Personal AI Assistant Web Console",
"scripts": {
"dev": "vite",
"build": "node --max-old-space-size=6144 ./node_modules/vue-tsc/bin/vue-tsc.js && node --max-old-space-size=6144 ./node_modules/vite/bin/vite.js build",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"lint": "eslint src --ext .ts,.vue --fix"
},
@ -47,6 +47,7 @@
"autoprefixer": "^10.4.20",
"eslint": "^9.18.0",
"eslint-plugin-vue": "^9.32.0",
"rollup-plugin-visualizer": "^7.0.1",
"tailwindcss": "^4.0.6",
"typescript": "~5.7.2",
"vite": "^7.3.1",

View File

@ -111,6 +111,9 @@ importers:
eslint-plugin-vue:
specifier: ^9.32.0
version: 9.33.0(eslint@9.39.4(jiti@2.6.1))
rollup-plugin-visualizer:
specifier: ^7.0.1
version: 7.0.1(rollup@4.60.1)
tailwindcss:
specifier: ^4.0.6
version: 4.2.2
@ -948,10 +951,18 @@ packages:
alien-signals@3.1.2:
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
ansi-regex@6.2.2:
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
engines: {node: '>=12'}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
ansi-styles@6.2.3:
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
engines: {node: '>=12'}
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
@ -993,6 +1004,10 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
bundle-name@4.1.0:
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
engines: {node: '>=18'}
call-bind-apply-helpers@1.0.2:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
@ -1017,6 +1032,10 @@ packages:
resolution: {integrity: sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==}
engines: {node: '>=22.0.0'}
cliui@9.0.1:
resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==}
engines: {node: '>=20'}
color-convert@2.0.1:
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
engines: {node: '>=7.0.0'}
@ -1238,6 +1257,18 @@ packages:
deep-is@0.1.4:
resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
default-browser-id@5.0.1:
resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==}
engines: {node: '>=18'}
default-browser@5.5.0:
resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==}
engines: {node: '>=18'}
define-lazy-prop@3.0.0:
resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==}
engines: {node: '>=12'}
delaunator@5.1.0:
resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==}
@ -1270,6 +1301,9 @@ packages:
peerDependencies:
vue: ^3.3.0
emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
enhanced-resolve@5.20.1:
resolution: {integrity: sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==}
engines: {node: '>=10.13.0'}
@ -1423,6 +1457,14 @@ packages:
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-east-asian-width@1.6.0:
resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==}
engines: {node: '>=18'}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
@ -1505,6 +1547,11 @@ packages:
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
engines: {node: '>=12'}
is-docker@3.0.0:
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
hasBin: true
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
@ -1513,6 +1560,15 @@ packages:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-in-ssh@1.0.0:
resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==}
engines: {node: '>=20'}
is-inside-container@1.0.0:
resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==}
engines: {node: '>=14.16'}
hasBin: true
is-promise@2.2.2:
resolution: {integrity: sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==}
@ -1520,6 +1576,10 @@ packages:
resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==}
engines: {node: '>=18'}
is-wsl@3.1.1:
resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==}
engines: {node: '>=16'}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
@ -1746,6 +1806,10 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
open@11.0.0:
resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==}
engines: {node: '>=20'}
optionator@0.9.4:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
@ -1827,6 +1891,10 @@ packages:
resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==}
engines: {node: ^10 || ^12 || >=14}
powershell-utils@0.1.0:
resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
engines: {node: '>=20'}
prelude-ls@1.2.1:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
@ -1856,6 +1924,19 @@ packages:
robust-predicates@3.0.3:
resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==}
rollup-plugin-visualizer@7.0.1:
resolution: {integrity: sha512-UJUT4+1Ho4OcWmPYU3sYXgUqI8B8Ayfe06MX7y0qCJ1K8aGoKtR/NDd/2nZqM7ADkrzny+I99Ul7GgyoiVNAgg==}
engines: {node: '>=22'}
hasBin: true
peerDependencies:
rolldown: 1.x || ^1.0.0-beta || ^1.0.0-rc
rollup: 2.x || 3.x || 4.x
peerDependenciesMeta:
rolldown:
optional: true
rollup:
optional: true
rollup@4.60.1:
resolution: {integrity: sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
@ -1864,6 +1945,10 @@ packages:
roughjs@4.6.6:
resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==}
run-applescript@7.1.0:
resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
engines: {node: '>=18'}
rw@1.3.3:
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
@ -1887,6 +1972,10 @@ packages:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
source-map@0.7.6:
resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==}
engines: {node: '>= 12'}
speakingurl@14.0.1:
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
engines: {node: '>=0.10.0'}
@ -1894,6 +1983,14 @@ packages:
state-local@1.0.7:
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
string-width@7.2.0:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
strip-ansi@7.2.0:
resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==}
engines: {node: '>=12'}
strip-json-comments@3.1.1:
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
engines: {node: '>=8'}
@ -2080,10 +2177,30 @@ packages:
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
engines: {node: '>=0.10.0'}
wrap-ansi@9.0.2:
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
engines: {node: '>=18'}
wsl-utils@0.3.1:
resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==}
engines: {node: '>=20'}
xml-name-validator@4.0.0:
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
engines: {node: '>=12'}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
yargs-parser@22.0.0:
resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
yargs@18.0.0:
resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==}
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@ -2826,10 +2943,14 @@ snapshots:
alien-signals@3.1.2: {}
ansi-regex@6.2.2: {}
ansi-styles@4.3.0:
dependencies:
color-convert: 2.0.1
ansi-styles@6.2.3: {}
argparse@2.0.1: {}
async-validator@4.2.5: {}
@ -2874,6 +2995,10 @@ snapshots:
node-releases: 2.0.37
update-browserslist-db: 1.2.3(browserslist@4.28.2)
bundle-name@4.1.0:
dependencies:
run-applescript: 7.1.0
call-bind-apply-helpers@1.0.2:
dependencies:
es-errors: 1.3.0
@ -2901,6 +3026,12 @@ snapshots:
'@chevrotain/types': 12.0.0
'@chevrotain/utils': 12.0.0
cliui@9.0.1:
dependencies:
string-width: 7.2.0
strip-ansi: 7.2.0
wrap-ansi: 9.0.2
color-convert@2.0.1:
dependencies:
color-name: 1.1.4
@ -3138,6 +3269,15 @@ snapshots:
deep-is@0.1.4: {}
default-browser-id@5.0.1: {}
default-browser@5.5.0:
dependencies:
bundle-name: 4.1.0
default-browser-id: 5.0.1
define-lazy-prop@3.0.0: {}
delaunator@5.1.0:
dependencies:
robust-predicates: 3.0.3
@ -3188,6 +3328,8 @@ snapshots:
transitivePeerDependencies:
- typescript
emoji-regex@10.6.0: {}
enhanced-resolve@5.20.1:
dependencies:
graceful-fs: 4.2.11
@ -3381,6 +3523,10 @@ snapshots:
function-bind@1.1.2: {}
get-caller-file@2.0.5: {}
get-east-asian-width@1.6.0: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
@ -3454,16 +3600,28 @@ snapshots:
internmap@2.0.3: {}
is-docker@3.0.0: {}
is-extglob@2.1.1: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-in-ssh@1.0.0: {}
is-inside-container@1.0.0:
dependencies:
is-docker: 3.0.0
is-promise@2.2.2: {}
is-what@5.5.0: {}
is-wsl@3.1.1:
dependencies:
is-inside-container: 1.0.0
isexe@2.0.0: {}
jiti@2.6.1: {}
@ -3673,6 +3831,15 @@ snapshots:
dependencies:
boolbase: 1.0.0
open@11.0.0:
dependencies:
default-browser: 5.5.0
define-lazy-prop: 3.0.0
is-in-ssh: 1.0.0
is-inside-container: 1.0.0
powershell-utils: 0.1.0
wsl-utils: 0.3.1
optionator@0.9.4:
dependencies:
deep-is: 0.1.4
@ -3749,6 +3916,8 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
powershell-utils@0.1.0: {}
prelude-ls@1.2.1: {}
promise-worker-transferable@1.0.4:
@ -3768,6 +3937,15 @@ snapshots:
robust-predicates@3.0.3: {}
rollup-plugin-visualizer@7.0.1(rollup@4.60.1):
dependencies:
open: 11.0.0
picomatch: 4.0.4
source-map: 0.7.6
yargs: 18.0.0
optionalDependencies:
rollup: 4.60.1
rollup@4.60.1:
dependencies:
'@types/estree': 1.0.8
@ -3806,6 +3984,8 @@ snapshots:
points-on-curve: 0.2.0
points-on-path: 0.2.1
run-applescript@7.1.0: {}
rw@1.3.3: {}
safer-buffer@2.1.2: {}
@ -3820,10 +4000,22 @@ snapshots:
source-map-js@1.2.1: {}
source-map@0.7.6: {}
speakingurl@14.0.1: {}
state-local@1.0.7: {}
string-width@7.2.0:
dependencies:
emoji-regex: 10.6.0
get-east-asian-width: 1.6.0
strip-ansi: 7.2.0
strip-ansi@7.2.0:
dependencies:
ansi-regex: 6.2.2
strip-json-comments@3.1.1: {}
stylis@4.4.0: {}
@ -3960,8 +4152,32 @@ snapshots:
word-wrap@1.2.5: {}
wrap-ansi@9.0.2:
dependencies:
ansi-styles: 6.2.3
string-width: 7.2.0
strip-ansi: 7.2.0
wsl-utils@0.3.1:
dependencies:
is-wsl: 3.1.1
powershell-utils: 0.1.0
xml-name-validator@4.0.0: {}
y18n@5.0.8: {}
yargs-parser@22.0.0: {}
yargs@18.0.0:
dependencies:
cliui: 9.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
string-width: 7.2.0
y18n: 5.0.8
yargs-parser: 22.0.0
yocto-queue@0.1.0: {}
zrender@6.0.0:

View File

@ -634,6 +634,19 @@ html.dark body::before {
box-shadow: var(--mc-shadow-soft);
}
/* While a workflow canvas is in fullscreen mode, suppress the backdrop
filter on every page frame. A non-`none` backdrop-filter promotes the
element to a containing block for position:fixed descendants, which
pinned the fullscreen canvas to the frame's top-left instead of the
viewport. The frames sit behind the fullscreen overlay anyway, so
the blur has no visible cost during this transient state. */
body.workflow-canvas-fullscreen .mc-page-frame {
backdrop-filter: none;
}
body.workflow-canvas-fullscreen .mc-page-frame::before {
display: none;
}
.mc-page-frame::before {
content: '';
position: absolute;

View File

@ -123,6 +123,11 @@ const props = withDefaults(defineProps<Props>(), { canvasId: 'workflow-canvas' }
const emit = defineEmits<{
(e: 'select-step', payload: StepNodeData | null): void
(e: 'insert-step', payload: { afterIndex: number; modeType: string }): void
/** Toggled by the fullscreen button. The parent uses this to float the
* property inspector above the fullscreen overlay (otherwise the panel,
* which lives as a flex sibling outside this component, gets hidden
* behind the z-index: 2000 fixed canvas). */
(e: 'update:fullscreen', value: boolean): void
}>()
function onAddNode(e: Event) {
@ -238,15 +243,29 @@ watch(fullscreen, (on) => {
document.addEventListener('keydown', handleEsc)
// Lock page scroll behind the overlay.
document.body.style.overflow = 'hidden'
// The settings layout's mc-page-frame uses backdrop-filter, which
// promotes it to a containing block for position:fixed descendants
// (CSS containment spec). That makes our `inset: 0` anchor to the
// frame instead of the viewport, leaving the canvas pinned to the
// top-left of the page rather than truly fullscreen. Suppress the
// filter while fullscreen is active the frames sit behind the
// overlay anyway, so dropping the blur has no visible cost.
document.body.classList.add('workflow-canvas-fullscreen')
} else {
document.removeEventListener('keydown', handleEsc)
document.body.style.overflow = ''
document.body.classList.remove('workflow-canvas-fullscreen')
}
emit('update:fullscreen', on)
})
onBeforeUnmount(() => {
window.removeEventListener('mateclaw:workflow-step-select', handleDomStepSelect as EventListener)
document.removeEventListener('keydown', handleEsc)
document.body.style.overflow = ''
// Unmounting while in fullscreen (route change, panel close) must
// restore the page frame's backdrop blur otherwise we leave the
// rest of the app permanently transparent.
document.body.classList.remove('workflow-canvas-fullscreen')
})
</script>

View File

@ -16,7 +16,12 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, watch } from 'vue'
import { VueMonacoEditor, loader } from '@guolao/vue-monaco-editor'
import * as monaco from 'monaco-editor'
// Precise imports replace `import * as monaco from 'monaco-editor'` the
// barrel pulled in TypeScript / CSS / HTML language workers (~8.7 MB) that
// this JSON editor never touches. The editor.api entry exposes the same
// `monaco` namespace surface we need (registerLanguage, KeyMod, etc.).
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api.js'
import 'monaco-editor/esm/vs/language/json/monaco.contribution.js'
// Vite's ?worker imports Monaco loads its language workers as separate
// JS bundles, and without this MonacoEnvironment shim the editor falls
// back to fetching from a CDN URL ('vs/base/worker/workerMain.js') that

View File

@ -2178,6 +2178,7 @@ export default {
tabs: {
canvas: 'Canvas',
json: 'JSON',
runs: 'Runs ({count})',
},
canvas: {
empty: 'The current draft has no steps. Add one in the JSON editor or pick a template.',

View File

@ -2190,6 +2190,7 @@ export default {
tabs: {
canvas: '画布视图',
json: 'JSON 编辑',
runs: '运行 ({count})',
},
canvas: {
empty: '当前草稿没有任何步骤,先在 JSON 编辑器中添加步骤或选择一个模板。',

View File

@ -43,35 +43,46 @@
</template>
<script setup lang="ts">
import { computed, ref, onMounted, onBeforeUnmount } from 'vue'
import { computed, ref, watch, onMounted, onBeforeUnmount } from 'vue'
import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
const route = useRoute()
const { t } = useI18n()
//
// Routes that benefit from extra editor width the sub-nav auto-collapses
// to a 56px rail unless the user has explicitly toggled it open.
const COMPACT_ROUTES = ['/settings/workflows', '/settings/triggers']
const navCollapsed = ref(localStorage.getItem('mc-settings-nav-collapsed') === 'true')
const userExplicit = ref(localStorage.getItem('mc-settings-nav-collapsed') === 'true')
const userExplicit = ref(localStorage.getItem('mc-settings-nav-collapsed') !== null)
let mediumQuery: MediaQueryList | null = null
function toggleNav() {
navCollapsed.value = !navCollapsed.value
userExplicit.value = navCollapsed.value
userExplicit.value = true
localStorage.setItem('mc-settings-nav-collapsed', String(navCollapsed.value))
}
function handleMediumChange(e: MediaQueryListEvent | MediaQueryList) {
if (e.matches && !userExplicit.value) {
navCollapsed.value = true
} else if (!e.matches && !userExplicit.value) {
navCollapsed.value = false
}
function isCompactRoute(path: string): boolean {
return COMPACT_ROUTES.some((p) => path.startsWith(p))
}
function recomputeAuto() {
if (userExplicit.value) return
const compact = isCompactRoute(route.path) || !!mediumQuery?.matches
navCollapsed.value = compact
}
function handleMediumChange(_e: MediaQueryListEvent | MediaQueryList) {
recomputeAuto()
}
watch(() => route.path, recomputeAuto)
onMounted(() => {
mediumQuery = window.matchMedia('(max-width: 1200px)')
handleMediumChange(mediumQuery)
recomputeAuto()
mediumQuery.addEventListener('change', handleMediumChange)
})

View File

@ -16,7 +16,7 @@
<div class="workflows-grid">
<!-- left: list -->
<aside class="workflows-list mc-surface-card">
<aside class="workflows-list">
<div class="list-header">
<span>{{ t('workflows.defined', { count: workflows.length }) }}</span>
<button class="btn-ghost" @click="reload">{{ t('workflows.refresh') }}</button>
@ -41,7 +41,7 @@
</aside>
<!-- middle: editor -->
<section class="workflows-editor mc-surface-card" v-if="selected">
<section class="workflows-editor" v-if="selected">
<header class="editor-header">
<input v-model="selected.name" class="editor-name" :placeholder="t('workflows.namePlaceholder')" />
<input v-model="selected.description" class="editor-desc" :placeholder="t('workflows.descPlaceholder')" />
@ -61,30 +61,32 @@
<button class="tab-btn" :class="{ active: editorTab === 'json' }" @click="editorTab = 'json'">
{{ t('workflows.tabs.json') }}
</button>
<button class="tab-btn" :class="{ active: editorTab === 'runs' }" @click="onSwitchToRuns">
{{ t('workflows.tabs.runs', { count: runs.length + pausedRuns.length }) }}
</button>
</div>
<div v-if="editorTab === 'canvas'" class="canvas-pane">
<div v-if="editorTab === 'canvas'" class="canvas-pane" :class="{ 'canvas-fullscreen': canvasFullscreen }">
<WorkflowCanvas
v-model="canvasModel"
:canvas-id="`wf-${selected.id}`"
@select-step="onCanvasSelect"
@insert-step="onInsertStep"
>
<template v-if="canvasSelection" #panel>
<StepPropertyPanel
:step="selectedStep"
:index="canvasSelection.index"
:available-agents="availableAgents"
:available-channels="availableChannels"
@patch="onStepPatch"
@duplicate="onStepDuplicate"
@delete="onStepDelete"
/>
</template>
</WorkflowCanvas>
@update:fullscreen="canvasFullscreen = $event"
/>
<StepPropertyPanel
v-if="canvasSelection"
:step="selectedStep"
:index="canvasSelection.index"
:available-agents="availableAgents"
:available-channels="availableChannels"
@patch="onStepPatch"
@duplicate="onStepDuplicate"
@delete="onStepDelete"
/>
</div>
<div v-else class="json-pane">
<div v-else-if="editorTab === 'json'" class="json-pane">
<div class="editor-toolbar">
<label class="template-picker">
<span>{{ t('workflows.templates.label') }}</span>
@ -108,6 +110,81 @@
/>
</div>
<div v-else class="runs-pane">
<section v-if="pausedRuns.length" class="paused-section">
<header class="paused-header">
<span>{{ t('workflows.paused.header', { count: pausedRuns.length }) }}</span>
<button class="btn-ghost" @click="reloadPausedRuns">{{ t('workflows.refresh') }}</button>
</header>
<ul class="paused-list">
<li v-for="entry in pausedRuns" :key="entry.run.id" class="paused-row">
<div class="paused-row-line">
<span class="run-state state-paused">paused</span>
<span class="paused-run-hash">{{ t('workflows.paused.runHash', { id: entry.run.id }) }}</span>
</div>
<div class="paused-meta" v-if="entry.pause">
<div><span>{{ t('workflows.paused.pauseTokenLabel') }}:</span>
<code class="pause-token">{{ truncateToken(entry.pause.pauseToken) }}</code></div>
<div v-if="entry.pause.pausedAt">
<span>{{ t('workflows.paused.pausedAtLabel') }}:</span> {{ formatTime(entry.pause.pausedAt) }}
</div>
<div v-if="entry.pause.resumeDeadline">
<span>{{ t('workflows.paused.deadlineLabel') }}:</span> {{ formatTime(entry.pause.resumeDeadline) }}
</div>
</div>
<div class="paused-actions" v-if="entry.pause">
<button class="resume-btn approve" :disabled="resumingId === entry.run.id"
@click="onResume(entry, 'approved')">
{{ t('workflows.paused.resumeApproved') }}
</button>
<button class="resume-btn reject" :disabled="resumingId === entry.run.id"
@click="onResume(entry, 'rejected')">
{{ t('workflows.paused.resumeRejected') }}
</button>
<button class="resume-btn neutral" :disabled="resumingId === entry.run.id"
@click="onResume(entry, 'timeout')">
{{ t('workflows.paused.resumeTimeout') }}
</button>
<button class="resume-btn neutral" :disabled="resumingId === entry.run.id"
@click="onResume(entry, 'cancelled')">
{{ t('workflows.paused.resumeCancelled') }}
</button>
</div>
</li>
</ul>
</section>
<header class="runs-header">
<span>{{ t('workflows.runs.header', { count: runs.length }) }}</span>
<button class="btn-ghost" @click="reloadRuns">{{ t('workflows.refresh') }}</button>
</header>
<ul class="runs-list">
<li v-for="run in runs" :key="run.id" class="run-row" @click="loadRun(run.id)">
<div class="run-row-line">
<span class="run-state" :class="'state-' + run.state">{{ run.state }}</span>
<span class="run-time">{{ formatTime(run.startedAt) }}</span>
</div>
<div class="run-row-meta">
<span>{{ t('workflows.runs.runHash', { id: run.id }) }}</span>
<span v-if="run.triggeredBy">· {{ run.triggeredBy }}</span>
<span v-if="run.errorMessage" class="run-err">· {{ run.errorMessage }}</span>
</div>
</li>
<li v-if="!runs.length" class="runs-empty">{{ t('workflows.runs.empty') }}</li>
</ul>
<section v-if="runDetail" class="run-detail">
<div class="run-detail-title">{{ t('workflows.runs.detailTitle', { id: runDetail.run.id, state: runDetail.run.state }) }}</div>
<ol class="run-steps">
<li v-for="step in runDetail.steps" :key="step.id">
<span class="step-state" :class="'state-' + step.state">{{ step.state }}</span>
<span class="step-name">{{ step.stepName || t('workflows.unnamed') }}</span>
<span v-if="step.durationMs != null" class="step-duration">{{ step.durationMs }} ms</span>
<span v-if="step.errorMessage" class="step-err">{{ step.errorMessage }}</span>
</li>
</ol>
</section>
</div>
<div v-if="compileErrors.length" class="errors-panel">
<div class="errors-title">{{ t('workflows.compileErrorsTitle', { count: compileErrors.length }) }}</div>
<ul>
@ -123,85 +200,9 @@
</div>
</section>
<section class="workflows-empty mc-surface-card" v-else>
<section class="workflows-empty" v-else>
<p>{{ t('workflows.selectHint') }}</p>
</section>
<!-- right: runs -->
<aside class="workflows-runs mc-surface-card" v-if="selected">
<section v-if="pausedRuns.length" class="paused-section">
<header class="paused-header">
<span>{{ t('workflows.paused.header', { count: pausedRuns.length }) }}</span>
<button class="btn-ghost" @click="reloadPausedRuns">{{ t('workflows.refresh') }}</button>
</header>
<ul class="paused-list">
<li v-for="entry in pausedRuns" :key="entry.run.id" class="paused-row">
<div class="paused-row-line">
<span class="run-state state-paused">paused</span>
<span class="paused-run-hash">{{ t('workflows.paused.runHash', { id: entry.run.id }) }}</span>
</div>
<div class="paused-meta" v-if="entry.pause">
<div><span>{{ t('workflows.paused.pauseTokenLabel') }}:</span>
<code class="pause-token">{{ truncateToken(entry.pause.pauseToken) }}</code></div>
<div v-if="entry.pause.pausedAt">
<span>{{ t('workflows.paused.pausedAtLabel') }}:</span> {{ formatTime(entry.pause.pausedAt) }}
</div>
<div v-if="entry.pause.resumeDeadline">
<span>{{ t('workflows.paused.deadlineLabel') }}:</span> {{ formatTime(entry.pause.resumeDeadline) }}
</div>
</div>
<div class="paused-actions" v-if="entry.pause">
<button class="resume-btn approve" :disabled="resumingId === entry.run.id"
@click="onResume(entry, 'approved')">
{{ t('workflows.paused.resumeApproved') }}
</button>
<button class="resume-btn reject" :disabled="resumingId === entry.run.id"
@click="onResume(entry, 'rejected')">
{{ t('workflows.paused.resumeRejected') }}
</button>
<button class="resume-btn neutral" :disabled="resumingId === entry.run.id"
@click="onResume(entry, 'timeout')">
{{ t('workflows.paused.resumeTimeout') }}
</button>
<button class="resume-btn neutral" :disabled="resumingId === entry.run.id"
@click="onResume(entry, 'cancelled')">
{{ t('workflows.paused.resumeCancelled') }}
</button>
</div>
</li>
</ul>
</section>
<header class="runs-header">
<span>{{ t('workflows.runs.header', { count: runs.length }) }}</span>
<button class="btn-ghost" @click="reloadRuns">{{ t('workflows.refresh') }}</button>
</header>
<ul class="runs-list">
<li v-for="run in runs" :key="run.id" class="run-row" @click="loadRun(run.id)">
<div class="run-row-line">
<span class="run-state" :class="'state-' + run.state">{{ run.state }}</span>
<span class="run-time">{{ formatTime(run.startedAt) }}</span>
</div>
<div class="run-row-meta">
<span>{{ t('workflows.runs.runHash', { id: run.id }) }}</span>
<span v-if="run.triggeredBy">· {{ run.triggeredBy }}</span>
<span v-if="run.errorMessage" class="run-err">· {{ run.errorMessage }}</span>
</div>
</li>
<li v-if="!runs.length" class="runs-empty">{{ t('workflows.runs.empty') }}</li>
</ul>
<section v-if="runDetail" class="run-detail">
<div class="run-detail-title">{{ t('workflows.runs.detailTitle', { id: runDetail.run.id, state: runDetail.run.state }) }}</div>
<ol class="run-steps">
<li v-for="step in runDetail.steps" :key="step.id">
<span class="step-state" :class="'state-' + step.state">{{ step.state }}</span>
<span class="step-name">{{ step.stepName || t('workflows.unnamed') }}</span>
<span v-if="step.durationMs != null" class="step-duration">{{ step.durationMs }} ms</span>
<span v-if="step.errorMessage" class="step-err">{{ step.errorMessage }}</span>
</li>
</ol>
</section>
</aside>
</div>
</div>
</div>
@ -213,7 +214,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'
import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { mcConfirm } from '@/components/common/useConfirm'
import { ElMessage } from 'element-plus'
@ -234,9 +235,18 @@ import {
} from '@/api'
import type { Channel } from '@/types'
import { useWorkspaceStore } from '@/stores/useWorkspaceStore'
import WorkflowCanvas from '@/components/workflow/WorkflowCanvas.vue'
// Lazy-load the canvas + Monaco editor each pulls a heavy vendor chunk
// (@vue-flow/* and monaco-editor respectively). Keeping them out of the
// route's main chunk means /settings/workflows starts as ~500 KB instead
// of 4 MB, and the Docker production build no longer needs the
// --max-old-space-size=6144 escape hatch.
const WorkflowCanvas = defineAsyncComponent(
() => import('@/components/workflow/WorkflowCanvas.vue'),
)
const WorkflowJsonEditor = defineAsyncComponent(
() => import('@/components/workflow/WorkflowJsonEditor.vue'),
)
import StepPropertyPanel from '@/components/workflow/StepPropertyPanel.vue'
import WorkflowJsonEditor from '@/components/workflow/WorkflowJsonEditor.vue'
import CreateWorkflowDialog from '@/components/workflow/CreateWorkflowDialog.vue'
import PublishDialog from '@/components/workflow/PublishDialog.vue'
import GenerateWorkflowDialog from '@/components/workflow/GenerateWorkflowDialog.vue'
@ -285,7 +295,13 @@ const templateChoice = ref('')
// the JSON) and the raw JSON editor. Canvas is the default most
// authors visit the page to make sense of an existing flow rather
// than to type fresh JSON.
const editorTab = ref<'canvas' | 'json'>('canvas')
const editorTab = ref<'canvas' | 'json' | 'runs'>('canvas')
async function onSwitchToRuns() {
editorTab.value = 'runs'
await reloadRuns()
await reloadPausedRuns()
}
// The canvas reads from `draftJson` directly. We keep the model write
// path on the JSON editor only the canvas is purely a derived view
@ -296,6 +312,12 @@ const canvasModel = computed({
})
const canvasSelection = ref<StepNodeData | null>(null)
// Mirrors the WorkflowCanvas's internal fullscreen flag so we can float
// the property inspector above the fixed-position overlay. Without this,
// clicking a node in fullscreen would emit a selection but the panel
// rendered as a flex sibling outside the canvas sits underneath the
// z-index: 2000 overlay and reads as "panel doesn't open".
const canvasFullscreen = ref(false)
function onCanvasSelect(payload: StepNodeData | null) {
canvasSelection.value = payload
}
@ -830,21 +852,53 @@ watch(workspaceId, async () => {
display: flex;
flex-direction: column;
gap: 16px;
min-width: 0;
min-height: 0;
flex: 1;
}
.workflows-page :deep(.mc-page-header) {
align-items: center;
margin-bottom: 18px;
flex-shrink: 0;
}
.workflows-page :deep(.header-actions) {
display: flex;
gap: 8px;
flex-shrink: 0;
align-self: center;
}
.workflows-grid {
display: grid;
grid-template-columns: 280px 1fr 320px;
grid-template-columns: minmax(220px, 260px) minmax(0, 1fr);
gap: 16px;
align-items: stretch;
min-height: 480px;
flex: 1;
min-height: 0;
min-width: 0;
}
.workflows-list,
.workflows-editor,
.workflows-runs,
.workflows-empty {
padding: 12px;
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
border: 1px solid var(--mc-border-light, rgba(0, 0, 0, 0.06));
border-radius: 10px;
background: var(--mc-bg-elevated, rgba(255, 255, 255, 0.4));
}
.workflows-list {
/* Slightly recessed so the editor reads as primary surface. */
background: var(--mc-bg-sunken, rgba(0, 0, 0, 0.02));
}
.runs-pane {
flex: 1;
display: flex;
flex-direction: column;
gap: 10px;
min-height: 360px;
overflow-y: auto;
}
.list-header,
.runs-header {
@ -1146,7 +1200,7 @@ button:disabled {
display: flex;
flex-direction: row;
gap: 12px;
min-height: 420px;
min-height: 320px;
align-items: stretch;
}
.canvas-pane > .workflow-canvas {
@ -1166,6 +1220,40 @@ button:disabled {
max-height: 360px;
}
}
/* Fullscreen mode: the canvas itself becomes a fixed overlay
(z-index: 2000). Float the panel above it so the same flex sibling
stays interactive the previous behaviour where the panel slipped
underneath the overlay made it look like the property panel had
broken when fullscreen was on.
bottom: 180px reserves room for vue-flow's MiniMap (~150px tall) at
the bottom-right corner. Without it, the panel covers the minimap
and the user loses overview navigation. The panel scrolls internally
when content exceeds the shortened height. */
.canvas-pane.canvas-fullscreen {
position: static;
}
.canvas-pane.canvas-fullscreen > .step-panel {
position: fixed;
top: 56px;
right: 14px;
bottom: 180px;
width: min(360px, 30vw);
max-height: none;
z-index: 2001;
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.22);
overflow-y: auto;
}
@media (max-width: 760px) {
.canvas-pane.canvas-fullscreen > .step-panel {
top: auto;
left: 10px;
right: 10px;
bottom: 10px;
width: auto;
height: min(56vh, 420px);
}
}
.canvas-inspector {
background: var(--mc-bg-elevated, rgba(0, 0, 0, 0.02));
border: 1px solid var(--mc-border, rgba(0, 0, 0, 0.08));
@ -1221,7 +1309,7 @@ button:disabled {
display: flex;
flex-direction: column;
gap: 6px;
min-height: 360px;
min-height: 280px;
}
.paused-section {
border-bottom: 1px dashed var(--mc-border-light, rgba(0, 0, 0, 0.08));
@ -1326,7 +1414,7 @@ button:disabled {
can breathe on tablets, and stack everything vertically below 720px
so the page is usable on a phone. The runs panel becomes a
collapsible details element on small screens. */
@media (max-width: 1100px) {
@media (max-width: 1300px) {
.workflows-grid {
grid-template-columns: 1fr;
}

View File

@ -1,6 +1,7 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
import { visualizer } from 'rollup-plugin-visualizer'
import { resolve } from 'path'
export default defineConfig({
@ -17,6 +18,15 @@ export default defineConfig({
},
}),
tailwindcss(),
// ANALYZE=1 pnpm build writes dist/stats.html. Skipped on normal builds so
// CI artifact upload is opt-in and the Docker image build doesn't waste
// memory generating an HTML report it never reads.
process.env.ANALYZE && visualizer({
filename: 'dist/stats.html',
open: false,
gzipSize: true,
brotliSize: true,
}),
],
resolve: {
alias: {
@ -51,5 +61,33 @@ export default defineConfig({
build: {
outDir: '../mateclaw-server/src/main/resources/static',
emptyOutDir: true,
// Cap warning so a future barrel import (see history with monaco) trips
// the build log instead of slipping in silently.
chunkSizeWarningLimit: 1024,
rollupOptions: {
output: {
// Pin heavy vendor libs to named chunks. Two reasons:
// 1. Cache: bumping a business chunk doesn't invalidate the
// monaco / vue-flow bundles, so returning visitors only
// re-download the few KB that actually changed.
// 2. Rollup minify peak heap: keeping monaco out of the route
// chunk lets each Worker on the chunk run with a smaller
// working set, which is what blew up the Docker build at
// 1.3.0 (needed --max-old-space-size=6144 as a band-aid).
manualChunks: {
'vendor-monaco': ['monaco-editor', '@guolao/vue-monaco-editor'],
'vendor-vue-flow': [
'@vue-flow/core',
'@vue-flow/background',
'@vue-flow/controls',
'@vue-flow/minimap',
],
'vendor-mermaid': ['mermaid'],
'vendor-echarts': ['echarts'],
'vendor-element': ['element-plus', '@element-plus/icons-vue'],
'vendor-markdown': ['marked', 'marked-highlight', 'highlight.js', 'dompurify'],
},
},
},
},
})