mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-09 10:09:47 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
614b45c354 | ||
|
|
d09a7c557c | ||
|
|
c16b08bc40 | ||
|
|
513d19db20 | ||
|
|
f396db7c20 | ||
|
|
49e64b6d13 | ||
|
|
bccfb5d684 | ||
|
|
bd898c3f30 | ||
|
|
29230c68bf | ||
|
|
c12faae30a | ||
|
|
44e3a19f8d | ||
|
|
1738c499df | ||
|
|
9ee11c6b1f | ||
|
|
3af45c0b53 | ||
|
|
643bbad2b3 | ||
|
|
5a1dcb19d2 | ||
|
|
564177d7c2 | ||
|
|
a89b729586 | ||
|
|
6844599056 | ||
|
|
d38c8b99ad | ||
|
|
10b92ed99c | ||
|
|
2738768c4d | ||
|
|
4684e5d7b2 | ||
|
|
06fae21f08 | ||
|
|
d5ee56c1b7 | ||
|
|
45e7f52d70 | ||
|
|
ccb39feeb5 | ||
|
|
f75b0624a6 | ||
|
|
7b783209fd | ||
|
|
03943d8a84 | ||
|
|
3db2fc12a6 |
No files matched your search
@@ -114,7 +114,7 @@ jobs:
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
# pnpm first: setup-node's pnpm cache needs the executable to exist.
|
||||
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v4
|
||||
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v4
|
||||
with:
|
||||
version: 10
|
||||
|
||||
@@ -124,7 +124,7 @@ jobs:
|
||||
cache: pnpm
|
||||
cache-dependency-path: desktop/frontend-next/pnpm-lock.yaml
|
||||
|
||||
- uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
|
||||
- uses: actions/setup-java@77dd5b2ce225ea11469928ac317850245a74ae78 # v6.2.0
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "21"
|
||||
|
||||
@@ -43,20 +43,20 @@ jobs:
|
||||
if: matrix.branch != ''
|
||||
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
uses: github/codeql-action/init@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.language == 'go' && 'autobuild' || 'none' }}
|
||||
- name: Perform CodeQL Analysis
|
||||
if: matrix.branch == ''
|
||||
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
uses: github/codeql-action/analyze@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
# Results from a checked-out line are filed under that line's ref, not the
|
||||
# default branch the schedule happened to run from.
|
||||
- name: Perform CodeQL Analysis (${{ matrix.branch }})
|
||||
if: matrix.branch != ''
|
||||
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
|
||||
uses: github/codeql-action/analyze@33d7b85da1a04171e211befe2c739303b9a4c066 # v4.39.3
|
||||
with:
|
||||
category: "/language:${{ matrix.language }}"
|
||||
ref: refs/heads/${{ matrix.branch }}
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
|
||||
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
@@ -608,7 +608,7 @@ jobs:
|
||||
ref: ${{ needs.resolve.outputs.sha }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
@@ -911,7 +911,7 @@ jobs:
|
||||
# The page every archive carries beside the executable, built once here
|
||||
# rather than once per target: serve.FindPage looks for
|
||||
# frontend-next/dist next to the binary.
|
||||
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
cache: true
|
||||
cache-dependency-path: go.sum
|
||||
|
||||
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
|
||||
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
@@ -108,7 +108,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- if: steps.spa.outputs.run == 'true'
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
@@ -143,6 +143,7 @@ jobs:
|
||||
node perf/hoverradius.mjs
|
||||
node perf/jump-ends.mjs
|
||||
node perf/modelrows.mjs
|
||||
node perf/model-menu-badge.mjs
|
||||
node perf/explorersize.mjs
|
||||
node perf/git-panel.mjs
|
||||
node perf/session-move-to.mjs
|
||||
@@ -157,9 +158,17 @@ jobs:
|
||||
node perf/codeeditor-size.mjs
|
||||
node perf/composer-reflow.mjs
|
||||
node perf/composer-toolbar-row.mjs
|
||||
node perf/control-icons.mjs
|
||||
node perf/nav-selection.mjs
|
||||
node perf/action-icons.mjs
|
||||
node perf/rowhit.mjs
|
||||
node perf/prefs-wide-centered.mjs
|
||||
node perf/settings-fullscreen.mjs
|
||||
node perf/settings-page-column.mjs
|
||||
node perf/sidebar-tools.mjs
|
||||
node perf/select-alignment.mjs
|
||||
node perf/provider-fields.mjs
|
||||
node perf/thought-clock.mjs
|
||||
node perf/tab-pip.mjs
|
||||
node perf/syntax-tokens.mjs
|
||||
node perf/tab-close-inside.mjs
|
||||
@@ -217,6 +226,21 @@ jobs:
|
||||
node perf/narrow-rail.mjs
|
||||
node perf/selfcheck.mjs branch-tooltip
|
||||
|
||||
- name: Keep UI control previews
|
||||
if: always() && steps.spa.outputs.run == 'true'
|
||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||
with:
|
||||
name: studio-ui-control-previews
|
||||
path: |
|
||||
desktop/frontend-next/perf/shots/control-icons-*.png
|
||||
desktop/frontend-next/perf/shots/nav-selection-*.png
|
||||
desktop/frontend-next/perf/shots/settings-full-*.png
|
||||
desktop/frontend-next/perf/shots/sidebar-tools-*.png
|
||||
desktop/frontend-next/perf/shots/select-alignment-*.png
|
||||
desktop/frontend-next/perf/shots/provider-fields-*.png
|
||||
desktop/frontend-next/perf/shots/thought-clock-*.png
|
||||
retention-days: 7
|
||||
|
||||
# What a hosted page does over the relay, end to end: the production bundle,
|
||||
# the real Studio host and kernel, a real Chromium; only the accounts API and
|
||||
# the relay are stand-ins (tools/relayfake). Event-transport changes are
|
||||
@@ -250,7 +274,7 @@ jobs:
|
||||
cache: true
|
||||
|
||||
- if: steps.scope.outputs.run == 'true'
|
||||
uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
@@ -435,7 +459,7 @@ jobs:
|
||||
cache: true
|
||||
cache-dependency-path: desktop/go.sum
|
||||
|
||||
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
- uses: pnpm/action-setup@73e46992e05b19abd31f2f3c20e0e7370d762115 # v6.1.1
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"launcher-live": "node test/launcher_live.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "45.1.0",
|
||||
"electron": "45.2.3",
|
||||
"electron-builder": "^26.18.0",
|
||||
"playwright": "1.64.1"
|
||||
},
|
||||
|
||||
Generated
+5
-5
@@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
devDependencies:
|
||||
electron:
|
||||
specifier: 45.1.0
|
||||
version: 45.1.0([email protected])
|
||||
specifier: 45.2.3
|
||||
version: 45.2.3([email protected])
|
||||
electron-builder:
|
||||
specifier: ^26.18.0
|
||||
version: 26.18.0([email protected])([email protected])
|
||||
@@ -374,8 +374,8 @@ packages:
|
||||
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
electron@45.1.0:
|
||||
resolution: {integrity: sha512-i7qKVAh/KR5v1pf56NhGGX2QYHcdjvLgS7koL6ui50c+zUDGLRunR95nXcTpHPURKOQ4GDBG8BmXiDgOQ8DD0A==}
|
||||
electron@45.2.3:
|
||||
resolution: {integrity: sha512-Vurzu5UOA5nTGT4LqIwXD16QFGrOZdiXxF+1UP4KNsAOo/YtDcT3/u0jhP4WSwNJ0iLrLDi85hzBJ0arnGDhyw==}
|
||||
engines: {node: '>= 22.12.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -1551,7 +1551,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron@45.1.0([email protected]):
|
||||
electron@45.2.3([email protected]):
|
||||
dependencies:
|
||||
'@electron-internal/extract-zip': 1.0.5
|
||||
'@electron/get': 5.1.0([email protected])
|
||||
|
||||
@@ -28,7 +28,7 @@ class HostStartError extends Error {
|
||||
function start(binary, args, { onStderr, onExit, onAct, onSlow, systemLanguage, timeoutMs = HANDSHAKE_TIMEOUT_MS } = {}) {
|
||||
const env = { ...process.env };
|
||||
if (systemLanguage) env.REASONIX_SYSTEM_LANG = systemLanguage;
|
||||
const child = spawn(binary, args, { stdio: ["pipe", "pipe", "pipe"], env });
|
||||
const child = spawn(binary, args, { stdio: ["pipe", "pipe", "pipe"], env, windowsHide: true });
|
||||
child.stderr.setEncoding("utf8");
|
||||
if (onStderr) child.stderr.on("data", onStderr);
|
||||
if (onExit) child.on("exit", onExit);
|
||||
|
||||
@@ -6,9 +6,9 @@ const { execFileSync } = require("node:child_process");
|
||||
// one Studio, canonicalized where it lives. The kernel answers it, because the
|
||||
// home is resolved from an environment and a platform default this process
|
||||
// does not own.
|
||||
function instanceID(binary) {
|
||||
function instanceID(binary, run = execFileSync) {
|
||||
try {
|
||||
return execFileSync(binary, ["-instance-id"], { encoding: "utf8", timeout: 10000 }).trim();
|
||||
return run(binary, ["-instance-id"], { encoding: "utf8", timeout: 10000, windowsHide: true }).trim();
|
||||
} catch {
|
||||
// A kernel that cannot be run at all is a launch that is going to fail with
|
||||
// a better message in a moment. Refusing to start here would report it as
|
||||
|
||||
@@ -625,7 +625,7 @@ test("Windows is given the format it reads the small sizes from", () => {
|
||||
assert.equal(iconFile("linux"), "icon.png");
|
||||
});
|
||||
|
||||
const { profileFor } = require("../src/instance.js");
|
||||
const { profileFor, instanceID } = require("../src/instance.js");
|
||||
|
||||
// Two homes are two Studios, and the profile is what carries that into the
|
||||
// platform's own lock. Sharing one would make the second launch look like a
|
||||
@@ -641,6 +641,14 @@ test("each instance gets a profile of its own", () => {
|
||||
assert.equal(profileFor(base, ""), base);
|
||||
});
|
||||
|
||||
test("the instance probe runs the kernel without a console window of its own", () => {
|
||||
let options;
|
||||
const run = (binary, args, opts) => { options = { binary, args, opts }; return "abc123\n"; };
|
||||
assert.equal(instanceID("host", run), "abc123");
|
||||
assert.deepEqual(options.args, ["-instance-id"]);
|
||||
assert.equal(options.opts.windowsHide, true);
|
||||
});
|
||||
|
||||
// A program that cannot be started emits "error" and never "exit". Waiting for
|
||||
// the handshake instead spent the full timeout and then blamed the kernel for
|
||||
// saying nothing, which is where the launch failure on Windows hid: go build
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
"census:verify": "node tools/ui-census/verify.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@a2ui/react": "0.14.0",
|
||||
"@a2ui/web_core": "0.14.0",
|
||||
"@a2ui/react": "0.15.0",
|
||||
"@a2ui/web_core": "0.15.0",
|
||||
"@codemirror/commands": "^6.11.1",
|
||||
"@codemirror/lang-css": "^6.3.1",
|
||||
"@codemirror/lang-go": "^6.0.1",
|
||||
@@ -32,9 +32,9 @@
|
||||
"@codemirror/state": "^6.7.6",
|
||||
"@codemirror/view": "^6.43.15",
|
||||
"@lezer/highlight": "^1.2.5",
|
||||
"@mantine/charts": "9.7.4",
|
||||
"@mantine/core": "9.7.4",
|
||||
"@mantine/hooks": "9.7.4",
|
||||
"@mantine/charts": "9.7.5",
|
||||
"@mantine/core": "9.7.5",
|
||||
"@mantine/hooks": "9.7.5",
|
||||
"@noble/curves": "2.4.0",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
@@ -44,10 +44,10 @@
|
||||
"dompurify": "^3.4.17",
|
||||
"exceljs": "4.4.0",
|
||||
"katex": "^0.19.1",
|
||||
"lucide-react": "^1.64.0",
|
||||
"lucide-react": "^1.69.0",
|
||||
"maplibre-gl": "6.16.0",
|
||||
"papaparse": "^5.7.0",
|
||||
"pdfjs-dist": "^6.4.299",
|
||||
"pdfjs-dist": "^6.5.438",
|
||||
"react": "^19.3.1",
|
||||
"react-dom": "^19.3.1",
|
||||
"react-map-gl": "8.1.3",
|
||||
@@ -64,18 +64,18 @@
|
||||
"zod": "3.25.76"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/parser": "^8.0.8",
|
||||
"@babel/parser": "^8.1.0",
|
||||
"@testing-library/react": "^16.3.3",
|
||||
"@testing-library/user-event": "^14.6.8",
|
||||
"@types/papaparse": "^5.5.2",
|
||||
"@types/react": "^19.3.0",
|
||||
"@types/react-dom": "^19.3.0",
|
||||
"@vitejs/plugin-react": "^6.1.2",
|
||||
"@vitejs/plugin-react": "^6.2.1",
|
||||
"jsdom": "^30.1.3",
|
||||
"jszip": "3.10.2",
|
||||
"playwright": "^1.64.1",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "^8.3.5",
|
||||
"vitest": "^5.0.5"
|
||||
"vite": "^8.3.6",
|
||||
"vitest": "^5.1.0"
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,12 @@ node perf/queue.mjs # 待送达:正文读不回来时,「改」不许
|
||||
node perf/reask.mjs # 改写框:没有换行的长段落,框要长到让人读得到全文,宽屏和手机宽度各一遍
|
||||
node perf/composer-reflow.mjs # Draft and skill-chip wrapping during column transitions, interface zoom and native scrollbar gutters
|
||||
node perf/composer-toolbar-row.mjs # The buttons under the composer stay on one row from 900 to 400 px of conversation width, with the widest realistic labels
|
||||
node perf/control-icons.mjs # Composer and browser action icons: readable geometry, alignment, running states, focus and labels
|
||||
node perf/nav-selection.mjs # Navigation selection: quiet theme surfaces, icon contrast and keyboard focus in default, sky and photo packs
|
||||
node perf/select-alignment.mjs # Selects: rendered text alignment and keyboard dismissal in both languages, schemes, widths and font sizes
|
||||
node perf/settings-fullscreen.mjs # All settings categories fill the window with fixed gutters, single-column cards and contained controls at wide/phone widths and enlarged text
|
||||
node perf/settings-page-column.mjs # Pill navigation scrolls to expanded controls without replacing editors or hiding them under the sticky index
|
||||
node perf/sidebar-tools.mjs # Named footer card: saved order and visibility, hover transfer, click, keyboard focus and viewport containment
|
||||
node perf/workbench-save.mjs # File toolbar sizing, Save contrast and edit/save/read states: light/dark × Chinese/English × desktop/browser × wide/phone
|
||||
node perf/onboarding.mjs # First-run setup: zoom-aware card bounds, resize, scrolling, focus and completion
|
||||
node perf/running.mjs # 执行过程:在跑的步骤自己在动、收起后由标题接着动,步骤行没有空栏
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const base = new URL(process.env.PERF_URL ?? "http://localhost:4399/perf.html");
|
||||
const browser = await chromium.launch();
|
||||
const measure = (buttons) => buttons.map((button) => {
|
||||
const icon = button.querySelector("svg");
|
||||
if (!icon) return { missing: true };
|
||||
const box = button.getBoundingClientRect();
|
||||
const svg = icon.getBoundingClientRect();
|
||||
const glyph = icon.getBBox();
|
||||
const unit = svg.width / icon.viewBox.baseVal.width;
|
||||
return {
|
||||
missing: false,
|
||||
glyphWidth: glyph.width * unit,
|
||||
glyphHeight: glyph.height * unit,
|
||||
offsetX: Math.abs(svg.x + svg.width / 2 - box.x - box.width / 2),
|
||||
offsetY: Math.abs(svg.y + svg.height / 2 - box.y - box.height / 2),
|
||||
fits: svg.left >= box.left && svg.right <= box.right && svg.top >= box.top && svg.bottom <= box.bottom,
|
||||
accessible: !!button.getAttribute("aria-label") && icon.getAttribute("aria-hidden") === "true",
|
||||
};
|
||||
});
|
||||
const check = (sizes, label, centered = true) => {
|
||||
assert.ok(sizes.length > 0, `${label}: controls exist`);
|
||||
for (const size of sizes) {
|
||||
assert.ok(!size.missing && size.accessible && size.fits, `${label}: named SVG controls fit (${JSON.stringify(size)})`);
|
||||
assert.ok(size.glyphWidth >= 9 && size.glyphHeight >= 9, `${label}: symbols remain legible (${JSON.stringify(size)})`);
|
||||
if (centered) assert.ok(size.offsetX <= 1 && size.offsetY <= 1, `${label}: symbols are centered (${JSON.stringify(size)})`);
|
||||
}
|
||||
};
|
||||
try {
|
||||
await mkdir("perf/shots", { recursive: true });
|
||||
for (const colorScheme of ["light", "dark"]) for (const width of [1300, 390]) for (const scale of [1, 1.3]) {
|
||||
const page = await browser.newPage({ viewport: { width, height: 900 }, colorScheme, reducedMotion: "reduce" });
|
||||
const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
await page.addInitScript(() => localStorage.setItem("rx-lang", "zh"));
|
||||
const url = new URL(base);
|
||||
url.searchParams.set("running", "1");
|
||||
await page.goto(url.href, { waitUntil: "networkidle" });
|
||||
await page.locator('.compose [data-action="session.stop"]').waitFor();
|
||||
await page.evaluate((value) => document.documentElement.style.setProperty("--fs-scale", String(value)), scale);
|
||||
const actions = page.locator(".compose .go");
|
||||
const send = actions.locator('[data-action="session.send"]');
|
||||
const stop = actions.locator('[data-action="session.stop"]');
|
||||
const label = `${colorScheme}/${width}/${scale}`;
|
||||
assert.equal(await send.isDisabled(), true, `${label}: an empty running draft cannot send`);
|
||||
const disabledBackground = await send.evaluate((button) => getComputedStyle(button).backgroundColor);
|
||||
check(await actions.locator("button").evaluateAll(measure), label);
|
||||
await actions.screenshot({ path: `perf/shots/control-icons-${colorScheme}-${width}-${scale}.png` });
|
||||
await page.locator(".compose textarea").fill("继续检查");
|
||||
assert.equal(await send.isEnabled(), true, `${label}: a running draft can steer`);
|
||||
await send.evaluate(async (button) => {
|
||||
getComputedStyle(button).backgroundColor;
|
||||
await Promise.all(button.getAnimations().map((animation) => animation.finished.catch(() => {})));
|
||||
});
|
||||
assert.notEqual(await send.evaluate((button) => getComputedStyle(button).backgroundColor), disabledBackground, `${label}: a disabled send looks inactive`);
|
||||
check(await send.evaluateAll(measure), `${label}/steer`, false);
|
||||
await stop.focus();
|
||||
assert.ok(await stop.evaluate((button) => parseFloat(getComputedStyle(button).outlineWidth) >= 2), `${label}: keyboard focus is visible`);
|
||||
await stop.click();
|
||||
await page.waitForFunction(() => document.querySelector('[data-action="session.stop"]')?.getAttribute("aria-busy") === "true");
|
||||
assert.equal(await stop.isDisabled(), true, `${label}: a pending stop cannot be repeated`);
|
||||
check(await stop.evaluateAll(measure), `${label}/stopping`);
|
||||
assert.deepEqual(errors, []);
|
||||
await page.close();
|
||||
console.log(`PASS composer icons ${label}: legibility, alignment, focus and running states`);
|
||||
}
|
||||
for (const colorScheme of ["light", "dark"]) {
|
||||
const page = await browser.newPage({ viewport: { width: 1300, height: 900 }, colorScheme, reducedMotion: "reduce" });
|
||||
const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem("rx-lang", "zh"); localStorage.setItem("rx-nav-rail", "on");
|
||||
window.reasonixHost = {
|
||||
shell: "electron", platform: "windows", titleBar: false,
|
||||
isWindowMaximised: async () => false, setUnreadBadge: async () => {},
|
||||
showBrowserView: async () => {}, hideBrowserView: async () => {}, freezeBrowserView: async () => "",
|
||||
controlBrowserView: async () => {}, navigateBrowserView: async () => "", onBrowserLoadState: () => () => {},
|
||||
};
|
||||
});
|
||||
const url = new URL(base);
|
||||
url.searchParams.set("pref", "zh");
|
||||
url.searchParams.set("ws", "2");
|
||||
url.searchParams.set("sess", "6");
|
||||
url.searchParams.set("agentpage", "web");
|
||||
await page.goto(url.href, { waitUntil: "networkidle" });
|
||||
assert.deepEqual(errors, []);
|
||||
await page.locator('.navbtn[aria-label="文件"]').first().click();
|
||||
await page.locator('[data-action-click="workbench.tab"][data-target="browser:view-a"]').click();
|
||||
const buttons = page.locator(".bnav .bbtn");
|
||||
await buttons.first().waitFor();
|
||||
assert.equal(await buttons.count(), 4, "the browser exposes four navigation actions");
|
||||
check(await buttons.evaluateAll(measure), `${colorScheme}/browser`);
|
||||
for (const button of await buttons.all()) assert.equal(await button.getAttribute("title"), await button.getAttribute("aria-label"));
|
||||
await page.locator(".bnav").screenshot({ path: `perf/shots/control-icons-browser-${colorScheme}.png` });
|
||||
assert.deepEqual(errors, []);
|
||||
await page.close();
|
||||
console.log(`PASS browser icons ${colorScheme}: legibility, alignment and action labels`);
|
||||
}
|
||||
for (const colorScheme of ["light", "dark"]) for (const width of [1300, 390]) {
|
||||
const page = await browser.newPage({ viewport: { width: 1300, height: 900 }, colorScheme, reducedMotion: "reduce" });
|
||||
const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
await page.goto(`${base}?pref=zh&ws=2&sess=6&turns=2`, { waitUntil: "networkidle" });
|
||||
const label = `${colorScheme}/${width}/library`;
|
||||
const toolIcons = page.locator(".sym svg");
|
||||
assert.ok(await toolIcons.count() > 0, `${label}: transcript icons exist`);
|
||||
assert.ok(await toolIcons.evaluateAll((icons) => icons.every((icon) => icon.classList.contains("lucide"))), `${label}: tool cards use library icons`);
|
||||
await page.locator('.model-picker').click();
|
||||
assert.ok(await page.locator(".studio-model-menu .mi-mono .lucide-server").count() > 0, `${label}: custom providers use service icons`);
|
||||
assert.equal(await page.locator(".studio-model-menu .mi-mono").first().textContent(), "");
|
||||
assert.ok(await page.locator(".studio-model-menu .menu-current.lucide-check").count() > 0, `${label}: selection uses a library check`);
|
||||
await page.keyboard.press("Escape");
|
||||
await page.locator('[data-action="chrome.settings"]').evaluate((button) => button.click());
|
||||
await page.locator('.prefs-nav [data-value="controls"]').click();
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
const order = page.locator('[data-action="appearance.nav-entry-move"]');
|
||||
await order.first().scrollIntoViewIfNeeded();
|
||||
check(await order.evaluateAll(measure), `${label}/reorder`);
|
||||
const down = order.locator('.lucide-arrow-down').first().locator('..');
|
||||
const value = await down.getAttribute("data-value");
|
||||
await down.focus();
|
||||
await page.keyboard.press("Enter");
|
||||
const moved = page.locator(`[data-action="appearance.nav-entry-move"][data-value="${value.replace(/:down$/, ":up")}"]`);
|
||||
assert.equal(await moved.isEnabled(), true, `${label}: reordering remains keyboard operable`);
|
||||
await page.locator("#set-naventries").screenshot({ path: `perf/shots/control-icons-library-${colorScheme}-${width}.png` });
|
||||
assert.deepEqual(errors, []);
|
||||
await page.close();
|
||||
console.log(`PASS semantic icons ${label}: tools, providers, selection and keyboard reordering`);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
// Settings and feedback centre on the window, not on the area below the title
|
||||
// bar; a phone-width window keeps settings full screen. Geometry needs a real browser.
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const BASE = process.env.PERF_URL ?? "http://localhost:4399/perf.html";
|
||||
@@ -20,10 +18,10 @@ for (const [w, h] of [[1600, 1000], [1280, 760], [390, 800]]) {
|
||||
page.on("pageerror", (e) => fails.push("page error: " + e.message));
|
||||
await page.goto(`${BASE}?turns=5&pref=en`, { waitUntil: "networkidle" });
|
||||
await page.waitForSelector(".app");
|
||||
await page.keyboard.press("Meta+Comma");
|
||||
await page.evaluate(() => document.querySelector('[data-action="chrome.settings"]')?.click());
|
||||
await page.waitForSelector(".prefs-sheet");
|
||||
await page.waitForTimeout(300);
|
||||
const prefs = await box(page, ".prefs-sheet");
|
||||
const prefs = await box(page, ".prefs-screen");
|
||||
await page.keyboard.press("Escape");
|
||||
await page.waitForSelector(".prefs-sheet", { state: "detached" });
|
||||
await page.evaluate(() => document.querySelector('[data-action="feedback.open"]')?.click());
|
||||
@@ -32,7 +30,7 @@ for (const [w, h] of [[1600, 1000], [1280, 760], [390, 800]]) {
|
||||
const fbk = await box(page, ".fbk");
|
||||
const tag = `${w}x${h}`;
|
||||
if (w > 760) {
|
||||
check(`${tag}: settings sits in the window's vertical middle`, Math.abs(prefs.top - prefs.bottom) <= 2, JSON.stringify(prefs));
|
||||
check(`${tag}: settings fills the window`, prefs.top === 0 && prefs.bottom === 0, JSON.stringify(prefs));
|
||||
check(`${tag}: feedback sits in the window's vertical middle`, Math.abs(fbk.top - fbk.bottom) <= 2, JSON.stringify(fbk));
|
||||
} else {
|
||||
check(`${tag}: settings still reaches the bottom of a phone-width window`, prefs.bottom === 0, JSON.stringify(prefs));
|
||||
|
||||
@@ -61,6 +61,14 @@ function benchPack(): ThemePack | null {
|
||||
|
||||
class BenchPort extends MockPort {
|
||||
private driverRunning: boolean | undefined;
|
||||
async providerHealthSettings() {
|
||||
return query.has("modelhealth") ? { enabled: true } : super.providerHealthSettings();
|
||||
}
|
||||
async providerHealth(name: string) {
|
||||
return query.has("modelhealth")
|
||||
? { connection: "reachable" as const, wallet: "unsupported" as const, checkedAt: Date.now() }
|
||||
: super.providerHealth(name);
|
||||
}
|
||||
opensFiles() { return query.has("deliveries") || super.opensFiles(); }
|
||||
revealsFiles() { return query.has("deliveries") || super.revealsFiles(); }
|
||||
async account() {
|
||||
|
||||
@@ -38,8 +38,8 @@ for (const scheme of ["light", "dark"]) {
|
||||
await page.evaluate(() => [...document.querySelectorAll("button")].find((x) => /备份原文件并修复/.test(x.textContent))?.click());
|
||||
await page.waitForTimeout(300);
|
||||
await page.evaluate(() => document.activeElement?.blur());
|
||||
const settingsOut = await outside(".prefs-sheet", 60);
|
||||
check(`${scheme}: Tab never leaves the settings sheet`, settingsOut.length === 0, settingsOut.slice(0, 3).join("; "));
|
||||
const settingsOut = await outside(".prefs-screen", 60);
|
||||
check(`${scheme}: Tab never leaves the full-screen settings surface`, settingsOut.length === 0, settingsOut.slice(0, 3).join("; "));
|
||||
await page.keyboard.press("Escape");
|
||||
await page.waitForTimeout(400);
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const PAGE = new URL(process.env.PERF_URL ?? "http://localhost:4399/perf.html");
|
||||
PAGE.searchParams.set("modelhealth", "1");
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
await mkdir("perf/shots", { recursive: true });
|
||||
for (const lang of ["zh", "en"]) for (const width of [1300, 760, 390]) for (const scale of [1, 1.3]) {
|
||||
const page = await browser.newPage({ viewport: { width, height: 900 }, colorScheme: lang === "zh" ? "light" : "dark" });
|
||||
const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
await page.addInitScript((language) => localStorage.setItem("rx-lang", language), lang);
|
||||
await page.goto(PAGE.href, { waitUntil: "networkidle" });
|
||||
await page.waitForSelector(".model-picker");
|
||||
await page.evaluate((value) => document.documentElement.style.setProperty("--fs-scale", String(value)), scale);
|
||||
await page.locator(".model-picker").first().click();
|
||||
const menu = page.locator(".studio-model-menu:not([hidden])");
|
||||
await menu.locator('.menu-item-status[data-tone="positive"]').first().waitFor();
|
||||
const effortWrapped = await menu.locator(".menu-aside .studio-item-line").evaluateAll((lines) => lines.some((line) => {
|
||||
const meta = line.querySelector(".studio-item-meta");
|
||||
return meta && meta.getBoundingClientRect().top >= line.querySelector(".lb").getBoundingClientRect().bottom;
|
||||
}));
|
||||
assert.ok(!effortWrapped, `${lang}/${width}/${scale}: reasoning choices keep their label and tier together`);
|
||||
const vision = menu.locator(".menu-rows button.mi:has(.studio-item-badge)").first();
|
||||
const visionRef = await vision.getAttribute("data-value");
|
||||
await vision.click();
|
||||
await page.waitForFunction((value) => document.querySelector('.studio-model-menu:not([hidden]) button[data-on]')?.dataset.value === value, visionRef);
|
||||
await page.keyboard.press("Escape");
|
||||
await page.locator(".model-picker").first().click();
|
||||
await menu.locator('.menu-item-status[data-tone="positive"]').first().waitFor();
|
||||
await menu.evaluate((element) => {
|
||||
for (const line of element.querySelectorAll(".menu-rows .studio-item-line")) {
|
||||
line.querySelector(".lb").textContent = "deepseek-v4.1-flash-preview-extended";
|
||||
}
|
||||
for (const head of element.querySelectorAll(".menu-rows .mi.head")) {
|
||||
head.querySelector(".lb").textContent = "Shanghai_AI_Lab";
|
||||
head.querySelector(".rt").textContent = "discovery-api.intern-ai.org.cn/compatible-mode/v1";
|
||||
}
|
||||
});
|
||||
const measured = await menu.evaluate((element) => {
|
||||
const rows = element.querySelector(".menu-rows");
|
||||
const sizes = [...rows.querySelectorAll(".studio-item-line")].map((line) => {
|
||||
const label = line.querySelector(".lb");
|
||||
const badge = line.querySelector(".studio-item-badge");
|
||||
const status = line.closest("button").querySelector(".menu-item-status");
|
||||
const labelBox = label.getBoundingClientRect();
|
||||
const badgeBox = badge?.getBoundingClientRect();
|
||||
return {
|
||||
labelWidth: labelBox.width,
|
||||
badgeHeight: badgeBox?.height,
|
||||
badgeLine: badge ? parseFloat(getComputedStyle(badge).lineHeight) + 4 : 0,
|
||||
badgeClipped: badge ? badge.scrollWidth > badge.clientWidth + 1 : false,
|
||||
badgeOverlap: badgeBox ? badgeBox.right > status.getBoundingClientRect().left : false,
|
||||
};
|
||||
});
|
||||
return { sizes, overflow: rows.scrollWidth - rows.clientWidth, menuOverflow: element.scrollWidth - element.clientWidth };
|
||||
});
|
||||
const tag = `${lang}/${width}/${scale}`;
|
||||
await menu.screenshot({ path: `perf/shots/model-menu-${lang}-${width}-${scale}.png` });
|
||||
assert.ok(measured.sizes.some((size) => size.badgeHeight), `${tag}: a model reads images`);
|
||||
for (const size of measured.sizes) {
|
||||
assert.ok(size.labelWidth >= 96 * scale, `${tag}: the model name remains readable (${JSON.stringify(size)})`);
|
||||
if (size.badgeHeight) assert.ok(size.badgeHeight <= size.badgeLine + 1 && !size.badgeClipped && !size.badgeOverlap, `${tag}: the image badge stays whole (${JSON.stringify(size)})`);
|
||||
}
|
||||
assert.ok(measured.overflow <= 1 && measured.menuOverflow <= 1, `${tag}: long endpoints do not create horizontal scrolling (${JSON.stringify(measured)})`);
|
||||
await menu.locator(".mfind").fill("kimi");
|
||||
const choice = menu.locator(".menu-rows button.mi:not(.plain)").first();
|
||||
const picked = await choice.getAttribute("data-value");
|
||||
assert.ok(picked?.includes("kimi"), `${tag}: search still finds the model`);
|
||||
await choice.click();
|
||||
await page.waitForFunction((value) => document.querySelector('.studio-model-menu:not([hidden]) button[data-on]')?.dataset.value === value, picked);
|
||||
await page.keyboard.press("Escape");
|
||||
await menu.waitFor({ state: "hidden" });
|
||||
await page.locator(".model-picker").first().click();
|
||||
await page.waitForFunction((value) => document.querySelector('.studio-model-menu:not([hidden]) button[data-on]')?.dataset.value === value, picked);
|
||||
assert.equal(await menu.locator(".mfind").inputValue(), "", `${tag}: reopening clears the search`);
|
||||
await page.keyboard.press("Escape");
|
||||
await menu.waitFor({ state: "hidden" });
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(`PASS model menu ${tag}: names, badges, connection status and endpoints fit`);
|
||||
await page.close();
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -19,7 +19,7 @@ try {
|
||||
await page.waitForSelector(".app", { timeout: 20000 });
|
||||
await page.getByRole("button", { name: "设置", exact: true }).click();
|
||||
await page.locator('[role="tab"][title="模型"]').click();
|
||||
await page.locator('[data-action="settings.open-page"][data-target="providers"]').click();
|
||||
await page.locator('[data-action="settings.page-index"][data-target="providers"]').click();
|
||||
await page.getByText("myrelay", { exact: true }).first().click();
|
||||
await page.getByText("从服务商读取可用模型").first().click();
|
||||
await page.waitForSelector(".mrows .mline");
|
||||
|
||||
@@ -24,7 +24,7 @@ for (const scheme of ["light", "dark"]) {
|
||||
await page.evaluate(() => [...document.querySelectorAll("button")].find((x) => /备份原文件并修复/.test(x.textContent))?.click());
|
||||
await page.waitForTimeout(400);
|
||||
await page.evaluate(() => document.querySelector('[role="tab"][title="模型"]')?.click());
|
||||
await page.evaluate(() => document.querySelector('[data-action="settings.open-page"][data-target="providers"]')?.click());
|
||||
await page.evaluate(() => document.querySelector('[data-action="settings.page-index"][data-target="providers"]')?.click());
|
||||
await page.waitForSelector(".pdetail", { state: "attached" });
|
||||
await page.evaluate(() => [...document.querySelectorAll("button,li,a")].find((x) => /myrelay-work/.test(x.textContent) && x.textContent.length < 80)?.click());
|
||||
await page.waitForSelector(".pdetail .mlhead", { state: "attached" });
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const base = new URL(process.env.PERF_URL ?? "http://localhost:4399/perf.html");
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
await mkdir("perf/shots", { recursive: true });
|
||||
for (const colorScheme of ["light", "dark"]) for (const pack of ["default", "sky", "photo"]) {
|
||||
const page = await browser.newPage({ viewport: { width: 1300, height: 900 }, colorScheme, reducedMotion: "reduce" });
|
||||
await page.addInitScript(() => { localStorage.setItem("rx-lang", "zh"); localStorage.setItem("rx-nav-rail", "on"); });
|
||||
const url = new URL(base);
|
||||
for (const [key, value] of Object.entries({ pref: "zh", ws: "2", sess: "6", turns: "1" })) url.searchParams.set(key, value);
|
||||
if (pack !== "default") url.searchParams.set("pack", pack);
|
||||
await page.goto(url.href, { waitUntil: "networkidle" });
|
||||
const current = page.locator('.navbtn[aria-current="page"]');
|
||||
await current.waitFor();
|
||||
const label = `${colorScheme}/${pack}`;
|
||||
await page.locator(".nav").screenshot({ path: `perf/shots/nav-selection-${colorScheme}-${pack}.png` });
|
||||
const colors = await current.evaluate((button) => {
|
||||
const ink = document.createElement("canvas").getContext("2d", { willReadFrequently: true });
|
||||
const rgb = (color) => {
|
||||
ink.clearRect(0, 0, 1, 1); ink.fillStyle = color; ink.fillRect(0, 0, 1, 1);
|
||||
return [...ink.getImageData(0, 0, 1, 1).data];
|
||||
};
|
||||
const style = getComputedStyle(button);
|
||||
const bg = rgb(style.backgroundColor);
|
||||
const fg = rgb(style.color);
|
||||
const over = (top, bottom) => {
|
||||
const a = top[3] / 255, b = bottom[3] / 255, opacity = a + b * (1 - a);
|
||||
return opacity ? top.slice(0, 3).map((v, i) => (v * a + bottom[i] * b * (1 - a)) / opacity).concat(opacity * 255) : top;
|
||||
};
|
||||
let surface = [0, 0, 0, 0];
|
||||
for (let element = button; element; element = element.parentElement) surface = over(surface, rgb(getComputedStyle(element).backgroundColor));
|
||||
const anchor = document.createElement("span");
|
||||
anchor.style.backgroundColor = "var(--accent)";
|
||||
button.append(anchor);
|
||||
const accent = rgb(getComputedStyle(anchor).backgroundColor);
|
||||
anchor.remove();
|
||||
const luminance = (color) => color.slice(0, 3).map((v) => {
|
||||
const s = v / 255; return s <= .04045 ? s / 12.92 : ((s + .055) / 1.055) ** 2.4;
|
||||
}).reduce((sum, v, i) => sum + v * [.2126, .7152, .0722][i], 0);
|
||||
const [a, b] = [luminance(over(fg, surface)), luminance(surface)].sort((x, y) => y - x);
|
||||
return { bg, accent, contrast: (a + .05) / (b + .05), marker: getComputedStyle(button, "::before").width };
|
||||
});
|
||||
assert.notDeepEqual(colors.bg, colors.accent, `${label}: navigation uses a quiet selection surface`);
|
||||
assert.ok(colors.contrast >= 3, `${label}: the selected icon stays legible (${JSON.stringify(colors)})`);
|
||||
assert.ok(parseFloat(colors.marker) >= 2, `${label}: a side marker also identifies the current section`);
|
||||
await current.focus();
|
||||
assert.ok(await current.evaluate((button) => parseFloat(getComputedStyle(button).outlineWidth) >= 2), `${label}: keyboard focus stays visible`);
|
||||
assert.equal(await current.getAttribute("aria-label"), "会话");
|
||||
await page.close();
|
||||
console.log(`PASS navigation selection ${label}: quiet surface, contrast and focus`);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -40,12 +40,12 @@ for (const scheme of ["light", "dark"]) {
|
||||
await page.waitForSelector('[data-action="settings.section"][data-value="tools"]');
|
||||
await section("session");
|
||||
await page.waitForTimeout(300);
|
||||
check(`${tag}: session messages have a settings entry of their own`, (await page.locator('[data-action="settings.open-page"][data-target="peers"]').count()) === 1);
|
||||
check(`${tag}: session messages have a settings entry of their own`, (await page.locator('[data-action="settings.page-index"][data-target="peers"]').count()) === 1);
|
||||
await section("tools");
|
||||
await page.waitForTimeout(300);
|
||||
check(`${tag}: and no longer sit at the foot of tools and permissions`, (await page.locator('[role="switch"][aria-label="启用会话间消息"]').count()) === 0);
|
||||
await section("session");
|
||||
await page.locator('[data-action="settings.open-page"][data-target="peers"]').click();
|
||||
await page.locator('[data-action="settings.page-index"][data-target="peers"]').click();
|
||||
await page.waitForSelector('[role="switch"][aria-label="启用会话间消息"]', { timeout: 8000 });
|
||||
const s = await page.evaluate(() => {
|
||||
const sw = (n) => document.querySelector(`[role="switch"][aria-label="${n}"]`);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// A wide settings window centres its content column between the nav and
|
||||
// the window edge: the room left over is shared, not piled up on the right.
|
||||
import assert from "node:assert/strict";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
@@ -18,9 +16,17 @@ try {
|
||||
const room = await page.evaluate(() => {
|
||||
const main = document.querySelector(".prefs-main").getBoundingClientRect();
|
||||
const col = document.querySelector(".prefs-main .prefs-col").getBoundingClientRect();
|
||||
return { left: Math.round(col.left - main.left), right: Math.round(main.right - col.right), width: Math.round(col.width) };
|
||||
const screen = document.querySelector(".prefs").getBoundingClientRect();
|
||||
const sheet = document.querySelector(".prefs-sheet").getBoundingClientRect();
|
||||
const nav = document.querySelector(".prefs-nav").getBoundingClientRect();
|
||||
const cards = [...document.querySelectorAll(".prefs-col > .prefs-card")].map((c) => c.getBoundingClientRect());
|
||||
return { sheet: { left: sheet.left, right: sheet.right, bottom: sheet.bottom }, nav: nav.width, left: Math.round(col.left - main.left), right: Math.round(main.right - col.right), width: Math.round(col.width), screen: { x: screen.x, y: screen.y, width: screen.width, height: screen.height }, single: cards.every((c, i) => Math.abs(c.width - col.width) < 1 && (!i || c.top >= cards[i - 1].bottom)) };
|
||||
});
|
||||
assert.ok(room.width <= 1200, `the column keeps its cap (${room.width})`);
|
||||
assert.deepEqual(room.screen, { x: 0, y: 0, width, height: 900 }, "settings occupies the application window");
|
||||
assert.deepEqual(room.sheet, { left: 0, right: width, bottom: 900 });
|
||||
assert.equal(room.nav, 240);
|
||||
assert.ok(room.single, "all cards fill one content column");
|
||||
assert.ok(room.left >= 20 && room.left <= 32 && room.right >= 20 && room.right <= 32, `${width}px: only fixed content gutters remain (${JSON.stringify(room)})`);
|
||||
assert.ok(Math.abs(room.left - room.right) <= 2, `${width}px: the room is shared evenly (left ${room.left}, right ${room.right})`);
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(`PASS ${width}px: column ${room.width}px with ${room.left}px each side`);
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const base = process.env.PERF_URL ?? "http://localhost:4399/perf.html";
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
await mkdir("perf/shots", { recursive: true });
|
||||
for (const colorScheme of ["light", "dark"]) for (const language of ["zh", "en"]) for (const width of [1800, 1400, 1000, 390]) for (const scale of [1, 1.3]) {
|
||||
const page = await browser.newPage({ viewport: { width: 1400, height: 1000 }, colorScheme, reducedMotion: "reduce" });
|
||||
page.setDefaultTimeout(10000);
|
||||
const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
page.on("requestfailed", (request) => errors.push(`${request.url()}: ${request.failure()?.errorText}`));
|
||||
await page.addInitScript((value) => localStorage.setItem("rx-lang", value), language);
|
||||
await page.goto(`${base}?pref=${language}&turns=1`, { waitUntil: "networkidle" });
|
||||
assert.deepEqual(errors, [], "the browser fixture and its styles load successfully");
|
||||
await page.locator('[data-action="chrome.settings"]').evaluate((button) => button.click());
|
||||
await page.locator('.prefs-nav [data-value="model"]').click();
|
||||
await page.getByText("myrelay", { exact: true }).first().click();
|
||||
const form = page.locator(".addp[data-edit]");
|
||||
await form.locator('.token-limits input').first().fill("1600000");
|
||||
await form.locator('[data-value="model-window"]').last().fill("1000000");
|
||||
await form.locator('[data-value="model-max-output"]').last().fill("1000000");
|
||||
await page.setViewportSize({ width, height: 1000 });
|
||||
await page.evaluate(async (value) => {
|
||||
document.documentElement.style.setProperty("--fs-scale", String(value));
|
||||
await document.fonts.ready;
|
||||
}, scale);
|
||||
const label = `${colorScheme}-${language}-${width}-${scale}`;
|
||||
const billing = form.locator('[data-value="billing-mode"]');
|
||||
const currency = form.locator('[data-value="model-price-currency"]').last();
|
||||
await currency.selectOption("USD");
|
||||
await billing.selectOption("subscription_equivalent");
|
||||
const measure = () => form.evaluate((root) => {
|
||||
const context = document.createElement("canvas").getContext("2d");
|
||||
const fields = [...root.querySelectorAll('.ml-row > .unit-field input, .ml-row > select, [data-value="billing-mode"]')];
|
||||
const clipped = fields.flatMap((field) => {
|
||||
const style = getComputedStyle(field);
|
||||
context.font = style.font;
|
||||
const text = field.tagName === "SELECT" ? field.selectedOptions[0].textContent : field.value || field.placeholder;
|
||||
const available = field.clientWidth - parseFloat(style.paddingLeft) - parseFloat(style.paddingRight);
|
||||
const needed = context.measureText(text).width;
|
||||
return needed > available + 1 ? [{ field: field.getAttribute("aria-label"), text, needed, available }] : [];
|
||||
});
|
||||
const rows = [...root.querySelectorAll(".ml-row")];
|
||||
const uneven = rows.flatMap((row) => {
|
||||
const heights = [...row.querySelectorAll(":scope > .unit-field, :scope > select")].map((field) => field.getBoundingClientRect().height);
|
||||
return Math.max(...heights) - Math.min(...heights) > 1 ? [heights] : [];
|
||||
});
|
||||
const outside = fields.filter((field) => {
|
||||
const box = field.getBoundingClientRect(), parent = field.closest(".ml-row, label").getBoundingClientRect();
|
||||
return box.right > parent.right + 1 || box.left < parent.left - 1;
|
||||
}).map((field) => field.getAttribute("aria-label"));
|
||||
for (const row of rows) {
|
||||
const controls = row.querySelectorAll(":scope > .unit-field, :scope > select");
|
||||
if (row.getBoundingClientRect().right - controls[controls.length - 1].getBoundingClientRect().right > 1) outside.push("unfilled row");
|
||||
}
|
||||
return { clipped, uneven, outside };
|
||||
});
|
||||
const selected = await measure();
|
||||
await form.locator(".ml-group").first().scrollIntoViewIfNeeded();
|
||||
await page.screenshot({ path: `perf/shots/provider-fields-${label}.png` });
|
||||
assert.deepEqual(selected, { clipped: [], uneven: [], outside: [] }, `${label}: values, inherited hints and the full billing choice fit aligned controls`);
|
||||
await billing.selectOption("payg");
|
||||
assert.deepEqual(await measure(), { clipped: [], uneven: [], outside: [] }, `${label}: the default billing choice fits too`);
|
||||
await currency.focus();
|
||||
await page.keyboard.press("Space");
|
||||
await page.waitForFunction(() => document.querySelector('[data-value="model-price-currency"]:open'));
|
||||
await page.keyboard.press("Enter");
|
||||
assert.equal(await currency.inputValue(), "USD", `${label}: keyboard confirmation preserves the currency`);
|
||||
await form.locator('[data-value="model-window"]').last().fill("");
|
||||
assert.deepEqual(await measure(), { clipped: [], uneven: [], outside: [] }, `${label}: clearing a value reveals its complete inheritance hint`);
|
||||
await form.locator('.token-limits input').first().fill("");
|
||||
assert.deepEqual(await measure(), { clipped: [], uneven: [], outside: [] }, `${label}: an undeclared shared window also fits`);
|
||||
await billing.scrollIntoViewIfNeeded();
|
||||
await page.screenshot({ path: `perf/shots/provider-fields-prices-${label}.png` });
|
||||
assert.deepEqual(errors, []);
|
||||
await page.close();
|
||||
console.log(`PASS provider fields ${label}: text fit, aligned heights, currency, billing and inheritance`);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -19,7 +19,7 @@ try {
|
||||
await page.waitForSelector(".app", { timeout: 20000 });
|
||||
await page.getByRole("button", { name: "设置", exact: true }).click();
|
||||
await page.locator('[role="tab"][title="模型"]').click();
|
||||
await page.locator('[data-action="settings.open-page"][data-target="providers"]').click();
|
||||
await page.locator('[data-action="settings.page-index"][data-target="providers"]').click();
|
||||
await page.waitForSelector('[data-action-click="provider.select"]');
|
||||
const text = await page.evaluate(() => [...document.querySelectorAll('[data-action-click="provider.select"]')].map((b) => b.textContent ?? ""));
|
||||
check(`${scheme}: the server on port 8756 is listed`, text.some((t) => t.includes("127.0.0.1:8756")), text.join(" | "));
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// With the icon rail hidden, the sidebar's shortcut strip is one row of equal icon buttons on a single line,
|
||||
// and the rows around it (new session, search, feedback) draw their icon and label at the sidebar's own edge.
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const BASE = process.env.PERF_URL ?? "http://localhost:4399/perf.html?pref=zh&ws=1&sess=1&turns=1";
|
||||
@@ -17,18 +15,19 @@ try {
|
||||
await ctx.addInitScript(() => { try { localStorage.setItem("rx-nav-rail", "off"); } catch {} });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto(BASE, { waitUntil: "networkidle" });
|
||||
await page.locator(".studio-tools").hover();
|
||||
await page.waitForSelector(".studio-quicknav > button");
|
||||
const strip = await page.evaluate(() => [...document.querySelectorAll(".studio-quicknav > button")].map((b) => {
|
||||
const r = b.getBoundingClientRect();
|
||||
const svg = b.querySelector("svg, .studio-icon").getBoundingClientRect();
|
||||
return { w: r.width, h: r.height, cy: r.y + r.height / 2, icon: [svg.width, svg.height], iconCy: svg.y + svg.height / 2 };
|
||||
return { w: r.width, h: r.height, left: r.left, cy: r.y + r.height / 2, icon: [svg.width, svg.height], iconCy: svg.y + svg.height / 2 };
|
||||
}));
|
||||
const spread = (key) => Math.max(...strip.map((s) => s[key])) - Math.min(...strip.map((s) => s[key]));
|
||||
check(`${scheme}: the shortcut strip has several buttons`, strip.length >= 5, `${strip.length}`);
|
||||
check(`${scheme}: the shortcut card has several entries`, strip.length >= 5, `${strip.length}`);
|
||||
check(`${scheme}: the shortcut buttons share one size`, spread("w") <= TOLERANCE && spread("h") <= TOLERANCE, JSON.stringify(strip.map((s) => [s.w, s.h])));
|
||||
check(`${scheme}: the shortcut buttons and their icons sit on one horizontal line`, spread("cy") <= TOLERANCE && spread("iconCy") <= TOLERANCE, `${spread("cy")} / ${spread("iconCy")}`);
|
||||
check(`${scheme}: the shortcut rows align and center their icons`, spread("left") <= TOLERANCE && strip.every((s) => Math.abs(s.cy - s.iconCy) <= TOLERANCE), JSON.stringify(strip.map((s) => [s.left, s.cy - s.iconCy])));
|
||||
check(`${scheme}: the shortcut icons share one size`, new Set(strip.map((s) => s.icon.join("x"))).size === 1, strip.map((s) => s.icon.join("x")).join(" "));
|
||||
const rows = await page.evaluate(() => [".studio-new-task", ".studio-search", ".studio-feedback"].map((s) => {
|
||||
const rows = await page.evaluate(() => [".studio-new-task", ".studio-search", ".studio-tools-row", ".studio-feedback"].map((s) => {
|
||||
const row = document.querySelector(s);
|
||||
if (!row) return null;
|
||||
const r = row.getBoundingClientRect();
|
||||
|
||||
@@ -16,6 +16,7 @@ try {
|
||||
await ctx.addInitScript(() => { try { localStorage.setItem("rx-nav-rail", "off"); } catch {} });
|
||||
const page = await ctx.newPage();
|
||||
await page.goto(BASE, { waitUntil: "networkidle" });
|
||||
await page.locator(".studio-tools").hover();
|
||||
await page.waitForSelector(".studio-quicknav > button");
|
||||
await page.mouse.move(1270, 890);
|
||||
const looks = await page.evaluate(() => [...document.querySelectorAll(".studio-quicknav > button")].map((b) => {
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const base = process.env.PERF_URL ?? "http://localhost:4399/perf.html";
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
await mkdir("perf/shots", { recursive: true });
|
||||
for (const colorScheme of ["light", "dark"]) for (const width of [1300, 390]) for (const language of ["zh", "en"]) for (const scale of [1, 1.3]) {
|
||||
const page = await browser.newPage({ viewport: { width: 1300, height: 900 }, colorScheme, reducedMotion: "reduce" });
|
||||
page.setDefaultTimeout(10000);
|
||||
const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
await page.addInitScript((value) => localStorage.setItem("rx-lang", value), language);
|
||||
await page.goto(`${base}?pref=${language}&turns=1`, { waitUntil: "networkidle" });
|
||||
await page.locator('[data-action="chrome.settings"]').click();
|
||||
await page.locator('.prefs-nav [data-value="common"]').click();
|
||||
const select = page.locator('[data-action="appearance.language"]');
|
||||
await select.waitFor();
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await page.evaluate(async (value) => {
|
||||
document.documentElement.style.setProperty("--fs-scale", String(value));
|
||||
await document.fonts.ready;
|
||||
}, scale);
|
||||
const label = `${colorScheme}-${width}-${language}-${scale}`;
|
||||
const colors = await select.evaluate((field) => ({ ink: getComputedStyle(field).color, ground: getComputedStyle(field).backgroundColor }));
|
||||
const shot = await select.screenshot({ path: `perf/shots/select-alignment-${label}.png` });
|
||||
const bounds = await page.evaluate(async ({ data, ink, ground }) => {
|
||||
const picture = new Image();
|
||||
picture.src = `data:image/png;base64,${data}`;
|
||||
await picture.decode();
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = picture.width; canvas.height = picture.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const rgb = (color) => { ctx.fillStyle = color; ctx.fillRect(0, 0, 1, 1); return [...ctx.getImageData(0, 0, 1, 1).data].slice(0, 3); };
|
||||
const fg = rgb(ink), bg = rgb(ground);
|
||||
ctx.drawImage(picture, 0, 0);
|
||||
const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||
const distance = (pixel, color) => color.reduce((sum, channel, i) => sum + (channel - pixel[i]) ** 2, 0);
|
||||
let top = canvas.height, bottom = -1;
|
||||
for (let y = 1; y < canvas.height - 1; y++) for (let x = 12; x < canvas.width - 34; x++) {
|
||||
const pixel = pixels.slice((y * canvas.width + x) * 4, (y * canvas.width + x) * 4 + 3);
|
||||
if (distance(pixel, fg) < distance(pixel, bg)) { top = Math.min(top, y); bottom = Math.max(bottom, y); }
|
||||
}
|
||||
return { top, bottom, height: canvas.height, offset: Math.abs((top + bottom + 1 - canvas.height) / 2) };
|
||||
}, { data: shot.toString("base64"), ...colors });
|
||||
assert.ok(bounds.bottom > bounds.top, `${label}: the selected text is visible`);
|
||||
assert.ok(bounds.offset <= 2, `${label}: text is vertically centered (${JSON.stringify(bounds)})`);
|
||||
assert.deepEqual(await select.locator("option").allTextContents(), language === "zh" ? ["跟随系统", "简体中文", "英文"] : ["Follow the system", "简体中文", "English"]);
|
||||
await select.focus();
|
||||
await page.keyboard.press("Space");
|
||||
await page.waitForFunction(() => document.querySelector('[data-action="appearance.language"]').matches(":open"));
|
||||
await page.keyboard.press("Escape");
|
||||
assert.equal(await page.evaluate(() => localStorage.getItem("rx-lang")), language, `${label}: dismissing the picker preserves the language`);
|
||||
assert.deepEqual(errors, []);
|
||||
await page.close();
|
||||
console.log(`PASS select ${label}: centered text, language names and keyboard dismissal`);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const PAGE = process.env.PERF_URL ?? "http://localhost:4399/perf.html?pref=zh";
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
for (const scheme of ["light", "dark"]) {
|
||||
const context = await browser.newContext({ viewport: { width: 1500, height: 900 }, colorScheme: scheme, deviceScaleFactor: 2 });
|
||||
const page = await context.newPage();
|
||||
await page.goto(PAGE, { waitUntil: "networkidle" });
|
||||
await page.evaluate(() => document.querySelector('[data-action="chrome.settings"]')?.click());
|
||||
await page.waitForSelector(".prefs-nav button");
|
||||
await page.locator(".prefs-nav button", { hasText: /^常用/ }).first().click();
|
||||
await page.waitForTimeout(500);
|
||||
const png = await page.locator("select").first().screenshot();
|
||||
const ink = await page.evaluate(async ({ image }) => {
|
||||
const img = new Image();
|
||||
img.src = image;
|
||||
await img.decode();
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const context2d = canvas.getContext("2d");
|
||||
context2d.drawImage(img, 0, 0);
|
||||
const data = context2d.getImageData(0, 0, img.width, img.height).data;
|
||||
const at = (x, y) => [data[(y * img.width + x) * 4], data[(y * img.width + x) * 4 + 1], data[(y * img.width + x) * 4 + 2]];
|
||||
const bg = at(Math.floor(img.width / 2), 6);
|
||||
let top = img.height;
|
||||
let bottom = -1;
|
||||
for (let y = 4; y < img.height - 4; y++) {
|
||||
for (let x = 16; x < img.width * 0.7; x++) {
|
||||
const p = at(x, y);
|
||||
if (Math.abs(p[0] - bg[0]) + Math.abs(p[1] - bg[1]) + Math.abs(p[2] - bg[2]) > 150) {
|
||||
top = Math.min(top, y);
|
||||
bottom = Math.max(bottom, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { above: top, below: img.height - 1 - bottom };
|
||||
}, { image: `data:image/png;base64,${png.toString("base64")}` });
|
||||
assert.ok(Math.abs(ink.above - ink.below) <= 4, `${scheme}: the chosen text sits in the middle of its select (${JSON.stringify(ink)})`);
|
||||
console.log(`PASS ${scheme}: select text centred (${ink.above}px above, ${ink.below}px below at 2x)`);
|
||||
await context.close();
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const PAGE = process.env.PERF_URL ?? "http://localhost:4399/perf.html?pref=zh";
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 1500, height: 900 }, deviceScaleFactor: 2 });
|
||||
await page.goto(PAGE, { waitUntil: "networkidle" });
|
||||
await page.evaluate(() => document.querySelector('[data-action="chrome.settings"]')?.click());
|
||||
await page.waitForSelector(".prefs-nav button");
|
||||
await page.locator(".prefs-nav button", { hasText: /^模型/ }).first().click();
|
||||
await page.waitForTimeout(400);
|
||||
await page.getByRole("button", { name: "按用途设置" }).click();
|
||||
await page.waitForTimeout(500);
|
||||
const fit = await page.evaluate(() => {
|
||||
const select = document.querySelector("select.usage-pick");
|
||||
const option = document.createElement("option");
|
||||
option.value = "long";
|
||||
option.textContent = "deepseek-v4.1-flash-1017-preview-extended · 读图";
|
||||
select.appendChild(option);
|
||||
select.value = "long";
|
||||
select.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
return new Promise((resolve) => requestAnimationFrame(() => {
|
||||
const box = select.getBoundingClientRect();
|
||||
resolve({ height: Math.round(box.height), overflow: select.scrollHeight - select.clientHeight, whiteSpace: getComputedStyle(select).whiteSpace });
|
||||
}));
|
||||
});
|
||||
assert.equal(fit.whiteSpace, "nowrap", "a select never wraps the name it shows");
|
||||
assert.ok(fit.overflow <= 2, `a long chosen name does not spill out of its box (${JSON.stringify(fit)})`);
|
||||
console.log("PASS a long chosen name stays on one line inside its select");
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const PAGE = process.env.PERF_URL ?? "http://localhost:4399/perf.html?pref=zh";
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 1300, height: 900 } });
|
||||
const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
await page.addInitScript(() => localStorage.setItem("rx-session-quick-actions", "on"));
|
||||
await page.goto(PAGE, { waitUntil: "networkidle" });
|
||||
await page.waitForSelector(".ds-row.sessrow");
|
||||
await page.waitForTimeout(500);
|
||||
const shown = (row, selector) =>
|
||||
row.evaluate((el, sel) => {
|
||||
const node = el.querySelector(sel);
|
||||
return !!node && getComputedStyle(node).display !== "none" && getComputedStyle(node).opacity !== "0" && node.getBoundingClientRect().width > 2;
|
||||
}, selector);
|
||||
const rows = page.locator(".ds-row.sessrow");
|
||||
await page.mouse.move(700, 450);
|
||||
for (let i = 0; i < Math.min(await rows.count(), 5); i++) {
|
||||
assert.equal(await shown(rows.nth(i), ".sesspin-action"), false, `row ${i}: a resting row shows no pin button`);
|
||||
assert.equal(await shown(rows.nth(i), ".sesspin-marker"), false, `row ${i}: an unpinned row shows no pin marker`);
|
||||
}
|
||||
const first = rows.first();
|
||||
await first.hover();
|
||||
assert.equal(await shown(first, ".sesspin-action"), true, "hovering a row offers the pin button");
|
||||
await first.locator(".sesspin-action").click();
|
||||
await page.waitForTimeout(400);
|
||||
await page.evaluate(() => document.activeElement?.blur());
|
||||
await page.mouse.move(700, 450);
|
||||
await page.waitForTimeout(200);
|
||||
const pinned = page.locator('.ds-row.sessrow:has(.sesspin-marker)').first();
|
||||
assert.equal(await shown(pinned, ".sesspin-marker"), true, "a pinned row keeps its marker while the pointer is away");
|
||||
assert.equal(await shown(pinned, ".sesspin-action"), false, "and still hides the button");
|
||||
await pinned.hover();
|
||||
assert.equal(await shown(pinned, ".sesspin-marker"), false, "the marker gives way to the button under the pointer");
|
||||
assert.equal(await shown(pinned, ".sesspin-action"), true);
|
||||
assert.deepEqual(errors, []);
|
||||
console.log("PASS session row pin button and marker");
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const base = process.env.PERF_URL ?? "http://localhost:4399/perf.html";
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
await mkdir("perf/shots", { recursive: true });
|
||||
for (const colorScheme of ["light", "dark"]) for (const width of [1600, 390]) for (const scale of [1, 1.3]) {
|
||||
const page = await browser.newPage({ viewport: { width: 1300, height: 1000 }, colorScheme, reducedMotion: "reduce" });
|
||||
page.setDefaultTimeout(10000);
|
||||
const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
await page.goto(`${base}?pref=zh&turns=1`, { waitUntil: "networkidle" });
|
||||
await page.locator('[data-action="chrome.settings"]').click();
|
||||
await page.locator('.prefs-nav').waitFor();
|
||||
await page.setViewportSize({ width, height: 1000 });
|
||||
await page.evaluate((size) => document.documentElement.style.setProperty('--fs-scale', String(size)), scale);
|
||||
const sections = await page.locator('.prefs-nav [role="tab"]').evaluateAll((tabs) => tabs.map((tab) => tab.dataset.value));
|
||||
for (const section of sections) {
|
||||
await page.locator(`.prefs-nav [data-value="${section}"]`).click();
|
||||
await page.waitForTimeout(200);
|
||||
const geometry = await page.locator('.prefs-screen').evaluate((root) => {
|
||||
const sheet = root.querySelector('.prefs-sheet').getBoundingClientRect();
|
||||
const panel = root.querySelector('.prefs-main');
|
||||
const col = panel.querySelector('.prefs-col').getBoundingClientRect();
|
||||
const cards = [...panel.querySelectorAll('.prefs-col > .prefs-card')].map((card) => card.getBoundingClientRect());
|
||||
const nav = root.querySelector('.prefs-nav').getBoundingClientRect();
|
||||
return {
|
||||
sheet: { left: sheet.left, right: sheet.right, bottom: sheet.bottom }, nav: nav.width,
|
||||
gutters: [col.left - panel.getBoundingClientRect().left, panel.getBoundingClientRect().right - col.right],
|
||||
spill: panel.scrollWidth - panel.clientWidth,
|
||||
logsFit: [...panel.querySelectorAll('#set-logs .acct-note')].every((note) => note.scrollWidth <= note.clientWidth + 1),
|
||||
single: cards.every((card, i) => Math.abs(card.width - col.width) < 1 && (!i || card.top >= cards[i - 1].bottom)),
|
||||
};
|
||||
});
|
||||
const label = `${colorScheme} ${width}px ${scale} ${section}`;
|
||||
if (geometry.spill > 1 || !geometry.logsFit) {
|
||||
console.log(label, await page.locator('.prefs-main').evaluate((panel) => [...panel.querySelectorAll('*')]
|
||||
.filter((node) => node.clientWidth > 1 && node.scrollWidth > node.clientWidth + 1)
|
||||
.map((node) => ({ tag: node.tagName, class: node.className, width: node.clientWidth, scroll: node.scrollWidth }))));
|
||||
await page.screenshot({ path: `perf/shots/settings-full-failed-${colorScheme}-${width}-${scale}-${section}.png` });
|
||||
}
|
||||
assert.deepEqual(geometry.sheet, { left: 0, right: width, bottom: 1000 }, label);
|
||||
assert.ok(geometry.single, `${label}: full-width cards in one column`);
|
||||
assert.ok(geometry.spill <= 1, `${label}: horizontal overflow ${geometry.spill}px`);
|
||||
assert.ok(geometry.logsFit, `${label}: log paths wrap within their rows`);
|
||||
if (width > 640) {
|
||||
assert.equal(geometry.nav, 240, label);
|
||||
assert.deepEqual(geometry.gutters, [24, 24], label);
|
||||
}
|
||||
if (section === 'common' && scale === 1) await page.screenshot({ path: `perf/shots/settings-full-${colorScheme}-${width}.png` });
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(`PASS full-screen settings ${colorScheme} ${width}px scale ${scale}: ${sections.length} categories`);
|
||||
await page.close();
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const PAGE = process.env.PERF_URL ?? "http://localhost:4399/perf.html?pref=zh";
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 1600, height: 900 } });
|
||||
const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
await page.goto(PAGE, { waitUntil: "networkidle" });
|
||||
await page.evaluate(() => document.querySelector('[data-action="chrome.settings"]')?.click());
|
||||
await page.waitForSelector(".prefs-nav button");
|
||||
const lefts = () =>
|
||||
page.evaluate(() => {
|
||||
const left = (selector) => Math.round(document.querySelector(selector).getBoundingClientRect().left);
|
||||
return { title: left(".prefs-main .prefs-pagetitle"), index: left(".prefs-main .page-index"), col: left(".prefs-main .prefs-col") };
|
||||
});
|
||||
for (const name of ["模型", "连接", "常用"]) {
|
||||
await page.locator(".prefs-nav button", { hasText: new RegExp(`^${name}`) }).first().click();
|
||||
await page.waitForTimeout(400);
|
||||
const at = await lefts();
|
||||
assert.equal(at.index, at.title, `${name}: the page index starts where the title does (${JSON.stringify(at)})`);
|
||||
assert.equal(at.col, at.title, `${name}: the cards start where the title does (${JSON.stringify(at)})`);
|
||||
console.log(`PASS ${name}: title, index and cards all start at ${at.title}px`);
|
||||
}
|
||||
await page.locator(".prefs-nav button", { hasText: /^操作与性能/ }).first().click();
|
||||
await page.waitForTimeout(400);
|
||||
const keys = await page.locator('#set-keys').elementHandle();
|
||||
await page.locator('[data-action="settings.page-index"][data-target="naventries"]').click();
|
||||
await page.waitForTimeout(600);
|
||||
assert.ok(await keys.evaluate((node) => node.isConnected), "locating a region preserves sibling editors");
|
||||
const placement = await page.locator('#set-naventries').evaluate((node) => {
|
||||
const panel = node.closest('.prefs-main');
|
||||
const index = panel.querySelector('.page-index').getBoundingClientRect();
|
||||
return { top: node.getBoundingClientRect().top, index: index.bottom, bottom: panel.getBoundingClientRect().bottom };
|
||||
});
|
||||
assert.ok(placement.top >= placement.index && placement.top < placement.bottom, `located region clears the pill index: ${JSON.stringify(placement)}`);
|
||||
assert.equal(await page.locator('.deep-page').count(), 0);
|
||||
console.log("PASS locating a region scrolls without replacing any editors");
|
||||
assert.deepEqual(errors, []);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const base = process.env.PERF_URL ?? "http://localhost:4399/perf.html";
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
await mkdir("perf/shots", { recursive: true });
|
||||
for (const colorScheme of ["light", "dark"]) for (const width of [1440, 390]) for (const hidden of [false, true]) {
|
||||
const page = await browser.newPage({ viewport: { width, height: 900 }, colorScheme, reducedMotion: "reduce" });
|
||||
page.setDefaultTimeout(10000);
|
||||
const errors = [];
|
||||
page.on("pageerror", (error) => errors.push(error.message));
|
||||
await page.addInitScript((hide) => {
|
||||
localStorage.setItem("rx-nav-rail", "off");
|
||||
localStorage.setItem("rx-lang", "zh");
|
||||
localStorage.setItem("rx-nav-entries", JSON.stringify({ order: ["remote", "usage"], hidden: hide ? ["usage", "files", "tools", "ext", "memory", "remote"] : [] }));
|
||||
}, hidden);
|
||||
await page.goto(`${base}?pref=zh&turns=1`, { waitUntil: "networkidle" });
|
||||
if (width === 390) await page.locator('.studio-menu').click();
|
||||
const root = page.locator(".studio-tools");
|
||||
const settings = root.locator('[data-action="chrome.settings"]');
|
||||
assert.equal(await settings.innerText(), "设置");
|
||||
const alignment = await settings.evaluate((button) => {
|
||||
const icon = button.querySelector('svg').getBoundingClientRect();
|
||||
const feedback = document.querySelector('.studio-feedback svg').getBoundingClientRect();
|
||||
return Math.abs(icon.x - feedback.x);
|
||||
});
|
||||
assert.ok(alignment <= 4, `settings and feedback icons align: ${alignment}`);
|
||||
if (hidden) {
|
||||
assert.equal(await root.locator('.studio-tools-toggle').count(), 0);
|
||||
await root.hover();
|
||||
await page.waitForTimeout(250);
|
||||
assert.equal(await page.getByRole('menu', { name: '侧栏入口' }).count(), 0);
|
||||
} else {
|
||||
await root.hover();
|
||||
const menu = page.getByRole('menu', { name: '侧栏入口' });
|
||||
await menu.waitFor();
|
||||
assert.deepEqual(await menu.locator('button > span').allTextContents(), ['远程', '用量', '文件', '工具与权限', '扩展', '记忆']);
|
||||
const item = menu.getByRole('menuitem', { name: '文件' });
|
||||
await item.hover();
|
||||
await page.waitForTimeout(220);
|
||||
assert.ok(await menu.isVisible(), 'pointer can cross the gap into the card');
|
||||
const bounds = await menu.boundingBox();
|
||||
assert.ok(bounds.x >= 0 && bounds.x + bounds.width <= width && bounds.y >= 0 && bounds.y + bounds.height <= 900);
|
||||
assert.equal(await menu.locator('button svg.lucide').count(), 6);
|
||||
await page.screenshot({ path: `perf/shots/sidebar-tools-${colorScheme}-${width}.png` });
|
||||
await page.mouse.move(width - 5, 10);
|
||||
await menu.waitFor({ state: 'hidden' });
|
||||
const toggle = root.locator('.studio-tools-toggle');
|
||||
await toggle.focus();
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForFunction(() => document.activeElement?.getAttribute('role') === 'menuitem');
|
||||
await page.keyboard.press('End');
|
||||
assert.equal(await page.locator(':focus > span').innerText(), '记忆');
|
||||
await page.keyboard.press('Escape');
|
||||
assert.ok(await toggle.evaluate((node) => node === document.activeElement));
|
||||
await toggle.click();
|
||||
await item.click();
|
||||
await menu.waitFor({ state: 'hidden' });
|
||||
assert.equal(await page.locator('.app').getAttribute('data-browser'), 'on');
|
||||
if (width === 390) await page.locator('.studio-menu').click();
|
||||
}
|
||||
await settings.click();
|
||||
await page.locator('.prefs-screen').waitFor();
|
||||
assert.equal(await page.getByRole('menu', { name: '侧栏入口' }).count(), 0);
|
||||
assert.deepEqual(errors, []);
|
||||
console.log(`PASS sidebar ${colorScheme} ${width}px ${hidden ? 'all hidden' : 'named hover/click/keyboard card'}`);
|
||||
await page.close();
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -37,7 +37,7 @@ for (const scheme of ["light", "dark"]) {
|
||||
await page.evaluate(() => document.querySelector('[data-action="chrome.settings"]')?.click());
|
||||
await page.waitForSelector(".prefs-nav button");
|
||||
await page.locator(".prefs-nav button", { hasText: "外观" }).first().click();
|
||||
await page.locator('[data-action="settings.open-page"][data-target="size"]').click();
|
||||
await page.locator('[data-action="settings.page-index"][data-target="size"]').click();
|
||||
await page.locator('[data-action="appearance.text-size"]', { hasText: new RegExp(`^${name}$`) }).click();
|
||||
await page.waitForTimeout(200);
|
||||
if (shotDir && scale === 1.3) await page.screenshot({ path: `${shotDir}/${scheme}-settings-1.3.png` });
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const PAGE = process.env.PERF_URL ?? "http://localhost:4399/perf.html?pref=zh";
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 1300, height: 900 } });
|
||||
await page.goto(PAGE, { waitUntil: "networkidle" });
|
||||
await page.waitForSelector(".app");
|
||||
const heights = await page.evaluate(() => {
|
||||
const long = Array.from({ length: 30 }, (_, i) => `line ${i}`).join("\n");
|
||||
const host = document.querySelector(".app");
|
||||
const make = (attrs) => {
|
||||
const box = document.createElement("div");
|
||||
box.innerHTML = `<details class="think" open ${attrs}><summary>t</summary><div class="tk"><div class="tk-in" style="white-space:pre-wrap">${long}</div></div></details>`;
|
||||
host.appendChild(box);
|
||||
return box.querySelector(".tk").getBoundingClientRect().height;
|
||||
};
|
||||
return { windowed: make("data-live"), full: make("data-live data-full") };
|
||||
});
|
||||
assert.ok(heights.windowed < 200, `a live thought is a window of a few lines (${heights.windowed}px)`);
|
||||
assert.ok(heights.full > heights.windowed * 2, `a thought the reader asked to keep open grows with its text (${heights.full}px)`);
|
||||
console.log(`PASS live thinking window ${Math.round(heights.windowed)}px, always-open ${Math.round(heights.full)}px`);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import { chromium } from "playwright";
|
||||
|
||||
const base = process.env.PERF_URL ?? "http://localhost:4399/perf.html";
|
||||
const browser = await chromium.launch();
|
||||
try {
|
||||
await mkdir("perf/shots", { recursive: true });
|
||||
for (const colorScheme of ["light", "dark"]) {
|
||||
const page = await browser.newPage({ viewport: { width: 1400, height: 1000 }, colorScheme, reducedMotion: "reduce" });
|
||||
page.setDefaultTimeout(10000);
|
||||
await page.addInitScript(() => localStorage.setItem("rx-lang", "zh"));
|
||||
await page.goto(`${base}?pref=zh&turns=1`, { waitUntil: "networkidle" });
|
||||
const pane = page.locator(".pane[data-active]");
|
||||
await pane.waitFor();
|
||||
const before = await pane.locator(".think").count();
|
||||
const now = 1_700_000_000_000;
|
||||
await page.clock.setFixedTime(now);
|
||||
const feed = (event) => page.evaluate((ev) => window.__feed(ev), event);
|
||||
await feed({ kind: "turn_started" });
|
||||
await feed({ kind: "reasoning", text: "计时回归" });
|
||||
await pane.locator(".think[data-live]").waitFor();
|
||||
await page.clock.setFixedTime(now + 1200);
|
||||
await page.waitForFunction(() => document.querySelector('.pane[data-active] .think[data-live] .think-label')?.textContent.includes("1.2"));
|
||||
const tool = { id: "clock-call", name: "bash", readOnly: true, issuer: "model" };
|
||||
await feed({ kind: "tool_dispatch", tool: { ...tool, partial: true } });
|
||||
await page.waitForFunction(() => !document.querySelector('.pane[data-active] .think[data-live]'));
|
||||
const labels = () => pane.locator(".think-label:not(.think-out)").allTextContents();
|
||||
assert.equal((await labels()).at(-1), "思考 1.2 秒 · 4 字");
|
||||
await page.clock.setFixedTime(now + 1_800_000);
|
||||
await page.waitForTimeout(200);
|
||||
assert.equal((await labels()).at(-1), "思考 1.2 秒 · 4 字", "a tool's running time never becomes thinking time");
|
||||
await feed({ kind: "text", text: "检查工具输出。" });
|
||||
await feed({ kind: "message", text: "检查工具输出。", reasoning: "计时回归", thoughtMs: 1100 });
|
||||
await feed({ kind: "tool_dispatch", tool });
|
||||
await feed({ kind: "tool_result", tool: { ...tool, output: "done" } });
|
||||
assert.equal(await pane.locator(".think").count(), before + 1, "interleaved tool events keep one response card");
|
||||
await feed({ kind: "reasoning", text: "第二轮" });
|
||||
await page.clock.setFixedTime(now + 1_800_500);
|
||||
await feed({ kind: "message", text: "完成。", reasoning: "第二轮", thoughtMs: 500 });
|
||||
await feed({ kind: "turn_done" });
|
||||
await page.clock.setFixedTime(now + 3_600_000);
|
||||
await page.waitForTimeout(300);
|
||||
assert.equal(await pane.locator(".think[data-live]").count(), 0);
|
||||
assert.deepEqual((await labels()).slice(-2), ["思考 1.1 秒 · 4 字", "思考 0.5 秒 · 3 字"]);
|
||||
await pane.locator(".think").last().scrollIntoViewIfNeeded();
|
||||
await page.screenshot({ path: `perf/shots/thought-clock-${colorScheme}.png` });
|
||||
await page.close();
|
||||
console.log(`PASS ${colorScheme}: completed thoughts stay still after tools, later rounds and another 30 minutes`);
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
@@ -23,7 +23,7 @@ for (const scheme of ["light", "dark"]) {
|
||||
await page.evaluate(() => document.querySelector('[data-action="chrome.settings"]')?.click());
|
||||
await page.waitForSelector('[data-action="settings.section"][data-value="tools"]');
|
||||
await page.evaluate(() => document.querySelector('[data-action="settings.section"][data-value="tools"]')?.click());
|
||||
await page.locator('[data-action="settings.open-page"][data-target="web-access"]').click();
|
||||
await page.locator('[data-action="settings.page-index"][data-target="web-access"]').click();
|
||||
await page.waitForSelector("#set-web-access");
|
||||
const r = await page.evaluate(() => {
|
||||
const g = document.querySelector("#set-web-access");
|
||||
|
||||
Generated
+207
-207
@@ -9,11 +9,11 @@ importers:
|
||||
.:
|
||||
dependencies:
|
||||
'@a2ui/react':
|
||||
specifier: 0.14.0
|
||||
version: 0.14.0([email protected]([email protected]))([email protected])([email protected])
|
||||
specifier: 0.15.0
|
||||
version: 0.15.0([email protected]([email protected]))([email protected])([email protected])
|
||||
'@a2ui/web_core':
|
||||
specifier: 0.14.0
|
||||
version: 0.14.0
|
||||
specifier: 0.15.0
|
||||
version: 0.15.0
|
||||
'@codemirror/commands':
|
||||
specifier: ^6.11.1
|
||||
version: 6.11.1
|
||||
@@ -69,14 +69,14 @@ importers:
|
||||
specifier: ^1.2.5
|
||||
version: 1.2.5
|
||||
'@mantine/charts':
|
||||
specifier: 9.7.4
|
||||
version: 9.7.4(@mantine/[email protected].4(@mantine/[email protected].4([email protected]))(@types/[email protected])([email protected]([email protected]))([email protected]))(@mantine/[email protected].4([email protected]))([email protected]([email protected]))([email protected])([email protected](@types/[email protected])([email protected]([email protected]))([email protected])([email protected])([email protected]))
|
||||
specifier: 9.7.5
|
||||
version: 9.7.5(@mantine/[email protected].5(@mantine/[email protected].5([email protected]))(@types/[email protected])([email protected]([email protected]))([email protected]))(@mantine/[email protected].5([email protected]))([email protected]([email protected]))([email protected])([email protected](@types/[email protected])([email protected]([email protected]))([email protected])([email protected])([email protected]))
|
||||
'@mantine/core':
|
||||
specifier: 9.7.4
|
||||
version: 9.7.4(@mantine/[email protected].4([email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
specifier: 9.7.5
|
||||
version: 9.7.5(@mantine/[email protected].5([email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@mantine/hooks':
|
||||
specifier: 9.7.4
|
||||
version: 9.7.4([email protected])
|
||||
specifier: 9.7.5
|
||||
version: 9.7.5([email protected])
|
||||
'@noble/curves':
|
||||
specifier: 2.4.0
|
||||
version: 2.4.0
|
||||
@@ -105,8 +105,8 @@ importers:
|
||||
specifier: ^0.19.1
|
||||
version: 0.19.1
|
||||
lucide-react:
|
||||
specifier: ^1.64.0
|
||||
version: 1.64.0(@types/[email protected])([email protected])
|
||||
specifier: ^1.69.0
|
||||
version: 1.69.0(@types/[email protected])([email protected])
|
||||
maplibre-gl:
|
||||
specifier: 6.16.0
|
||||
version: 6.16.0
|
||||
@@ -114,8 +114,8 @@ importers:
|
||||
specifier: ^5.7.0
|
||||
version: 5.7.0
|
||||
pdfjs-dist:
|
||||
specifier: ^6.4.299
|
||||
version: 6.4.299
|
||||
specifier: ^6.5.438
|
||||
version: 6.5.438
|
||||
react:
|
||||
specifier: ^19.3.1
|
||||
version: 19.3.1
|
||||
@@ -160,8 +160,8 @@ importers:
|
||||
version: 3.25.76
|
||||
devDependencies:
|
||||
'@babel/parser':
|
||||
specifier: ^8.0.8
|
||||
version: 8.0.8
|
||||
specifier: ^8.1.0
|
||||
version: 8.1.0
|
||||
'@testing-library/react':
|
||||
specifier: ^16.3.3
|
||||
version: 16.3.3(@testing-library/[email protected])(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
@@ -178,8 +178,8 @@ importers:
|
||||
specifier: ^19.3.0
|
||||
version: 19.3.0(@types/[email protected])
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^6.1.2
|
||||
version: 6.1.2([email protected].5(@types/[email protected])([email protected]))
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.1([email protected].6(@types/[email protected])([email protected]))
|
||||
jsdom:
|
||||
specifier: ^30.1.3
|
||||
version: 30.1.3(@noble/[email protected])
|
||||
@@ -193,11 +193,11 @@ importers:
|
||||
specifier: ^7.0.2
|
||||
version: 7.0.2
|
||||
vite:
|
||||
specifier: ^8.3.5
|
||||
version: 8.3.5(@types/[email protected])([email protected])
|
||||
specifier: ^8.3.6
|
||||
version: 8.3.6(@types/[email protected])([email protected])
|
||||
vitest:
|
||||
specifier: ^5.0.5
|
||||
version: 5.0.5(@types/[email protected])([email protected](@noble/[email protected]))([email protected].5(@types/[email protected])([email protected]))
|
||||
specifier: ^5.1.0
|
||||
version: 5.1.0(@types/[email protected])([email protected](@noble/[email protected]))([email protected].6(@types/[email protected])([email protected]))
|
||||
|
||||
packages:
|
||||
|
||||
@@ -206,16 +206,16 @@ packages:
|
||||
peerDependencies:
|
||||
'@a2ui/web_core': ^0.14.0
|
||||
|
||||
'@a2ui/[email protected]4.0':
|
||||
resolution: {integrity: sha512-kew3gVx4/U4qZ41FwyanDndCuBsx3s+5kPzjBt5N9w8E6UKSfwJpOmWD3Vy5WN5bxuiQKmorN+Ozh59ZLrwkMA==}
|
||||
'@a2ui/[email protected]5.0':
|
||||
resolution: {integrity: sha512-SkbdIMoRgtYNGh+nSEvK8YHDSAgS8LVFggDMCYdDqrUAWMLb2PS1unYOL8lfkXjskYK/+VoD7sa6X+I2F+F62w==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
zod: ^3.25.76
|
||||
|
||||
'@a2ui/[email protected]4.0':
|
||||
resolution: {integrity: sha512-OEJOgcfRMW0lwActVl1NKToxE2Ff5G3mIBHDB6gKm0elGZSwAhB7bj+Ef2a/h2nrL+57VPnSR67YSNz9JVV4yw==}
|
||||
'@a2ui/[email protected]5.0':
|
||||
resolution: {integrity: sha512-S3k9rAJ9O55z5TlDeWy22ly3yQnDLO+XBWihlL5JQSuyxaCUmd7jgFnxO4eq/Bn6dSgdBF0WG8cve/euMoz8SA==}
|
||||
|
||||
'@asamuzakjp/[email protected]':
|
||||
resolution: {integrity: sha512-1t1U8Cm3RBl9vh3RAZhjkPaduzLSvQJRToRrxpGR/eDRW99mOREleuu+mWfiFa795bUzjwht7NgphCKTs2ianQ==}
|
||||
@@ -241,8 +241,8 @@ packages:
|
||||
resolution: {integrity: sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==}
|
||||
engines: {node: ^22.18.0 || >=24.11.0}
|
||||
|
||||
'@babel/parser@8.0.8':
|
||||
resolution: {integrity: sha512-Gq65JogAr0S2p0BzvfSJboyuiRnxDQApyOqHkKmkrvm+VlLK4gC3pKsiOKEdnhFBemrFbSnOu90mZZWDpiJnmQ==}
|
||||
'@babel/parser@8.1.0':
|
||||
resolution: {integrity: sha512-fpqZYAC+Tthr19JbEKxXhCGutvfm/+roMe+ULrgUKI+GVyzkQiTMTSheORcWIAzW8T3Oo3W5UavWSvAWmsp7fA==}
|
||||
engines: {node: ^22.18.0 || >=24.11.0}
|
||||
hasBin: true
|
||||
|
||||
@@ -250,8 +250,8 @@ packages:
|
||||
resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@babel/types@8.0.8':
|
||||
resolution: {integrity: sha512-iXSSZAKQWmaeAwGtFw10v9fg5hUiUs0L+w+Z2K11kym3pDNK+SF0VcAQPsEFvWcTSZ7LMgClMXDBvTbH0IlICg==}
|
||||
'@babel/types@8.1.0':
|
||||
resolution: {integrity: sha512-8V8aSTDx012YPmDQF30vrokyxVFdRAU2YBS2VzrnfLAGkP8floVnjckwgFcD0Jrvf6GWMSRdCrYVsk3tyMwZRg==}
|
||||
engines: {node: ^22.18.0 || >=24.11.0}
|
||||
|
||||
'@bramus/[email protected]':
|
||||
@@ -607,24 +607,24 @@ packages:
|
||||
'@lit/[email protected]':
|
||||
resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==}
|
||||
|
||||
'@mantine/[email protected].4':
|
||||
resolution: {integrity: sha512-liMa0KIxcbE7goA0YqslEj9Ir8NVBV2158vW11J8g20+yrYe6fAqFu0nrtFMqN0Jm72wU0DMqCqZcrsKlWf0DQ==}
|
||||
'@mantine/[email protected].5':
|
||||
resolution: {integrity: sha512-udNEXI9NWs/BOXNOS868O4dNuQDzBNO9hP3ahFRe/i1It7kbFGlZ2P5y0HNYZ4+a38+coUeTuYaN7AJ7a41BiA==}
|
||||
peerDependencies:
|
||||
'@mantine/core': 9.7.4
|
||||
'@mantine/hooks': 9.7.4
|
||||
'@mantine/core': 9.7.5
|
||||
'@mantine/hooks': 9.7.5
|
||||
react: ^19.2.0
|
||||
react-dom: ^19.2.0
|
||||
recharts: '>=3.2.1'
|
||||
|
||||
'@mantine/[email protected].4':
|
||||
resolution: {integrity: sha512-c7aTGELNn74CxG9i5/tq9BHGh0lKdKmGGz+9iDF+gR7Ipt4U1TMM286lAmLpRrKL2bR+UEaN4blZ8UJmOuGJ4Q==}
|
||||
'@mantine/[email protected].5':
|
||||
resolution: {integrity: sha512-NiOTHpAZssi1Z5ISZBvcXEA4XDn0BWcxwpmmUthFx/eL0CsJ18FXMn/ydRCJmoeMGcIKmRE+pjt90KM70GOsUw==}
|
||||
peerDependencies:
|
||||
'@mantine/hooks': 9.7.4
|
||||
'@mantine/hooks': 9.7.5
|
||||
react: ^19.2.0
|
||||
react-dom: ^19.2.0
|
||||
|
||||
'@mantine/[email protected].4':
|
||||
resolution: {integrity: sha512-j6Xf9Sc1fhhGEKXg6BZA2geyP/Czqvo6Rsi0IduOCWgwl/MAAJbKRjRc7xe2+9vePLdfqLZqRIAOY5rta88uNA==}
|
||||
'@mantine/[email protected].5':
|
||||
resolution: {integrity: sha512-+3cNisLOUkzF77SjRKkG/V+svZii9WDGvcwlaxFH58b14MNohz1zWbV+WfjZt9qfzrIPPUOedks/z6PMo5rAFA==}
|
||||
peerDependencies:
|
||||
react: ^19.2.0
|
||||
|
||||
@@ -667,79 +667,79 @@ packages:
|
||||
'@marijn/[email protected]':
|
||||
resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==}
|
||||
|
||||
'@napi-rs/canvas-android-arm64@1.0.12':
|
||||
resolution: {integrity: sha512-Z8hg9BD7ABaZNhdM8O96IrfBaYLM7KzO/YyKA8eU9BDdc5fZJRTWyOhSAyOv59/ari6bOYa+p96h7zN6wbJRDA==}
|
||||
'@napi-rs/canvas-android-arm64@1.1.0':
|
||||
resolution: {integrity: sha512-mKrSmRyhWK4q699Q1xthbZIZi7JBvLtsRNnOZAeFaLPOFT1yo8y82aRC2nNZbXTGJXU7fVDONgGQPr0vo3kerw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@1.0.12':
|
||||
resolution: {integrity: sha512-dRtZCi/u1IOZyRjEDCA73PqgWakf5689K2jfv4uPRYGMJvEqtV+Eq7MIQkBJLmdfUPe7AktSiBZRml2pggnjRQ==}
|
||||
'@napi-rs/canvas-darwin-arm64@1.1.0':
|
||||
resolution: {integrity: sha512-iqwST4g0oH+x8VCpBIYh+MYokv/mVvkM+v9oJGB0d9A3P2qBh42ogJlVyWq9SpPCrAjS7tplMb1D3OIfxl+utw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@1.0.12':
|
||||
resolution: {integrity: sha512-YwvMgK5NBXwPY1AjZGXfPCG3hp/OFGM0Kw+JZHCOR/FYaeSOftaZRyxaDEGkJQSLEUZZIMNJXKCWgZYSzgWZEA==}
|
||||
'@napi-rs/canvas-darwin-x64@1.1.0':
|
||||
resolution: {integrity: sha512-jkeA8eVTxDum03LTvc32YJ28F6KB1U0gYwnWMfOAUQZvWWEh/kvONby4iWGJx3+Tw45UqMGMXjtw2GKNVZDhVg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@1.0.12':
|
||||
resolution: {integrity: sha512-LlOsZy8qh+qJq1euEvDZqa9muACS1fL1Ag38c7J+axAnA8NeXazMtqmUyKb4NVyft6DqbU/MesEj3C0bAFnv4Q==}
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@1.1.0':
|
||||
resolution: {integrity: sha512-Y4YkKG1ijoqkgUdTEEBNG4k+2BK5bR5jGL6S56YxCXYPwGfUd5nED8/74xyc7z+bpixPasCjSYoP3xYPOIGNxw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@1.0.12':
|
||||
resolution: {integrity: sha512-nvuqhi85GzaFl/6sid4qf7wwjHuGU6gfIuDyywUb/fB+kp6Fp3h8GyIAKss/QPil3zVO2r8g6O9ah+IOUoBrIw==}
|
||||
'@napi-rs/canvas-linux-arm64-gnu@1.1.0':
|
||||
resolution: {integrity: sha512-dE/XthqYkiWcBVtKFlZbUXwAYfDuQGqDWixznipJNmevLGpprNnTVQZyftm1RZJudkIOn2EkzYZDMMETM08Row==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@1.0.12':
|
||||
resolution: {integrity: sha512-Bf6+9PoEr9K06P/oyMab7zZOpW90KimbIuNsy/vL0VhZZKsb0B0rCC78cp0jXhK6PIM7v8t6uX1sn7isOZ8KEw==}
|
||||
'@napi-rs/canvas-linux-arm64-musl@1.1.0':
|
||||
resolution: {integrity: sha512-DqZ/CMcHlhU56pPjv9AP0h3U7MFQ5LzC1HzMyRMotL+PFsJmZJA/ziApPYVyyz78DaVLhI8t3So3ztqtijbYpg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@1.0.12':
|
||||
resolution: {integrity: sha512-j7VcZduLSRx60nv/wbdyH+fyB5QriWGuO8GrvMDlFlt7GNUKAw5lHAHpcuuQW3QwLOe+3P+3qryzyT3sW5pCqQ==}
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@1.1.0':
|
||||
resolution: {integrity: sha512-kJ7m5n2VffxWXM5QGa5lfq8ZN4LdOJEoz3aAJAmIqYvKiZw1gRdXqpucnRXsF6D9nLYiiIlevaAtAclR7c5ilg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@1.0.12':
|
||||
resolution: {integrity: sha512-twDb8ZNgoCF8EG6ka+Lk4etU+ea9lv6NiV5ptIf+thz3LgWuHBbt85c+YgL1s05VsHe4WYWK0f+GhR45IuqARg==}
|
||||
'@napi-rs/canvas-linux-x64-gnu@1.1.0':
|
||||
resolution: {integrity: sha512-uQ9JuJlRhQdHyttcMWOiwLkfIqkqaB0YFUVe835LUe12THt2b51HXkTItq0fxWT1jdhUPX2ylWZ1walkJyD6GQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@1.0.12':
|
||||
resolution: {integrity: sha512-mtZbBeNCdGA0MYQxivPmMOp5JMH80LaJLQeEMg7Nne517fvgGb+DWTJTmoE/qUW8YJ4nkiP2vQyr1XVGrH/Ktg==}
|
||||
'@napi-rs/canvas-linux-x64-musl@1.1.0':
|
||||
resolution: {integrity: sha512-has4CPPAkkCac/LZ13VAw5p+MJLdaKVoyrhumN/A7uJd6960xYkJNDZgUebyxiha82AFzXx6RV+QJg5qGG079g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@1.0.12':
|
||||
resolution: {integrity: sha512-HgGRuBUkFL8U99ogwUnTz3SmwUu7WKjmj0eOVWRqVZTPPyfWGEIMr+ycb8oDoNZcKM9vSiF1rpy3r+UIqqaKqw==}
|
||||
'@napi-rs/canvas-win32-arm64-msvc@1.1.0':
|
||||
resolution: {integrity: sha512-MJnktLKNijmXqdjTughHDZ9TDLZ9uUm2hvFXqaOqn0+w7lphXFnft6bpSgQAc9U1exc20UGIBlzGu9+kiCj9Ag==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@1.0.12':
|
||||
resolution: {integrity: sha512-SEEfblIAnYeS4UkeFPp9jw6ZWuHxaTqumUuksMgnMXmPLW8Ofb21uu7cujhw+lk4E9V3aWgGAE9f8g+CdoAU0Q==}
|
||||
'@napi-rs/canvas-win32-x64-msvc@1.1.0':
|
||||
resolution: {integrity: sha512-08575bFk7iiadxap4h+JxgGut9xDCHAGum6wh0eInN53zEMImkmEHi5MushX2FOmNO7qRNX7BPiDC5ZYBwHeEg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas@1.0.12':
|
||||
resolution: {integrity: sha512-PSwhRdvizfuiSDlhE4Ld2jl/JqkrbSFFyZO0ycamE6mBpf1H0uUF3Tm8yQaVDeAwCNNDzxjRfrN22KmWLTDwcw==}
|
||||
'@napi-rs/canvas@1.1.0':
|
||||
resolution: {integrity: sha512-k+ph/D8H9GS8ZZ48okvHrKeJynkqAA1kMUZRuzRWNlQUDHXngq7oRaWMZBdmSfd9ZhVy9bwNKei/7wMboI/uMg==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@noble/[email protected]':
|
||||
@@ -750,8 +750,8 @@ packages:
|
||||
resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
'@oxc-project/[email protected]5.0':
|
||||
resolution: {integrity: sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==}
|
||||
'@oxc-project/[email protected]6.0':
|
||||
resolution: {integrity: sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==}
|
||||
|
||||
'@preact/[email protected]':
|
||||
resolution: {integrity: sha512-HNB6HYeYKhQbJ1aKl+YRjrS4+QWHLKX6qKoUsfS/m0vqzsVaEBiZiaKbG/e+NKk2ch5ALQr/ihWaMHxiCuuWHA==}
|
||||
@@ -767,98 +767,98 @@ packages:
|
||||
react-redux:
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
resolution: {integrity: sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==}
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
resolution: {integrity: sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==}
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
resolution: {integrity: sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==}
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
resolution: {integrity: sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==}
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
resolution: {integrity: sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==}
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
resolution: {integrity: sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==}
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
resolution: {integrity: sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==}
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
resolution: {integrity: sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==}
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
resolution: {integrity: sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==}
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
resolution: {integrity: sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==}
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
resolution: {integrity: sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==}
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
resolution: {integrity: sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==}
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
resolution: {integrity: sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==}
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
resolution: {integrity: sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==}
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
resolution: {integrity: sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==}
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -1129,13 +1129,13 @@ packages:
|
||||
maplibre-gl:
|
||||
optional: true
|
||||
|
||||
'@vitejs/plugin-react@6.1.2':
|
||||
resolution: {integrity: sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==}
|
||||
'@vitejs/plugin-react@6.2.1':
|
||||
resolution: {integrity: sha512-9KyWCij3r0+qLG3Q62vBmv0VyevmYibAtxaCIVK9o0k6CXgaajZRIe8tpfchQcdZArJ5b/7OrIz4W3b2uxVRDA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
peerDependencies:
|
||||
'@rolldown/plugin-babel': ^0.1.7 || ^0.2.0
|
||||
'@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 || ^0.3.0
|
||||
babel-plugin-react-compiler: ^1.0.0
|
||||
oxc-transform-react: ^0.152.0
|
||||
oxc-transform-react: '>=0.152.0 <0.157.0'
|
||||
vite: ^8.0.0
|
||||
peerDependenciesMeta:
|
||||
'@rolldown/plugin-babel':
|
||||
@@ -1145,8 +1145,8 @@ packages:
|
||||
oxc-transform-react:
|
||||
optional: true
|
||||
|
||||
'@vitest/mocker@5.0.5':
|
||||
resolution: {integrity: sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==}
|
||||
'@vitest/mocker@5.1.0':
|
||||
resolution: {integrity: sha512-5umB9/jlWcqVS6fsrFmv5Gdnpu6wAUio/lEOwAmvB7v+lFo/IW2B9FhAD0p19GCULTgMzLCdUmuCDCnAzWgppQ==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9 || ^3.0.0
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
@@ -1156,8 +1156,8 @@ packages:
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/spy@5.0.5':
|
||||
resolution: {integrity: sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==}
|
||||
'@vitest/spy@5.1.0':
|
||||
resolution: {integrity: sha512-2DPO5OMHWKrBngC+fCmTQqemNF0gidRhp3lxbBmRhW+M1XJrfgPpqNqWVevXtn2f2ufD5ZaR1B6B641aAlcX5A==}
|
||||
|
||||
'@xterm/[email protected]':
|
||||
resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==}
|
||||
@@ -2075,8 +2075,8 @@ packages:
|
||||
resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==}
|
||||
engines: {node: 20 || >=22}
|
||||
|
||||
[email protected]4.0:
|
||||
resolution: {integrity: sha512-v626F/bcLNvFom6vzztHILtG9tUgXXbgA5AGkd82YkV401+Ko/NHyrWafbPwGofa+6p/EgDJcvEkIwsMH9098w==}
|
||||
[email protected]9.0:
|
||||
resolution: {integrity: sha512-UTFECm4yEgkgMN0kfLuxWcmlk+4u02Jrqehgk3ZXRx/Ud68OpP7hjkp67bvkBHmIpMaZCIveF1QDYeupO7XmYQ==}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
@@ -2322,8 +2322,8 @@ packages:
|
||||
resolution: {integrity: sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w==}
|
||||
hasBin: true
|
||||
|
||||
pdfjs-dist@6.4.299:
|
||||
resolution: {integrity: sha512-AVl138zALtfaAPvADulE0PZThbYzCBS79nL4pOSL/6Sm/4AH5A21BD9VHt97OlCuzJuCpmeZtAtkinisF4Vb1g==}
|
||||
pdfjs-dist@6.5.438:
|
||||
resolution: {integrity: sha512-vg8Pw1x+AFKy4AMDvhuWl5cOYV8d698UoEANoFFhc79hqXDnZQqnOXoxw3HG907WHOg1JYOvLhP756boddFxpA==}
|
||||
engines: {node: '>=22.13.0 || >=24'}
|
||||
|
||||
[email protected]:
|
||||
@@ -2347,8 +2347,8 @@ packages:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
[email protected]0:
|
||||
resolution: {integrity: sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==}
|
||||
[email protected]2:
|
||||
resolution: {integrity: sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==}
|
||||
engines: {node: ^10 || ^12 || >=14}
|
||||
|
||||
[email protected]:
|
||||
@@ -2545,8 +2545,8 @@ packages:
|
||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
hasBin: true
|
||||
|
||||
[email protected]5:
|
||||
resolution: {integrity: sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==}
|
||||
[email protected]6:
|
||||
resolution: {integrity: sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
@@ -2874,8 +2874,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==}
|
||||
|
||||
[email protected].5:
|
||||
resolution: {integrity: sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==}
|
||||
[email protected].6:
|
||||
resolution: {integrity: sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -2917,20 +2917,20 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
vitest@5.0.5:
|
||||
resolution: {integrity: sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==}
|
||||
vitest@5.1.0:
|
||||
resolution: {integrity: sha512-MzmNFnrwa8v8lF1bLxw3UJ4EREwVOVb75VqJZmUDRjJ6EluK/x9th7qTbaeDk41WxE1GxWqmTXcdsfobfH5sOA==}
|
||||
engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@edge-runtime/vm': '*'
|
||||
'@opentelemetry/api': ^1.9.0
|
||||
'@types/node': ^22.0.0 || >=24.0.0
|
||||
'@vitest/browser-playwright': 5.0.5
|
||||
'@vitest/browser-preview': 5.0.5
|
||||
'@vitest/browser-playwright': 5.1.0
|
||||
'@vitest/browser-preview': 5.1.0
|
||||
'@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0
|
||||
'@vitest/coverage-istanbul': 5.0.5
|
||||
'@vitest/coverage-v8': 5.0.5
|
||||
'@vitest/ui': 5.0.5
|
||||
'@vitest/coverage-istanbul': 5.1.0
|
||||
'@vitest/coverage-v8': 5.1.0
|
||||
'@vitest/ui': 5.1.0
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
vite: ^6.4.0 || ^7.0.0 || ^8.0.0
|
||||
@@ -3028,23 +3028,23 @@ packages:
|
||||
|
||||
snapshots:
|
||||
|
||||
'@a2ui/[email protected](@a2ui/[email protected]4.0)':
|
||||
'@a2ui/[email protected](@a2ui/[email protected]5.0)':
|
||||
dependencies:
|
||||
'@a2ui/web_core': 0.14.0
|
||||
'@a2ui/web_core': 0.15.0
|
||||
dompurify: 3.4.11
|
||||
markdown-it: 14.3.2
|
||||
|
||||
'@a2ui/[email protected]4.0([email protected]([email protected]))([email protected])([email protected])':
|
||||
'@a2ui/[email protected]5.0([email protected]([email protected]))([email protected])([email protected])':
|
||||
dependencies:
|
||||
'@a2ui/markdown-it': 0.2.2(@a2ui/[email protected]4.0)
|
||||
'@a2ui/web_core': 0.14.0
|
||||
'@a2ui/markdown-it': 0.2.2(@a2ui/[email protected]5.0)
|
||||
'@a2ui/web_core': 0.15.0
|
||||
clsx: 2.1.1
|
||||
markdown-it: 14.3.2
|
||||
react: 19.3.1
|
||||
react-dom: 19.3.1([email protected])
|
||||
zod: 3.25.76
|
||||
|
||||
'@a2ui/[email protected]4.0':
|
||||
'@a2ui/[email protected]5.0':
|
||||
dependencies:
|
||||
'@lit/context': 1.1.6
|
||||
'@preact/signals-core': 1.14.4
|
||||
@@ -3081,13 +3081,13 @@ snapshots:
|
||||
|
||||
'@babel/[email protected]': {}
|
||||
|
||||
'@babel/parser@8.0.8':
|
||||
'@babel/parser@8.1.0':
|
||||
dependencies:
|
||||
'@babel/types': 8.0.8
|
||||
'@babel/types': 8.1.0
|
||||
|
||||
'@babel/[email protected]': {}
|
||||
|
||||
'@babel/types@8.0.8':
|
||||
'@babel/types@8.1.0':
|
||||
dependencies:
|
||||
'@babel/helper-string-parser': 8.0.8
|
||||
'@babel/helper-validator-identifier': 8.0.6
|
||||
@@ -3488,18 +3488,18 @@ snapshots:
|
||||
dependencies:
|
||||
'@lit-labs/ssr-dom-shim': 1.6.0
|
||||
|
||||
'@mantine/[email protected].4(@mantine/[email protected].4(@mantine/[email protected].4([email protected]))(@types/[email protected])([email protected]([email protected]))([email protected]))(@mantine/[email protected].4([email protected]))([email protected]([email protected]))([email protected])([email protected](@types/[email protected])([email protected]([email protected]))([email protected])([email protected])([email protected]))':
|
||||
'@mantine/[email protected].5(@mantine/[email protected].5(@mantine/[email protected].5([email protected]))(@types/[email protected])([email protected]([email protected]))([email protected]))(@mantine/[email protected].5([email protected]))([email protected]([email protected]))([email protected])([email protected](@types/[email protected])([email protected]([email protected]))([email protected])([email protected])([email protected]))':
|
||||
dependencies:
|
||||
'@mantine/core': 9.7.4(@mantine/[email protected].4([email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@mantine/hooks': 9.7.4([email protected])
|
||||
'@mantine/core': 9.7.5(@mantine/[email protected].5([email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
|
||||
'@mantine/hooks': 9.7.5([email protected])
|
||||
react: 19.3.1
|
||||
react-dom: 19.3.1([email protected])
|
||||
recharts: 3.10.1(@types/[email protected])([email protected]([email protected]))([email protected])([email protected])([email protected])
|
||||
|
||||
'@mantine/[email protected].4(@mantine/[email protected].4([email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
|
||||
'@mantine/[email protected].5(@mantine/[email protected].5([email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
|
||||
dependencies:
|
||||
'@floating-ui/react': 0.27.20([email protected]([email protected]))([email protected])
|
||||
'@mantine/hooks': 9.7.4([email protected])
|
||||
'@mantine/hooks': 9.7.5([email protected])
|
||||
clsx: 2.1.1
|
||||
react: 19.3.1
|
||||
react-dom: 19.3.1([email protected])
|
||||
@@ -3509,7 +3509,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
|
||||
'@mantine/[email protected].4([email protected])':
|
||||
'@mantine/[email protected].5([email protected])':
|
||||
dependencies:
|
||||
react: 19.3.1
|
||||
|
||||
@@ -3563,52 +3563,52 @@ snapshots:
|
||||
|
||||
'@marijn/[email protected]': {}
|
||||
|
||||
'@napi-rs/canvas-android-arm64@1.0.12':
|
||||
'@napi-rs/canvas-android-arm64@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@1.0.12':
|
||||
'@napi-rs/canvas-darwin-arm64@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@1.0.12':
|
||||
'@napi-rs/canvas-darwin-x64@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@1.0.12':
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@1.0.12':
|
||||
'@napi-rs/canvas-linux-arm64-gnu@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@1.0.12':
|
||||
'@napi-rs/canvas-linux-arm64-musl@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@1.0.12':
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@1.0.12':
|
||||
'@napi-rs/canvas-linux-x64-gnu@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@1.0.12':
|
||||
'@napi-rs/canvas-linux-x64-musl@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@1.0.12':
|
||||
'@napi-rs/canvas-win32-arm64-msvc@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@1.0.12':
|
||||
'@napi-rs/canvas-win32-x64-msvc@1.1.0':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas@1.0.12':
|
||||
'@napi-rs/canvas@1.1.0':
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas-android-arm64': 1.0.12
|
||||
'@napi-rs/canvas-darwin-arm64': 1.0.12
|
||||
'@napi-rs/canvas-darwin-x64': 1.0.12
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf': 1.0.12
|
||||
'@napi-rs/canvas-linux-arm64-gnu': 1.0.12
|
||||
'@napi-rs/canvas-linux-arm64-musl': 1.0.12
|
||||
'@napi-rs/canvas-linux-riscv64-gnu': 1.0.12
|
||||
'@napi-rs/canvas-linux-x64-gnu': 1.0.12
|
||||
'@napi-rs/canvas-linux-x64-musl': 1.0.12
|
||||
'@napi-rs/canvas-win32-arm64-msvc': 1.0.12
|
||||
'@napi-rs/canvas-win32-x64-msvc': 1.0.12
|
||||
'@napi-rs/canvas-android-arm64': 1.1.0
|
||||
'@napi-rs/canvas-darwin-arm64': 1.1.0
|
||||
'@napi-rs/canvas-darwin-x64': 1.1.0
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf': 1.1.0
|
||||
'@napi-rs/canvas-linux-arm64-gnu': 1.1.0
|
||||
'@napi-rs/canvas-linux-arm64-musl': 1.1.0
|
||||
'@napi-rs/canvas-linux-riscv64-gnu': 1.1.0
|
||||
'@napi-rs/canvas-linux-x64-gnu': 1.1.0
|
||||
'@napi-rs/canvas-linux-x64-musl': 1.1.0
|
||||
'@napi-rs/canvas-win32-arm64-msvc': 1.1.0
|
||||
'@napi-rs/canvas-win32-x64-msvc': 1.1.0
|
||||
optional: true
|
||||
|
||||
'@noble/[email protected]':
|
||||
@@ -3617,7 +3617,7 @@ snapshots:
|
||||
|
||||
'@noble/[email protected]': {}
|
||||
|
||||
'@oxc-project/[email protected]5.0': {}
|
||||
'@oxc-project/[email protected]6.0': {}
|
||||
|
||||
'@preact/[email protected]': {}
|
||||
|
||||
@@ -3633,49 +3633,49 @@ snapshots:
|
||||
react: 19.3.1
|
||||
react-redux: 9.3.0(@types/[email protected])([email protected])([email protected])
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
'@rolldown/[email protected]6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
'@rolldown/[email protected]6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
'@rolldown/[email protected]6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
'@rolldown/[email protected]6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
'@rolldown/[email protected]6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
'@rolldown/[email protected]6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
'@rolldown/[email protected]6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
'@rolldown/[email protected]6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
'@rolldown/[email protected]6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
'@rolldown/[email protected]6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
'@rolldown/[email protected]6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
'@rolldown/[email protected]6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
'@rolldown/[email protected]6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
'@rolldown/[email protected]6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]5':
|
||||
'@rolldown/[email protected]6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]': {}
|
||||
@@ -3867,21 +3867,21 @@ snapshots:
|
||||
optionalDependencies:
|
||||
maplibre-gl: 6.16.0
|
||||
|
||||
'@vitejs/plugin-react@6.1.2([email protected].5(@types/[email protected])([email protected]))':
|
||||
'@vitejs/plugin-react@6.2.1([email protected].6(@types/[email protected])([email protected]))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.1
|
||||
vite: 8.3.5(@types/[email protected])([email protected])
|
||||
vite: 8.3.6(@types/[email protected])([email protected])
|
||||
|
||||
'@vitest/mocker@5.0.5([email protected].5(@types/[email protected])([email protected]))':
|
||||
'@vitest/mocker@5.1.0([email protected].6(@types/[email protected])([email protected]))':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
'@vitest/spy': 5.0.5
|
||||
'@vitest/spy': 5.1.0
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 1.4.3
|
||||
optionalDependencies:
|
||||
vite: 8.3.5(@types/[email protected])([email protected])
|
||||
vite: 8.3.6(@types/[email protected])([email protected])
|
||||
|
||||
'@vitest/spy@5.0.5': {}
|
||||
'@vitest/spy@5.1.0': {}
|
||||
|
||||
'@xterm/[email protected]': {}
|
||||
|
||||
@@ -4948,7 +4948,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]4.0(@types/[email protected])([email protected]):
|
||||
[email protected]9.0(@types/[email protected])([email protected]):
|
||||
dependencies:
|
||||
react: 19.3.1
|
||||
optionalDependencies:
|
||||
@@ -5440,9 +5440,9 @@ snapshots:
|
||||
dependencies:
|
||||
resolve-protobuf-schema: 2.1.0
|
||||
|
||||
pdfjs-dist@6.4.299:
|
||||
pdfjs-dist@6.5.438:
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas': 1.0.12
|
||||
'@napi-rs/canvas': 1.1.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -5456,7 +5456,7 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]0:
|
||||
[email protected]2:
|
||||
dependencies:
|
||||
nanoid: 3.3.20
|
||||
picocolors: 1.1.1
|
||||
@@ -5723,26 +5723,26 @@ snapshots:
|
||||
dependencies:
|
||||
glob: 7.2.3
|
||||
|
||||
[email protected]5:
|
||||
[email protected]6:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.155.0
|
||||
'@oxc-project/types': 0.156.0
|
||||
'@rolldown/pluginutils': 1.0.1
|
||||
optionalDependencies:
|
||||
'@rolldown/binding-android-arm-eabi': 1.2.15
|
||||
'@rolldown/binding-android-arm64': 1.2.15
|
||||
'@rolldown/binding-darwin-arm64': 1.2.15
|
||||
'@rolldown/binding-darwin-x64': 1.2.15
|
||||
'@rolldown/binding-freebsd-x64': 1.2.15
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.2.15
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.2.15
|
||||
'@rolldown/binding-linux-arm64-musl': 1.2.15
|
||||
'@rolldown/binding-linux-ppc64-gnu': 1.2.15
|
||||
'@rolldown/binding-linux-s390x-gnu': 1.2.15
|
||||
'@rolldown/binding-linux-x64-gnu': 1.2.15
|
||||
'@rolldown/binding-linux-x64-musl': 1.2.15
|
||||
'@rolldown/binding-openharmony-arm64': 1.2.15
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.2.15
|
||||
'@rolldown/binding-win32-x64-msvc': 1.2.15
|
||||
'@rolldown/binding-android-arm-eabi': 1.2.16
|
||||
'@rolldown/binding-android-arm64': 1.2.16
|
||||
'@rolldown/binding-darwin-arm64': 1.2.16
|
||||
'@rolldown/binding-darwin-x64': 1.2.16
|
||||
'@rolldown/binding-freebsd-x64': 1.2.16
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.2.16
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.2.16
|
||||
'@rolldown/binding-linux-arm64-musl': 1.2.16
|
||||
'@rolldown/binding-linux-ppc64-gnu': 1.2.16
|
||||
'@rolldown/binding-linux-s390x-gnu': 1.2.16
|
||||
'@rolldown/binding-linux-x64-gnu': 1.2.16
|
||||
'@rolldown/binding-linux-x64-musl': 1.2.16
|
||||
'@rolldown/binding-openharmony-arm64': 1.2.16
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.2.16
|
||||
'@rolldown/binding-win32-x64-msvc': 1.2.16
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -6183,22 +6183,22 @@ snapshots:
|
||||
d3-time: 3.1.0
|
||||
d3-timer: 3.0.1
|
||||
|
||||
[email protected].5(@types/[email protected])([email protected]):
|
||||
[email protected].6(@types/[email protected])([email protected]):
|
||||
dependencies:
|
||||
lightningcss: 1.33.0
|
||||
picomatch: 4.0.7
|
||||
postcss: 8.5.30
|
||||
rolldown: 1.2.15
|
||||
postcss: 8.5.32
|
||||
rolldown: 1.2.16
|
||||
tinyglobby: 0.2.17
|
||||
optionalDependencies:
|
||||
'@types/node': 26.6.5
|
||||
esbuild: 0.28.2
|
||||
fsevents: 2.3.3
|
||||
|
||||
vitest@5.0.5(@types/[email protected])([email protected](@noble/[email protected]))([email protected].5(@types/[email protected])([email protected])):
|
||||
vitest@5.1.0(@types/[email protected])([email protected](@noble/[email protected]))([email protected].6(@types/[email protected])([email protected])):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/mocker': 5.0.5([email protected].5(@types/[email protected])([email protected]))
|
||||
'@vitest/mocker': 5.1.0([email protected].6(@types/[email protected])([email protected]))
|
||||
chai: 6.3.0
|
||||
es-module-lexer: 2.3.2
|
||||
expect-type: 1.4.0
|
||||
@@ -6209,7 +6209,7 @@ snapshots:
|
||||
tinybench: 6.2.2
|
||||
tinyexec: 1.3.1
|
||||
tinyglobby: 0.2.17
|
||||
vite: 8.3.5(@types/[email protected])([email protected])
|
||||
vite: 8.3.6(@types/[email protected])([email protected])
|
||||
why-is-node-running: 3.2.1
|
||||
optionalDependencies:
|
||||
'@types/node': 26.6.5
|
||||
|
||||
@@ -577,7 +577,6 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "tray.icon", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "tray.close-to-tray", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "notify.enabled", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "notify.more", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "notify.turn-done", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "notify.approval", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "notify.ask", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
@@ -651,8 +650,8 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "appearance.effects", kind: "view", target: "none", proof: "browser" },
|
||||
{ id: "pane.close", kind: "destructive", target: "none", proof: "authority-effect" },
|
||||
{ id: "settings.section", kind: "navigation", target: "none", proof: "interaction" },
|
||||
{ id: "settings.open-page", kind: "navigation", target: "entity", proof: "interaction" },
|
||||
{ id: "settings.close-page", kind: "navigation", target: "none", proof: "interaction" },
|
||||
{ id: "settings.edit-persona", kind: "navigation", target: "none", proof: "interaction" },
|
||||
{ id: "sidebar.tools", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "settings.page", kind: "navigation", target: "none", proof: "interaction" },
|
||||
// Finding a setting changes what is on screen and nothing else: it reaches
|
||||
// no kernel state, and the jump it offers is settings.section, because
|
||||
|
||||
@@ -16,7 +16,6 @@ export const WORKBENCH_ACTIONS: UIAction[] = [
|
||||
{ id: "workbench.change-open", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "workbench.section-fold", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "workbench.changes-page", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "workbench.launch-find", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "workbench.dismiss", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "workbench.launch-browser", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "workbench.launch-terminal", kind: "view", target: "none", proof: "interaction" },
|
||||
|
||||
@@ -90,11 +90,12 @@
|
||||
.compose .btn.send { border: 0; background: var(--accent); color: var(--accent-fg); }
|
||||
.compose .btn.send:hover:not(:disabled) { background: var(--accent); filter: brightness(1.18); }
|
||||
.compose .btn.send:disabled { background: var(--overlay); color: var(--ghost); filter: none; opacity: 1; }
|
||||
.compose .btn.send:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; box-shadow: none; }
|
||||
.compose .btn.send:focus-visible, .compose .btn.stop:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; box-shadow: none; }
|
||||
.compose .btn.send[data-running]:not(:disabled) { width: auto; display: inline-flex; gap: 5px; padding: 0 12px 0 10px; border-radius: var(--r-pill); background: var(--accent); color: var(--accent-fg); font: var(--w-med) var(--fs-ui)/1 var(--ui); }
|
||||
.compose .btn.stop, .compose .btn.stop[data-primary] { border: 1px solid var(--err); background: transparent; color: var(--err-ink); }
|
||||
.compose .btn.stop:hover { background: color-mix(in srgb, var(--err) 9%, transparent); border-color: var(--err); color: var(--err-ink); }
|
||||
.compose .btn :is(.ic, .ic > svg) { width: 13px; height: 13px; }
|
||||
.compose .btn.stop, .compose .btn.stop[data-primary] { border: 0; background: color-mix(in srgb, var(--err) 12%, transparent); color: var(--err-ink); }
|
||||
.compose .btn.stop:hover:not(:disabled) { background: color-mix(in srgb, var(--err) 20%, transparent); color: var(--err-ink); }
|
||||
.compose .btn :is(.ic, .ic > svg) { width: 18px; height: 18px; }
|
||||
.compose .btn.send .ic > svg { stroke-width: 2; }
|
||||
|
||||
.studio-meterrail { left: 0; right: 0; padding: 0 2px; gap: 0; color: var(--faint); font: var(--w-reg) var(--fs-micro)/1 var(--ui); }
|
||||
.studio-meterrail b { color: var(--muted); font-weight: var(--w-med); }
|
||||
@@ -132,6 +133,14 @@
|
||||
:root .composer-menu.studio-model-menu .studio-item-line { flex-wrap: nowrap; align-items: center; gap: 8px; }
|
||||
:root .composer-menu.studio-model-menu .studio-item-line > .lb { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font: var(--w-med) var(--fs-ui)/1.3 var(--ui); }
|
||||
:root .composer-menu.studio-model-menu .studio-item-meta { flex: none; color: var(--faint); font: var(--w-reg) var(--fs-micro)/1.3 var(--ui); white-space: nowrap; }
|
||||
:root .composer-menu.studio-model-menu .studio-item-badge { flex: none; white-space: nowrap; }
|
||||
:root .composer-menu.studio-model-menu .menu-rows .mi.head .lb { min-width: 0; max-width: 50%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
:root .composer-menu.studio-model-menu .menu-rows .mi.head .rt { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
:root .composer-menu.studio-model-menu .menu-rows .studio-item-line { display: grid; grid-template-columns: minmax(0, 1fr) max-content; align-items: center; gap: 2px 8px; }
|
||||
:root .composer-menu.studio-model-menu .menu-rows .studio-item-line > .lb { grid-column: 1 / -1; }
|
||||
:root .composer-menu.studio-model-menu .menu-rows .studio-item-meta { grid-column: 1; grid-row: 2; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
:root .composer-menu.studio-model-menu .menu-rows .studio-item-badge { grid-column: 2; grid-row: 2; }
|
||||
:root .composer-menu.studio-model-menu .menu-rows .mi .tx > .ds { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
:root .composer-menu.studio-model-menu .mi[data-on]::after { color: var(--net-ink); }
|
||||
:root .composer-menu.studio-effort-menu .mi.head { padding: 12px 14px 8px; }
|
||||
:root .composer-menu.studio-effort-menu .mi.head:first-child .lb { color: var(--text); font: var(--w-semi) var(--fs-ui)/1.3 var(--ui); }
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
.onb-demo .d.tool code { color: var(--faint); font: var(--w-reg) var(--fs-micro) var(--mono); }
|
||||
.onb-demo .d.tool small { margin-left: auto; color: var(--faint); font: var(--w-reg) var(--fs-micro) var(--mono); }
|
||||
.onb-demo .d.tool small.add { color: var(--add); }
|
||||
.onb-demo .d.tool small.ok { color: var(--ok-ink); }
|
||||
.onb-demo .d.tool small.ok { display: inline-flex; align-items: center; gap: 4px; color: var(--ok-ink); }
|
||||
.onb-demo .d.done { margin-top: 4px; padding-top: 10px; border-top: 1px solid var(--hair); color: var(--ok-ink); }
|
||||
.onb-demo .demo-spin { color: var(--net); }
|
||||
.onb-demo .demo-spin path { animation: demo-draw 1.2s ease-in-out infinite; }
|
||||
@@ -106,7 +106,7 @@
|
||||
.onb-flow .onb-go .onb-spin { width: 16px; height: 16px; color: currentColor; opacity: 1; }
|
||||
.onb-flow .onb-go .onb-spin path { animation: demo-draw 1.1s ease-in-out infinite; }
|
||||
.onb-flow .onb-go .onb-check { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.onb-flow .onb-go .onb-check path { stroke-dasharray: 100; stroke-dashoffset: 100; animation: demo-draw 300ms ease-out forwards; }
|
||||
.onb-flow .onb-go .onb-check path { stroke-dasharray: 32; stroke-dashoffset: 32; animation: demo-draw 300ms ease-out forwards; }
|
||||
.onb-flow .onb-found[data-enter] { animation: onb-banner-in 260ms var(--ease-out) both; }
|
||||
@keyframes onb-banner-in { from { opacity: 0; transform: translateY(-8px); } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
[data-fold~="phone"] .prefs { inset: 0; padding: 0; }
|
||||
[data-fold~="phone"] .prefs-sheet { width: 100%; height: 100%; border: 0; border-radius: 0; grid-template-columns: minmax(0, 1fr); grid-template-rows: auto auto auto minmax(0, 1fr); }
|
||||
[data-fold~="phone"] .prefs-sheet .prefs-hd h1 { grid-column: 1; grid-row: 1; padding: 14px 16px 8px; }
|
||||
[data-fold~="phone"] .prefs-sheet .prefs-actions { grid-column: 1; grid-row: 1; justify-self: end; align-self: center; padding: 8px 10px; }
|
||||
[data-fold~="phone"] .prefs-sheet .prefs-find { grid-column: 1; grid-row: 2; width: calc(100% - 24px); margin: 0 12px 8px; }
|
||||
[data-fold~="phone"] .prefs-sheet > .prefs-hd > .prefs-find { grid-column: 1; grid-row: 2; width: calc(100% - 24px); margin: 0 12px 8px; }
|
||||
[data-fold~="phone"] .prefs-sheet .prefs-nav { grid-column: 1; grid-row: 3; display: flex; flex-direction: row; gap: 4px; padding: 4px 28px 8px 10px; mask-image: linear-gradient(90deg, #000 calc(100% - 28px), transparent); overflow-x: auto; overflow-y: hidden; border-right: 0; border-bottom: 1px solid var(--hair); scrollbar-width: none; }
|
||||
@@ -9,6 +8,7 @@
|
||||
[data-fold~="phone"] .prefs-sheet .prefs-nav :is(.navsec, .nv) { display: none; }
|
||||
[data-fold~="phone"] .prefs-sheet .prefs-nav button { flex: none; min-height: 32px; padding: 6px 10px; }
|
||||
[data-fold~="phone"] .prefs-sheet .prefs-main { grid-column: 1; grid-row: 4; padding: 16px 14px 48px; }
|
||||
[data-fold~="phone"] .prefs-sheet .prefs-main > .page-index { top: -16px; }
|
||||
[data-fold~="phone"] .prefs-sheet[data-solo] .prefs-main { grid-column: 1; grid-row: 2 / -1; }
|
||||
[data-fold~="phone"] .prefs-sheet .prefs-main .prefs-pagetitle { display: grid; gap: 6px; padding-right: 0; }
|
||||
[data-fold~="phone"] .prefs-sheet .prefs-main .prefs-pagetitle > * { max-width: none; width: auto; }
|
||||
|
||||
@@ -29,13 +29,6 @@ describe("the settings sheet on a phone", () => {
|
||||
expect(rule(S + " .prefs-main")).toMatch(/grid-column:\s*1/);
|
||||
});
|
||||
|
||||
it("puts the close button in the title row, at the right", () => {
|
||||
const actions = rule(S + " .prefs-actions");
|
||||
expect(actions).toMatch(/grid-column:\s*1/);
|
||||
expect(actions).toMatch(/grid-row:\s*1/);
|
||||
expect(actions).toMatch(/justify-self:\s*end/);
|
||||
});
|
||||
|
||||
it("turns the category list into a strip that scrolls sideways without a scrollbar", () => {
|
||||
const nav = rule(S + " .prefs-nav");
|
||||
expect(nav).toMatch(/display:\s*flex/);
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
[data-fold~="phone"] .flow .call[data-approval] .apv-ft .apv-more { display: block; order: 3; grid-column: 1 / -1; justify-self: start; min-height: 32px; padding: 0; border: 0; background: none; color: var(--link); font: var(--w-reg) var(--fs-meta)/1 var(--ui); cursor: pointer; }
|
||||
|
||||
.turntools .turn-more { display: none; }
|
||||
.btn.stop .ic-pause { display: none; }
|
||||
[data-fold~="phone"] .compose .composecaps { display: none; }
|
||||
[data-fold~="phone"] .compose .turntools { flex-wrap: wrap; align-items: center; gap: 6px; row-gap: 8px; }
|
||||
[data-fold~="phone"] .compose .turntools > .studio-attach:first-child { box-sizing: border-box; width: 32px; height: 32px; padding: 0; border: 1px solid var(--border); border-radius: 50%; background: transparent; color: var(--muted); }
|
||||
@@ -48,11 +47,8 @@
|
||||
[data-fold~="phone"] .compose .turntools[data-more] > :not(:first-child, .studio-model-group, .turn-more) { display: flex; }
|
||||
[data-fold~="phone"] .compose .go { align-self: flex-start; gap: 6px; }
|
||||
[data-fold~="phone"] .compose .go .btn.send, [data-fold~="phone"] .compose .go .btn.stop { box-sizing: border-box; width: 36px; height: 36px; min-width: 0; padding: 0; border: 0; border-radius: 50%; display: grid; place-items: center; }
|
||||
[data-fold~="phone"] .compose .go .btn.send { background: var(--accent); color: var(--accent-fg); }
|
||||
[data-fold~="phone"] .compose .go .btn.send:not(:disabled) { background: var(--accent); color: var(--accent-fg); }
|
||||
[data-fold~="phone"] .compose .go .btn.stop { background: var(--overlay); color: var(--text); }
|
||||
[data-fold~="phone"] .compose .go .btn > span:not(.ic) { display: none; }
|
||||
[data-fold~="phone"] .compose .go .btn svg { width: 15px; height: 15px; stroke-width: 1.45; }
|
||||
[data-fold~="phone"] .compose .btn.stop .ic-stop { display: none; }
|
||||
[data-fold~="phone"] .compose .btn.stop .ic-pause { display: inline; }
|
||||
[data-fold~="phone"] .compose .turntools > :first-child { order: 0; }
|
||||
[data-fold~="phone"] .compose .turntools[data-more]::after { content: ""; order: 3; flex: 0 0 100%; height: 0; }
|
||||
@@ -119,15 +119,13 @@ describe("the composer's tool row on a phone", () => {
|
||||
expect(rule(row + " > .turn-more")).toMatch(/display:\s*inline-grid/);
|
||||
});
|
||||
|
||||
it("makes send a 36px accent circle, and stop the same size with the pause mark", () => {
|
||||
it("makes send a 36px accent circle, and stop the same size", () => {
|
||||
const both = rule('[data-fold~="phone"] .compose .go .btn.send, [data-fold~="phone"] .compose .go .btn.stop');
|
||||
expect(both).toMatch(/width:\s*36px/);
|
||||
expect(both).toMatch(/height:\s*36px/);
|
||||
expect(both).toMatch(/border-radius:\s*50%/);
|
||||
expect(rule('\n[data-fold~="phone"] .compose .go .btn.send')).toMatch(/background:\s*var\(--accent\)/);
|
||||
expect(rule('\n[data-fold~="phone"] .compose .go .btn.send:not(:disabled)')).toMatch(/background:\s*var\(--accent\)/);
|
||||
expect(rule('\n[data-fold~="phone"] .compose .go .btn.stop')).toMatch(/background:\s*var\(--overlay\)/);
|
||||
expect(rule('[data-fold~="phone"] .compose .btn.stop .ic-stop')).toMatch(/display:\s*none/);
|
||||
expect(rule(".btn.stop .ic-pause")).toMatch(/display:\s*none/);
|
||||
});
|
||||
|
||||
it("drops the keyboard hints, which a touch screen has no keys for", () => {
|
||||
|
||||
@@ -47,9 +47,11 @@
|
||||
.ds-row:is(:hover, :focus-within) > :is(.ds-meta, .ds-pin) { display: none; }
|
||||
@media (hover: none) { .ds-row .ds-more { display: grid; } }
|
||||
.ds-row .ds-pin { width: 12px; height: 12px; flex: none; }
|
||||
.ds-row .sessquick, .ds-row .sesspin-action { flex: none; display: none; }
|
||||
.ds-row:is(:hover, :focus-within) .sessquick, .ds-row:is(:hover, :focus-within) .sesspin-action { display: flex; }
|
||||
.ds-row .sessquick > button, .ds-row .sesspin-action { width: 24px; height: 28px; padding: 0; display: grid; place-items: center; border: 0; border-radius: var(--r-xs); background: transparent; color: var(--faint); cursor: pointer; }
|
||||
.ds-row .sessquick { flex: none; display: flex; }
|
||||
.ds-row .sesspin-action { flex: none; display: grid; }
|
||||
.ds-row:not(:is(:hover, :focus-within)) :is(.sessquick, .sesspin-action) { position: absolute; width: 0; height: 0; overflow: hidden; clip-path: inset(50%); opacity: 0; pointer-events: none; }
|
||||
.ds-row .sessquick > button, .ds-row .sesspin-action { width: 24px; height: 28px; padding: 0; place-items: center; border: 0; border-radius: var(--r-xs); background: transparent; color: var(--faint); cursor: pointer; }
|
||||
.ds-row .sessquick > button { display: grid; }
|
||||
.ds-row .sessquick > button:hover, .ds-row .sesspin-action:hover { background: var(--raised); color: var(--text); }
|
||||
.ds-row .sessquick > button:disabled, .ds-row .sesspin-action:disabled { opacity: .35; cursor: default; }
|
||||
.ds-row .sessquick .studio-icon, .ds-row .sesspin-action .studio-icon { width: 14px; height: 14px; }
|
||||
|
||||
@@ -32,4 +32,13 @@ describe("session and project rows follow the board", () => {
|
||||
expect(rule(".ds-row:is(:hover, :focus-within) > :is(.ds-meta, .ds-pin)")).toMatch(/display:\s*none/);
|
||||
expect(rule(".ds-row:is(:hover, :focus-within) .ds-more")).toMatch(/display:\s*grid/);
|
||||
});
|
||||
|
||||
it("hides the quick buttons until the row is hovered or focused, without taking them out of the tab order", () => {
|
||||
const hidden = rule(".ds-row:not(:is(:hover, :focus-within)) :is(.sessquick, .sesspin-action)");
|
||||
expect(hidden).toMatch(/opacity:\s*0/);
|
||||
expect(hidden).toMatch(/pointer-events:\s*none/);
|
||||
expect(hidden).not.toMatch(/display:\s*none/);
|
||||
expect(rule(".ds-row .sesspin-action")).toMatch(/display:\s*grid/);
|
||||
expect(rule(".ds-row .sessquick, .ds-row .sesspin-action")).not.toMatch(/display:\s*none/);
|
||||
});
|
||||
});
|
||||
@@ -18,10 +18,9 @@ describe("the settings search results", () => {
|
||||
expect(found).toMatch(/width:\s*auto/);
|
||||
});
|
||||
|
||||
it("cover the whole column, leaving the header row's height empty and the close button above them", () => {
|
||||
it("cover the whole column, leaving room above the results", () => {
|
||||
expect(found).toMatch(/top:\s*0/);
|
||||
expect(found).toMatch(/padding:\s*58px /);
|
||||
expect(studio).toMatch(/\.prefs-hd \{ min-height: 58px/);
|
||||
expect(rule(':root:not([data-fold~="phone"]) .prefs-actions')).toMatch(/z-index:\s*6/);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,12 @@
|
||||
.prefs.prefs-screen { inset: 0; display: flex; flex-direction: column; align-items: stretch; padding: 0; backdrop-filter: none; -webkit-backdrop-filter: none; }
|
||||
.prefs-screen > .prefs-sheet { flex: 1; min-height: 0; width: 100%; height: auto; border: 0; border-radius: 0; box-shadow: none; }
|
||||
.prefs-windowbar { flex: none; display: flex; align-items: center; justify-content: space-between; min-height: var(--chrome-h, 48px); padding: 0 12px; border-bottom: 1px solid var(--hair); background: var(--page); -webkit-app-region: drag; }
|
||||
.prefs-back { display: inline-flex; align-items: center; gap: 8px; min-height: 32px; padding: 0 10px; border: 0; border-radius: var(--r-sm); background: transparent; color: var(--muted); font: var(--w-med) var(--fs-ui)/1.4 var(--ui); cursor: pointer; -webkit-app-region: no-drag; }
|
||||
.prefs-back:hover:not(:disabled) { background: var(--overlay); color: var(--text); }
|
||||
.prefs-back:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
||||
.prefs-back:disabled { opacity: .5; cursor: default; }
|
||||
.prefs-windowbar .winctl { margin-left: auto; }
|
||||
[data-titlebar="app"][data-platform="darwin"] .prefs-windowbar { padding-left: var(--lights-w, 80px); }
|
||||
.prefs-sheet { grid-template-columns: 240px minmax(0, 1fr); border-radius: var(--r-lg); background: var(--surface); }
|
||||
.prefs-hd h1 { padding: 14px 18px 10px; background: var(--page); font: var(--w-semi) var(--fs-lead)/1.3 var(--ui); letter-spacing: 0; }
|
||||
.prefs-sheet > .prefs-hd > .prefs-find {
|
||||
@@ -10,30 +19,37 @@
|
||||
.prefs-nav button { height: 30px; min-height: 30px; padding: 0 10px; gap: 9px; border-radius: var(--r-sm); color: var(--muted); font: var(--w-reg) var(--fs-ui) var(--ui); }
|
||||
.prefs-nav button[aria-selected="true"] { background: var(--overlay); color: var(--text); font-weight: var(--w-reg); }
|
||||
.prefs-nav button .nv { font: var(--w-reg) var(--fs-meta) var(--ui); color: var(--faint); }
|
||||
.prefs-main { padding: 18px 24px; background: var(--surface); }
|
||||
.prefs-pagetitle { margin: 0 0 12px; gap: 10px; max-width: none; align-items: baseline; }
|
||||
.prefs-pagetitle h2 { font: var(--w-semi) var(--fs-title)/1.3 var(--ui); }
|
||||
.prefs-pagetitle .desc { font: var(--w-reg) var(--fs-meta)/1.4 var(--ui); color: var(--faint); }
|
||||
.prefs-col { max-width: 880px; margin-inline: auto; }
|
||||
.prefs-card { margin: 0 0 14px; padding: 12px 18px 4px; border: 1px solid var(--hair); border-radius: var(--r-lg); background: var(--raised); overflow: visible; }
|
||||
.prefs-card > header { padding: 0; margin: 0 0 4px; }
|
||||
.prefs-card > header h3 { margin: 0; font: var(--w-semi) var(--fs-ui)/1.4 var(--ui); }
|
||||
.prefs-card > header p { display: none; }
|
||||
.prefs-main { padding: 24px 24px 48px; background: var(--surface); }
|
||||
.prefs-pagetitle { display: flex; flex-direction: column; margin: 0 0 16px; gap: 6px; max-width: none; align-items: flex-start; }
|
||||
.prefs-pagetitle h2 { font: var(--w-semi) var(--fs-display)/1.3 var(--ui); }
|
||||
.prefs-pagetitle .desc { display: block; font: var(--w-reg) var(--fs-ui)/1.5 var(--ui); color: var(--muted); }
|
||||
.prefs-pagetitle .count { white-space: nowrap; }
|
||||
.prefs-col { width: 100%; max-width: none; margin: 0; min-width: 0; }
|
||||
.prefs-card { box-sizing: border-box; width: 100%; margin: 0 0 16px; padding: 18px 20px 8px; border: 1px solid var(--hair); border-radius: var(--r-lg); background: var(--raised); overflow: visible; }
|
||||
.prefs-card > header { padding: 0; margin: 0 0 12px; }
|
||||
.prefs-card > header h3 { margin: 0; font: var(--w-semi) var(--fs-title)/1.4 var(--ui); }
|
||||
.prefs-card > header p { display: block; margin: 4px 0 0; color: var(--muted); font: var(--w-reg) var(--fs-meta)/1.5 var(--ui); }
|
||||
.prefs-card > .grp { margin: 0; padding: 10px 0; border: 0; border-top: 1px solid var(--hair); border-radius: 0; background: none; box-shadow: none; column-gap: 16px; }
|
||||
.prefs-card > .grp .grp-hd h3 { font: var(--w-med) var(--fs-ui)/1.4 var(--ui); color: var(--text); }
|
||||
.prefs-card > .grp .grp-hd :is(h3, h4) { font: var(--w-med) var(--fs-ui)/1.4 var(--ui); color: var(--text); }
|
||||
.prefs-card > .grp .hint { margin: 2px 0 0; font: var(--w-reg) var(--fs-meta)/1.5 var(--ui); color: var(--faint); max-width: none; }
|
||||
.prefs-card > .grp .apply { margin: 2px 0 0; font: var(--w-reg) var(--fs-micro)/1.4 var(--ui); color: var(--faint); }
|
||||
.prefs-card .seg { padding: 2px; gap: 2px; border-radius: var(--r-sm); background: var(--overlay); }
|
||||
.prefs-card .seg button { height: 26px; min-height: 26px; padding: 0 10px; border-radius: var(--r-xs); font: var(--w-reg) var(--fs-meta) var(--ui); color: var(--muted); }
|
||||
.prefs-card .seg button[aria-pressed="true"], .prefs-card .seg button[aria-checked="true"] { background: var(--raised); color: var(--text); font-weight: var(--w-med); box-shadow: 0 1px 2px rgb(0 0 0 / .07); }
|
||||
.prefs-card select { width: 140px; height: 30px; padding: 0 10px; border-radius: var(--r-sm); border: 1px solid var(--border); background-color: var(--surface); font: var(--w-reg) var(--fs-ui) var(--ui); }
|
||||
.prefs-card select { width: 140px; height: 30px; padding: 0 30px 0 10px; overflow: hidden; white-space: nowrap; border-radius: var(--r-sm); border: 1px solid var(--border); background-color: var(--surface); font: var(--w-reg) var(--fs-ui) var(--ui); }
|
||||
.prefs-card select.peerfield { width: 100%; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.prefs-col .prefs-card > .grp .grp-hd h3 { font-size: var(--fs-ui); font-weight: var(--w-med); }
|
||||
.prefs-card .addp select { width: 100%; height: 32px; }
|
||||
.prefs-card .addp .unit-field { height: 32px; }
|
||||
.prefs-card .addp .ml-group { container: model-fields / inline-size; font-size: var(--fs-meta); }
|
||||
.prefs-card .addp .ml-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.prefs-card .addp .ml-row .me-name { grid-column: 1 / -1; }
|
||||
.prefs-card .addp .mp-row { grid-template-columns: repeat(4, minmax(0, 1fr)); }
|
||||
.prefs-card .addp .ml-row > select { font-size: var(--fs-meta); }
|
||||
.prefs-card .addp .mp-source-fields { grid-template-columns: repeat(auto-fit, minmax(min(100%, 22em), 1fr)); }
|
||||
@container model-fields (width < 74em) { .prefs-card .addp .mp-row { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@container model-fields (width < 45em) { .prefs-card .addp .ml-row { grid-template-columns: minmax(0, 1fr); } }
|
||||
.prefs-col .prefs-card > .grp .grp-hd :is(h3, h4) { font-size: var(--fs-ui); font-weight: var(--w-med); }
|
||||
.prefs-main { container: prefsmain / inline-size; }
|
||||
@container prefsmain (min-width: 860px) {
|
||||
.prefs-col[data-cols="2"] { max-width: 1400px; column-count: 2; column-gap: 16px; }
|
||||
.prefs-col[data-cols="2"] > .prefs-card { break-inside: avoid; }
|
||||
}
|
||||
.prefs-card > .srow, .grp[data-setting="global-shortcut"] .srow { display: flex; align-items: center; gap: 16px; padding: 10px 0; border-top: 1px solid var(--hair); }
|
||||
.srow-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
||||
.srow-name { color: var(--text); font: var(--w-med) var(--fs-ui)/1.4 var(--ui); }
|
||||
@@ -58,8 +74,7 @@
|
||||
.prefs-col .prefs-card .seg button { font-weight: var(--w-reg); }
|
||||
.prefs-col .prefs-card .seg button[aria-pressed="true"], .prefs-col .prefs-card .seg button[aria-checked="true"] { font-weight: var(--w-med); }
|
||||
.prefs-main { grid-row: 1 / span 3; }
|
||||
.prefs-actions { grid-row: 1; position: absolute; top: 10px; right: 14px; z-index: 6; padding: 0; }
|
||||
.prefs-pagetitle { padding-right: 150px; }
|
||||
.prefs-pagetitle { padding-right: 0; }
|
||||
.prefs-hd h1 { padding: 14px 18px 10px; }
|
||||
.prefs-sheet > .prefs-hd > .prefs-find { margin: 0 10px 8px; width: calc(100% - 20px); }
|
||||
.prefs-nav { padding: 0 10px 14px; gap: 2px; }
|
||||
@@ -103,7 +118,7 @@
|
||||
.ext-add-menu small { color: var(--faint); font: var(--w-med) var(--fs-micro) var(--ui); }
|
||||
.grp[data-setting="ext-runtime"] { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; padding: 10px 2px; border: 0; background: none; box-shadow: none; }
|
||||
.grp[data-setting="ext-runtime"] .grp-hd { order: 2; margin: 0; }
|
||||
.grp[data-setting="ext-runtime"] .grp-hd h3 { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
||||
.grp[data-setting="ext-runtime"] .grp-hd :is(h3, h4) { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
||||
.grp[data-setting="ext-runtime"] .hint { order: 1; flex: 1; min-width: 240px; margin: 0; color: var(--faint); font: var(--w-med) var(--fs-meta)/1.6 var(--ui); }
|
||||
.grp[data-setting="ext-runtime"] .grp-items { order: 3; flex-basis: 100%; }
|
||||
.grp[data-setting="ext-runtime"] .apply { display: none; }
|
||||
@@ -252,7 +267,6 @@
|
||||
.rmthost-menu > button { min-height: 30px; padding: 4px 10px; border: 0; border-radius: var(--r-sm); background: transparent; color: var(--text); font: var(--w-med) var(--fs-ui) var(--ui); text-align: start; white-space: nowrap; cursor: pointer; }
|
||||
.rmthost-menu > button:hover:not(:disabled) { background: var(--overlay); }
|
||||
.rmthost-menu > button[data-danger] { color: var(--err-ink); }
|
||||
.grp-link { padding: 0; }
|
||||
.grp-row {
|
||||
display: flex; align-items: center; gap: 12px; width: 100%; padding: 12px 14px; box-sizing: border-box;
|
||||
border: 0; border-radius: inherit; background: transparent; color: var(--text); text-align: start; cursor: pointer;
|
||||
@@ -263,28 +277,19 @@
|
||||
.grp-row-t small { overflow: hidden; color: var(--faint); font: var(--w-reg) var(--fs-meta)/1.5 var(--ui); white-space: nowrap; text-overflow: ellipsis; }
|
||||
.grp-row > .now { flex: none; color: var(--muted); font: var(--w-med) var(--fs-meta) var(--ui); }
|
||||
.grp-row > .studio-icon { flex: none; width: 14px; height: 14px; color: var(--faint); }
|
||||
.prefs-col[data-deep] :not(.deep-page, .deep-page *, :has(.deep-page)) { display: none; }
|
||||
.prefs-col[data-deep] :is(.prefs-card, .grp):has(.deep-page) { padding: 0; border: 0; background: none; box-shadow: none; }
|
||||
.deep-page { display: flex; flex-direction: column; gap: 12px; animation: m-panel var(--t-panel) var(--ease-out, ease-out); }
|
||||
.deep-crumb { display: flex; align-items: center; gap: 6px; color: var(--faint); font: var(--w-med) var(--fs-meta) var(--ui); }
|
||||
.deep-crumb button { padding: 0; border: 0; background: none; color: var(--muted); font: inherit; cursor: pointer; }
|
||||
.deep-crumb button:hover { color: var(--text); text-decoration: underline; }
|
||||
.deep-crumb b { color: var(--text); font-weight: var(--w-semi); }
|
||||
@media (prefers-reduced-motion: reduce) { .deep-page { animation: none; } }
|
||||
|
||||
.prefs-sheet[data-solo] { grid-template-columns: minmax(0, 1fr) auto; grid-template-rows: auto minmax(0, 1fr); }
|
||||
.prefs-sheet[data-solo] .prefs-find { display: none; }
|
||||
.prefs-sheet[data-solo]::before, .prefs-sheet[data-solo]::after { display: none; }
|
||||
.prefs-sheet[data-solo] .prefs-hd h1 { border-bottom: 1px solid var(--hair); }
|
||||
.prefs-sheet[data-solo] .prefs-hd h1 { grid-column: 1; grid-row: 1; padding: 16px 22px 8px; font-size: var(--fs-title); }
|
||||
.prefs-sheet[data-solo] .prefs-actions { grid-column: 2; grid-row: 1; border-bottom: 1px solid var(--hair); background: none; }
|
||||
.prefs-sheet[data-solo] .prefs-main { grid-column: 1 / -1; grid-row: 2; }
|
||||
.prefs-sheet[data-solo] .prefs-col { max-width: none; }
|
||||
.ext-md { display: grid; grid-template-columns: minmax(300px, 400px) minmax(0, 1fr); gap: 16px; align-items: start; }
|
||||
.ext-md-list { min-width: 0; }
|
||||
.ext-md-list .grp { margin: 0; padding: 0 0 8px; border: 0; border-radius: 0; background: none; box-shadow: none; }
|
||||
.ext-md-list .grp-hd { margin: 10px 0 2px; padding: 0 10px; }
|
||||
.ext-md-list .grp-hd h3 { color: var(--muted); font: var(--w-semi) var(--fs-meta) var(--ui); }
|
||||
.ext-md-list .grp-hd :is(h3, h4) { color: var(--muted); font: var(--w-semi) var(--fs-meta) var(--ui); }
|
||||
.ext-md-list .grp-hd .now { padding: 0; background: none; color: var(--faint); }
|
||||
.ext-md-list .srv > :not(summary):not(.srv-hd) { display: none; }
|
||||
.ext-md-list .srv > summary, .ext-md-list .srv > .srv-hd, .ext-md-list .skrow { cursor: pointer; border-radius: var(--r-md); }
|
||||
@@ -363,9 +368,6 @@
|
||||
.ulegend i[data-s="miss"], .ustack .utip i[data-s="miss"] { background: var(--chart-miss); }
|
||||
.ustack .utip .r { display: flex; justify-content: space-between; gap: 12px; color: var(--muted); font: var(--w-med) var(--fs-micro) var(--ui); }
|
||||
.ustack .utip .r b { color: var(--text); font-weight: var(--w-semi); }
|
||||
.prefs { padding: 24px; }
|
||||
.prefs-sheet { width: min(1360px, 100%); height: min(900px, 100%); }
|
||||
.srow-ctl .unit-field input { width: 72px; }
|
||||
.srow-val { color: var(--muted); font: var(--w-reg) var(--fs-meta) var(--ui); max-width: 260px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.srow-val > * { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.prefs-card > .grp[data-inline] { display: grid; grid-template-columns: minmax(0, 1fr) fit-content(320px); align-items: center; column-gap: 16px; row-gap: 2px; }
|
||||
@@ -388,7 +390,8 @@
|
||||
.prefs-icon:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
||||
.prefs-icon .studio-icon { width: 16px; height: 16px; }
|
||||
.prefs-card .unit-field { align-items: center; gap: 6px; height: 28px; padding: 0 10px 0 2px; border: 1px solid var(--hair); border-radius: var(--r-sm); background: transparent; }
|
||||
.prefs-card .unit-field input { width: 64px; text-align: right; font: var(--w-reg) var(--fs-meta) var(--mono); }
|
||||
.prefs-card .unit-field input { flex: 1; width: 0; text-align: right; font: var(--w-reg) var(--fs-meta) var(--mono); }
|
||||
.prefs-card :is(.srow-ctl, .threshold-control) .unit-field input { flex: none; width: 64px; }
|
||||
.prefs-card .threshold-control { width: auto; justify-content: flex-start; }
|
||||
.prefs-card .threshold-control .unit-field { flex: none; }
|
||||
.prefs-card .unit-field > i { padding: 0; border: 0; color: var(--faint); font: var(--w-reg) var(--fs-meta) var(--ui); font-style: normal; }
|
||||
@@ -406,13 +409,12 @@
|
||||
.usage .urow-t .n { font: var(--w-semi) var(--fs-meta) var(--mono); }
|
||||
.usage .urow-t .v { font: var(--w-reg) var(--fs-meta) var(--mono); }
|
||||
.usage .utwo .ucard + .ucard .ucard-h h3 { font: var(--w-reg) var(--fs-meta) var(--ui); color: var(--muted); }
|
||||
.deep-page .grp-hd h3 { font: var(--w-semi) var(--fs-title)/1.3 var(--ui); }
|
||||
.memdetail .facts h4 { font: var(--w-med) var(--fs-micro) var(--ui); }
|
||||
.memhead .btn { font: var(--w-reg) var(--fs-meta) var(--ui); }
|
||||
.ext-bar .mkt-tabs button { font: var(--w-reg) var(--fs-meta) var(--ui); }
|
||||
.ext-bar .mkt-tabs button[aria-selected="true"] { font-weight: var(--w-med); }
|
||||
.ext-bar .ext-add-btn { font: var(--w-reg) var(--fs-meta) var(--ui); }
|
||||
.ext-md-list .grp-hd h3 { font: var(--w-med) var(--fs-meta) var(--ui); }
|
||||
.ext-md-list .grp-hd :is(h3, h4) { font: var(--w-med) var(--fs-meta) var(--ui); }
|
||||
.ext-md-list .grp-hd .now { font: var(--w-reg) var(--fs-meta) var(--ui); }
|
||||
.ext-md-list .srv > summary b, .ext-md-list .srv > .srv-hd b, .ext-md-list .skrow .nm { font-weight: var(--w-med); }
|
||||
.ext-detail-hd > b { font: var(--w-semi) var(--fs-title) var(--ui); }
|
||||
@@ -436,24 +438,20 @@
|
||||
.changed-keys { display: flex; flex-direction: column; gap: 4px; margin: 4px 0 0; padding: 8px 0 2px; border-top: 1px solid var(--hair); list-style: none; }
|
||||
.changed-keys li { color: var(--muted); font: var(--w-reg) var(--fs-meta) var(--mono); }
|
||||
.prefs-card > .grp { grid-template-columns: minmax(0, 1fr) fit-content(320px); }
|
||||
.prefs-main > .prefs-pagetitle { max-width: 880px; margin-inline: auto; }
|
||||
@container prefsmain (min-width: 860px) {
|
||||
.prefs-main:has(.prefs-col[data-cols="2"]) > .prefs-pagetitle { max-width: 1400px; }
|
||||
}
|
||||
.prefs-main > :is(.prefs-pagetitle, .page-index) { max-width: none; margin-inline: 0; }
|
||||
.prefs-main > .page-index { position: sticky; top: -24px; z-index: 2; padding-block: 12px; background: var(--surface); border-bottom: 1px solid var(--hair); }
|
||||
.apply[data-usual] { display: none; }
|
||||
.prefs-card > .grp > .apply { display: none; }
|
||||
.scope-tag { display: inline-grid; place-items: center; height: 18px; padding: 0 7px; border-radius: var(--r-pill); background: var(--overlay); color: var(--muted); font: var(--w-reg) var(--fs-micro)/1 var(--ui); white-space: nowrap; }
|
||||
.prefs-card > .grp { padding-block: 6px; min-height: 36px; box-sizing: border-box; }
|
||||
.prefs-card > .srow { padding-block: 6px; min-height: 36px; box-sizing: border-box; }
|
||||
.prefs-card > .grp > .hint, .srow-desc { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.prefs-card > .grp > .hint, .srow-desc { overflow-wrap: anywhere; white-space: normal; }
|
||||
.prefs-card > .grp > .hint { margin: 0; line-height: 1.4; }
|
||||
.prefs-card > .grp > .grp-hd { display: flex; align-items: center; gap: 8px; }
|
||||
.prefs-pagetitle { flex-wrap: wrap; }
|
||||
.prefs-pagetitle > .usual { flex-basis: 100%; margin: 0; color: var(--faint); font: var(--w-reg) var(--fs-meta)/1.4 var(--ui); }
|
||||
.srow-name { display: flex; align-items: center; gap: 8px; }
|
||||
.prefs-pagetitle > .usual { margin: 0; color: var(--faint); font: var(--w-reg) var(--fs-meta)/1.4 var(--ui); }
|
||||
.srow-name { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; }
|
||||
.srow-meta > .apply:not([data-usual]) { display: none; }
|
||||
.prefs-strip { position: fixed; inset: 0 0 auto; height: var(--chrome-h, 39px); z-index: 50; background: color-mix(in srgb, var(--page) 40%, transparent); -webkit-backdrop-filter: blur(14px) saturate(.92); backdrop-filter: blur(14px) saturate(.92); pointer-events: none; }
|
||||
:root[data-fold~="scene"] .prefs-strip { display: none; }
|
||||
.usage .ucharts-spend { display: flex; flex-direction: column; gap: 14px; min-width: 0; }
|
||||
.usage .utable th { font: var(--w-med) var(--fs-micro) var(--ui); }
|
||||
.usage .utable th, .usage .utable td { padding-block: 9px; }
|
||||
@@ -479,4 +477,3 @@
|
||||
.page-index .pi-label { margin-right: 2px; color: var(--faint); font: var(--w-reg) var(--fs-meta) var(--ui); }
|
||||
.page-index .spill { height: 26px; }
|
||||
:root:not([data-fold~="phone"]) .prefs-found { top: 0; left: 240px; right: 0; bottom: 0; width: auto; max-height: none; padding: 58px 22px 14px; border: 0; border-left: 1px solid var(--hair); border-radius: 0 var(--r-lg) var(--r-lg) 0; background: var(--surface); box-shadow: none; }
|
||||
:root:not([data-fold~="phone"]) .prefs-actions { position: relative; z-index: 6; }
|
||||
@@ -39,17 +39,25 @@
|
||||
|
||||
.railfoot { flex: none; display: flex; flex-direction: column; gap: 0; padding: 0 10px 10px; border: 0; background: transparent; }
|
||||
.railfoot::before { content: ""; display: block; height: 1px; margin: 6px 4px; background: var(--hair); }
|
||||
.studio-quicknav { display: flex; align-items: center; gap: 2px; margin: 0; padding: 0 2px; }
|
||||
.studio-quicknav > .qspace { flex: 1; }
|
||||
.studio-tools-row { display: flex; align-items: center; min-height: 32px; gap: 2px; }
|
||||
.studio-tools-row > button { display: inline-flex; align-items: center; gap: 10px; height: 32px; padding: 0 10px; border: 0; border-radius: var(--r-sm); background: transparent; color: var(--muted); font: var(--w-reg) var(--fs-ui)/1.4 var(--ui); cursor: pointer; }
|
||||
.studio-tools-row > .studio-settings { flex: 1; min-width: 0; text-align: left; }
|
||||
.studio-tools-row > .studio-tools-toggle { flex: none; width: 32px; padding: 0; justify-content: center; }
|
||||
.studio-tools-row > button:hover, .studio-tools-toggle[aria-expanded="true"] { background: var(--overlay); color: var(--text); }
|
||||
.studio-tools-row > button:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px; }
|
||||
.studio-tools-row .upd-dot { margin-left: auto; }
|
||||
.studio-quicknav { position: fixed; z-index: 80; display: flex; flex-direction: column; gap: 2px; box-sizing: border-box; width: 260px; max-width: calc(100vw - 24px); max-height: calc(100vh - 24px); overflow-y: auto; margin: 0; padding: 6px; border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--float); box-shadow: var(--shadow-float); }
|
||||
.studio-quicknav[hidden] { display: none; }
|
||||
.studio-quicknav > button {
|
||||
position: relative; width: 28px; height: 28px; padding: 0; flex: none; box-sizing: border-box;
|
||||
display: inline-grid; place-items: center; border: 0; border-radius: var(--r-sm);
|
||||
background: transparent; color: var(--muted); cursor: pointer;
|
||||
position: relative; width: 100%; min-height: 36px; padding: 8px 10px; flex: none; box-sizing: border-box;
|
||||
display: flex; align-items: center; gap: 10px; border: 0; border-radius: var(--r-sm);
|
||||
background: transparent; color: var(--text); font: var(--w-reg) var(--fs-ui)/1.4 var(--ui); text-align: left; cursor: pointer;
|
||||
transition: background var(--m-line), color var(--m-line), transform var(--t-press);
|
||||
}
|
||||
.studio-quicknav > button:hover { background: var(--overlay); color: var(--text); }
|
||||
.studio-quicknav > button:active { transform: scale(.94); }
|
||||
.studio-quicknav > button:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
||||
.studio-quicknav > button > :is(small, kbd) { margin-left: auto; color: var(--faint); font: var(--w-reg) var(--fs-micro)/1.4 var(--ui); }
|
||||
.studio-quicknav > button :is(svg, .studio-icon) { width: 16px; height: 16px; stroke-width: 1.45; }
|
||||
.studio-quicknav :is(.qdot, .upd-dot) { position: absolute; top: 3px; right: 3px; width: 7px; height: 7px; box-sizing: border-box; border-radius: 50%; border: 1.5px solid var(--page); box-shadow: none; }
|
||||
.studio-quicknav .qdot[data-tone="ok"] { background: var(--ok); }
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
height: 26px; min-height: 26px; padding: 0 6px; margin: 0 -6px; gap: 7px; box-sizing: border-box; border-radius: var(--r-xs);
|
||||
background: transparent; color: var(--muted); font: var(--w-reg) var(--fs-ui)/1 var(--ui);
|
||||
}
|
||||
.activity-group[open] > summary { background: transparent; border-bottom: 0; }
|
||||
.activity-group[open] > summary { background: var(--surface); border-bottom: 0; }
|
||||
.activity-group > summary:hover { background: var(--overlay); color: var(--text); }
|
||||
.activity-group > summary .studio-icon { width: 12px; height: 12px; color: var(--faint); }
|
||||
.activity-group .activity-title { color: inherit; font: inherit; }
|
||||
@@ -68,8 +68,8 @@
|
||||
.think-swap > .think-label { grid-area: 1 / 1; }
|
||||
.think[data-settled] .think-label:not(.think-out) { animation: m-fade var(--t-card) var(--ease-out) both; }
|
||||
.think-out { animation: m-exit var(--t-card) var(--ease-out) both; pointer-events: none; }
|
||||
.think[data-live] .tk { max-height: calc(1.75em * 6); overflow: hidden; display: flex; flex-direction: column; justify-content: flex-end; }
|
||||
.think[data-live] .tk-in { flex: none; }
|
||||
.think[data-live]:not([data-full]) .tk { max-height: calc(1.75em * 6); overflow: hidden; display: flex; flex-direction: column; justify-content: flex-end; }
|
||||
.think[data-live]:not([data-full]) .tk-in { flex: none; }
|
||||
.think[data-cut] > summary .fold, .think[data-cut] > summary:hover .fold { color: var(--warn-ink); }
|
||||
.think-note { margin: 4px 0 0 16px; color: var(--faint); font: var(--w-reg) var(--fs-meta)/1.5 var(--ui); }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -17,4 +17,8 @@ describe("the turn follows the boards", () => {
|
||||
expect(rule(".rc[data-worked]")).toMatch(/justify-content:\s*flex-end/);
|
||||
expect(rule(".rc[data-worked] .rc-tick")).toMatch(/width:\s*14px/);
|
||||
});
|
||||
|
||||
it("paints a stuck, open activity heading in the conversation's own colour so rows scroll under it unseen", () => {
|
||||
expect(rule(".activity-group[open] > summary")).toMatch(/background:\s*var\(--surface\)/);
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@ import type { Messages } from "./types";
|
||||
|
||||
export const LOOK: Messages = {
|
||||
"look.a_few_of_name_s": { zh: "{name} 有几项没生效", en: "A few of {name}'s entries did not take" },
|
||||
"look.advanced_look": { zh: "高级外观", en: "Advanced appearance" },
|
||||
|
||||
"look.at_the_current_window_size": { zh: "当前窗口尺寸下,此壁纸只有上下会被裁剪,左右正好填满,因此横向焦点无法调整。", en: "At the current window size this wallpaper is cropped only top and bottom and fills the width exactly, so the horizontal focus cannot be moved." },
|
||||
"look.at_the_current_window_size_2": { zh: "当前窗口尺寸下,此壁纸只有左右会被裁剪,上下正好填满,因此纵向焦点无法调整。", en: "At the current window size this wallpaper is cropped only left and right and fills the height exactly, so the vertical focus cannot be moved." },
|
||||
"look.back_to_the_default_typeface": { zh: "恢复默认字体", en: "Back to the default typeface" },
|
||||
@@ -41,7 +41,7 @@ export const LOOK: Messages = {
|
||||
"look.if_the_window_keeps_your": { zh: "窗口一直占用显卡、或界面花屏时,先试「视觉效果:节能」,不行再改为「仅软件渲染」:改用 CPU 绘制,显卡不再参与。设置保存后,需退出并重新打开 Studio 才会生效。", en: "If the window keeps your GPU busy or draws garbage, try Visual effects: Reduced first; if that is not enough, choose Software only: the CPU draws the window and the GPU stays out of it. The choice is saved now and applies after you quit and reopen Studio." },
|
||||
"look.illustrated_and_background_themes_you": { zh: "插画与背景主题,可随时切回基础主题", en: "Illustrated and background themes; you can return to a foundation theme at any time" },
|
||||
"look.import_theme": { zh: "导入主题…", en: "Import theme…" },
|
||||
"look.import_themes_custom_colours": { zh: "导入主题、自定义颜色", en: "Import themes, custom colours" },
|
||||
|
||||
"look.import_wallpaper_engine": { zh: "导入 Wallpaper Engine…", en: "Import Wallpaper Engine…" },
|
||||
"look.imported_name": { zh: "已导入「{name}」。", en: "Imported “{name}”." },
|
||||
"look.in_effect_now": { zh: "立即生效", en: "In effect now" },
|
||||
|
||||
@@ -253,11 +253,9 @@ export const MODELS: Messages = {
|
||||
"models.with_no_level_picked_the": { zh: "未选档位时按继承处理。", en: "With no level picked, the model inherits." },
|
||||
"models.you_can_write_proxy_password": { zh: "可填写 ${PROXY_PASSWORD}", en: "You can write ${PROXY_PASSWORD}" },
|
||||
"models.check_while_the_list_opens": { zh: "打开模型列表时顺便检查连接和余额,不发送生成请求", en: "Check connections and balances when the model list opens; no generation request is sent" },
|
||||
"models.services_connected_n": { zh: "已接入 {n} 个服务;地址、密钥、模型列表在这里改", en: "{n} services connected; change addresses, keys and model lists here" },
|
||||
"models.manage_services": { zh: "管理服务", en: "Manage services" },
|
||||
|
||||
"models.services": { zh: "服务", en: "Services" },
|
||||
"models.uses_and_context": { zh: "用途与上下文", en: "Uses and context" },
|
||||
"models.which_model_for_each_use": { zh: "对话、规划、子代理、看图分别用哪个模型", en: "Which model chat, planning, subagents and image reading each use" },
|
||||
"models.set_by_use": { zh: "按用途设置", en: "Set by use" },
|
||||
"models.compacts_from_this_share": { zh: "上下文到多少开始压缩;默认窗口的 {p}%", en: "How full the context gets before it compacts; {p}% of the window by default" },
|
||||
|
||||
};
|
||||
@@ -123,15 +123,13 @@ export const SAFETY: Messages = {
|
||||
"safety.writing_files_every_write_tool": { zh: "写文件(全部写入工具)", en: "writing files (every write tool)" },
|
||||
"safety.ask_before_writing_or_running": { zh: "写文件、跑命令前要不要先问", en: "Whether to ask before writing files or running commands" },
|
||||
"safety.allow_and_deny_by_command": { zh: "按命令、路径写死的允许和拒绝;拒绝规则永远优先", en: "Allow and deny by command or path; a deny rule always wins" },
|
||||
"safety.edit_rules_n": { zh: "编辑规则({n} 条)", en: "Edit rules ({n})" },
|
||||
|
||||
"safety.ask_before_copied_text": { zh: "照抄网页或工具结果里的文字去调用工具前先问你", en: "Ask before a call that copies text from a web page or tool result" },
|
||||
"safety.which_tools_the_model_sees": { zh: "模型能看到哪些工具", en: "Which tools the model can see" },
|
||||
"safety.view_and_adjust_n": { zh: "查看与调整({n})", en: "View and adjust ({n})" },
|
||||
|
||||
"safety.commands_write_only_here": { zh: "命令只能写工作区、临时目录和工具缓存", en: "Commands can only write to the workspace, temp folders and tool caches" },
|
||||
"safety.which_program_runs_a_command": { zh: "跑命令用哪个程序", en: "Which program runs a command" },
|
||||
"safety.when_two_sessions_edit_one": { zh: "两个会话同时改一个项目时怎么处理", en: "What happens when two sessions edit one project" },
|
||||
"safety.web": { zh: "联网", en: "Web" },
|
||||
"safety.search_and_fetch_when_needed": { zh: "需要时搜索网页、抓取页面内容", en: "Search the web and fetch pages when needed" },
|
||||
"safety.details": { zh: "详细设置", en: "Details" },
|
||||
|
||||
"safety.what_outside_text_covers": { zh: "调用逐字复制了网页或工具结果里的文字时,Auto 和 YOLO 也先问你。只认用户配置;已授权的 MCP 服务器、改写过或编码过的文字不在其内。无人可问时(无头 YOLO),这类调用会被拒绝而不是执行。修改会重建运行时,任务运行期间无法变更。默认关闭。", en: "When a call copies text word for word from a web page or a tool result, Auto and YOLO ask you first. Only your user config counts; servers you have authorised, and text that was reworded or encoded, are not covered. When nobody can be asked (headless YOLO) such a call is refused instead of run. Changing it rebuilds the runtime and cannot be done while a task runs. Off by default." },
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Messages } from "./types";
|
||||
|
||||
export const SETTINGS: Messages = {
|
||||
"settings.back_to_workspace": { zh: "返回工作区", en: "Back to workspace" },
|
||||
"settings.a_system_notification_when_a": { zh: "一轮结束、等你处理时,系统通知提醒你", en: "A system notification when a turn ends or something waits for you" },
|
||||
"settings.account": { zh: "账号", en: "Account" },
|
||||
"settings.account_and_about": { zh: "账号与关于", en: "Account and About" },
|
||||
@@ -38,7 +39,7 @@ export const SETTINGS: Messages = {
|
||||
"settings.data": { zh: "数据", en: "Data" },
|
||||
"settings.default_0_nothing_is_folded": { zh: "默认 0:不自动收起,打开的会话都保留。设成一个数字后,打开的会话超过它时,会收起最久没动的空闲会话;对话仍在列表里,随时可以重新打开。保留得越多,占用的内存越多。", en: "Default 0: nothing is folded away and every open session stays. Set a number and, once more sessions than that are open, the idle one untouched longest is folded away; its conversation stays in the list and can be opened again. The more you keep, the more memory it uses." },
|
||||
"settings.detailed": { zh: "详细", en: "Detailed" },
|
||||
"settings.details": { zh: "细项", en: "Details" },
|
||||
|
||||
"settings.door_does_not_offer_model": { zh: "{door} 不提供 {model},请在下方选择该来源支持的模型。", en: "{door} does not offer {model}. Choose one it does provide below." },
|
||||
"settings.drafts_a_plan_read_only": { zh: "仅生成计划,不写入", en: "Drafts a plan, read-only" },
|
||||
"settings.empty_is_automatic_1_to": { zh: "留空表示自动;可填 1 到 {max}", en: "Empty is automatic; 1 to {max}" },
|
||||
@@ -181,7 +182,7 @@ export const SETTINGS: Messages = {
|
||||
"settings.tool_approvals": { zh: "工具批准", en: "Tool approvals" },
|
||||
"settings.type_this_code_into_the": { zh: "在浏览器里打开的页面输入这串代码。没自动打开就手动访问 {uri}", en: "Type this code into the page your browser opened. If none opened, go to {uri}" },
|
||||
"settings.use_send_now_in_the": { zh: "可在待发送队列中选择“立即发送”来停止当前回复并开始新一轮。暂停、编辑和取回仍然可用。", en: "Use Send now in the queue to stop the current response and start a new turn. Hold, edit, and take back remain available." },
|
||||
"settings.where_you_are": { zh: "当前位置", en: "Where you are" },
|
||||
|
||||
"settings.changes_apply_from_the_next_turn": { zh: "启用、停用、安装或更新后,从下一轮开始生效;正在跑的这一轮不受影响。改了扩展自己的代码,用「重载运行时」重新读取。", en: "Enabling, disabling, installing or updating takes effect from the next turn; the one running is unaffected. After editing an extension's own code, use Reload runtime to read it again." },
|
||||
"settings.used_for_posting_to_the": { zh: "用于社区发帖与崩溃问题跟进。与模型 API key 无关 —— 登录不会上传你的对话、代码或密钥。", en: "Used for posting to the community and following up crashes. Unrelated to your model API keys — signing in uploads none of your conversations, code or secrets." },
|
||||
"settings.warns_when_the_drive_holding": { zh: "项目或会话所在的磁盘剩余空间很少时提醒(不足 5 GB 或不足 3%),同一块盘长时间不重复提醒;只提醒,不会清理任何文件", en: "Warns when the drive holding your project or sessions runs short (under 5 GB or 3%), and does not repeat for the same drive for hours. It only warns; it never deletes anything." },
|
||||
@@ -208,41 +209,40 @@ export const SETTINGS: Messages = {
|
||||
"settings.token_unit": { zh: "令牌", en: "tokens" },
|
||||
"settings.edit_persona": { zh: "编辑人设", en: "Edit persona" },
|
||||
"settings.persona_hint": { zh: "写在每个会话开头的身份说明", en: "The identity note written at the start of every session" },
|
||||
"settings.presets_hint": { zh: "工具、模型、提示词的组合;新任务默认用哪个", en: "A bundle of tools, model and prompt; which one new tasks start with" },
|
||||
|
||||
"settings.sessions_send_each_other": { zh: "让一个会话给另一个会话发消息(实验)", en: "Let one session send another a message (experimental)" },
|
||||
"settings.persona_and_presets": { zh: "人设与预设", en: "Persona and presets" },
|
||||
"settings.manage_presets": { zh: "管理预设", en: "Manage presets" },
|
||||
|
||||
"settings.which_currency_costs_show": { zh: "费用用什么币种显示", en: "Which currency costs are shown in" },
|
||||
"settings.days_before_archiving": { zh: "多少天没动的会话自动归档", en: "How many idle days before a conversation is archived" },
|
||||
"settings.migrated_sessions_in_the_sidebar": { zh: "旧版迁移过来的会话是否出现在侧栏", en: "Whether sessions migrated from 1.x show in the sidebar" },
|
||||
"settings.where_session_records_go": { zh: "会话记录写到哪里", en: "Where session records are written" },
|
||||
"settings.open_memory": { zh: "打开记忆", en: "Open memory" },
|
||||
|
||||
"settings.what_it_remembers": { zh: "它记住的东西:星系、遗忘、整理", en: "What it remembers: constellations, forgetting, tidying" },
|
||||
"settings.view_usage": { zh: "查看用量", en: "View usage" },
|
||||
|
||||
"settings.usage_hint": { zh: "输入、输出、费用,按天和按模型看", en: "Input, output and cost, by day and by model" },
|
||||
"settings.manage_storage": { zh: "管理", en: "Manage" },
|
||||
|
||||
"settings.storage_hint": { zh: "会话、索引、缓存占了多少空间", en: "How much space sessions, indexes and caches take" },
|
||||
"settings.export_and_import": { zh: "导出与导入…", en: "Export and import…" },
|
||||
|
||||
"settings.config_in_one_file": { zh: "把模型、扩展、记忆等配置加密导出成一个文件", en: "Export models, extensions and memory to one encrypted file" },
|
||||
"settings.back_up_to_the_cloud": { zh: "登录后把配置备份到云端", en: "Back up your configuration to the cloud once signed in" },
|
||||
"settings.size_of_requests_sent": { zh: "发给模型服务商的请求有多大", en: "How large the requests sent to model providers are" },
|
||||
"settings.other_machines": { zh: "其他机器", en: "Other machines" },
|
||||
"settings.network_hint": { zh: "代理和网络诊断", en: "Proxy and network diagnostics" },
|
||||
"settings.remote_hint": { zh: "连接其他机器上的 Reasonix", en: "Connect to Reasonix on other machines" },
|
||||
"settings.remote_machines": { zh: "远程机器", en: "Remote machines" },
|
||||
|
||||
"settings.phone_hint": { zh: "用手机访问这台电脑上的会话,含通知设置", en: "Reach this computer's sessions from a phone, with notification settings" },
|
||||
"settings.set_up_phone_access": { zh: "设置手机访问", en: "Set up phone access" },
|
||||
|
||||
"settings.account_hint": { zh: "社区发帖、崩溃跟进时用;与模型密钥无关", en: "Used for community posts and crash follow-up; unrelated to model keys" },
|
||||
"settings.sign_in": { zh: "登录", en: "Sign in" },
|
||||
"settings.account_info": { zh: "账号信息", en: "Account" },
|
||||
|
||||
"settings.version_and_diagnostics": { zh: "版本与诊断", en: "Version and diagnostics" },
|
||||
"settings.versions_hint": { zh: "当前版本、可用更新、回退", en: "Current version, available updates, rolling back" },
|
||||
"settings.versions_and_updates": { zh: "版本与更新", en: "Versions and updates" },
|
||||
|
||||
"settings.logs_hint": { zh: "出问题时附上的日志位置", en: "Where the logs to attach when something goes wrong are" },
|
||||
"settings.trace_hint": { zh: "把执行轨迹导出给开发者排查", en: "Export execution traces for a developer to look into" },
|
||||
"settings.about_note": { zh: "社区与贡献者、赞助在这里展示,不是开关。", en: "The community, contributors and sponsorship are shown here; they are not switches." },
|
||||
"settings.look_at": { zh: "查看", en: "View" },
|
||||
"settings.set_up_trace": { zh: "导出轨迹", en: "Export traces" },
|
||||
|
||||
"settings.about": { zh: "关于", en: "About" },
|
||||
"settings.preview_office_in_workbench": { zh: "在工作台里预览 Word、Excel、PPT", en: "Preview Word, Excel and PowerPoint in the workbench" },
|
||||
"settings.divider_before_new_answer": { zh: "回到会话时,在新回答前画一条分隔线", en: "A divider before the new answer when you return to a session" },
|
||||
@@ -251,28 +251,22 @@ export const SETTINGS: Messages = {
|
||||
"settings.how_long_before_a_page_sleeps": { zh: "网页在后台闲置多久后休眠", en: "How long a background page idles before it sleeps" },
|
||||
"settings.hint_text_in_the_composer": { zh: "输入框里的提示文字", en: "The hint text inside the composer" },
|
||||
"settings.buttons_shown_on_row_hover": { zh: "会话行悬停时显示的按钮", en: "Buttons shown when you hover a session row" },
|
||||
"settings.which_entries_the_left_rail_shows": { zh: "左侧导航显示哪些入口、什么顺序", en: "Which entries the left rail shows, and in what order" },
|
||||
"settings.which_entries_the_left_rail_shows": { zh: "选择图标栏与侧栏工具卡片显示的入口和顺序", en: "Choose the entries and order for the icon rail and sidebar tools card" },
|
||||
"settings.finished_sessions_stay_in_running": { zh: "会话结束后在进行中列表里再留多久", en: "How long a finished session stays in the running list" },
|
||||
"settings.theme_card": { zh: "主题", en: "Theme" },
|
||||
"settings.text_card": { zh: "文字", en: "Text" },
|
||||
"settings.accent_and_overall_tone": { zh: "强调色和整体色调", en: "Accent colour and overall tone" },
|
||||
"settings.chat_background_image_or_video": { zh: "对话背景图片或视频", en: "The conversation's background image or video" },
|
||||
"settings.choose_wallpaper": { zh: "选择壁纸", en: "Choose wallpaper" },
|
||||
|
||||
"settings.frosted_glass_glow_effects": { zh: "毛玻璃、光晕等视觉效果", en: "Frosted glass, glow and other effects" },
|
||||
"settings.interface_and_code_typeface": { zh: "界面和代码字体", en: "Interface and code typefaces" },
|
||||
"settings.change": { zh: "更换", en: "Change" },
|
||||
"settings.overall_text_size": { zh: "整体文字大小", en: "Overall text size" },
|
||||
"settings.adjust": { zh: "调整", en: "Adjust" },
|
||||
|
||||
"settings.weight_of_text": { zh: "文字粗细", en: "Weight of text" },
|
||||
"settings.contrast_between_text_and_background": { zh: "文字和底色的对比", en: "Contrast between text and background" },
|
||||
"settings.view_and_edit": { zh: "查看与修改", en: "View and edit" },
|
||||
|
||||
"settings.all_shortcuts_incl_global_and_esc": { zh: "所有快捷键,含全局显示 / 隐藏、Esc 停止", en: "All shortcuts, including global show / hide and Esc to stop" },
|
||||
"settings.adjust_navigation": { zh: "调整导航", en: "Adjust navigation" },
|
||||
"settings.remember_window_position_and_size": { zh: "记住窗口位置和大小、标题栏样式", en: "Remember window position and size, and the title bar style" },
|
||||
|
||||
"settings.process_card": { zh: "过程", en: "Process" },
|
||||
"settings.files_and_previews_card": { zh: "文件与预览", en: "Files and previews" },
|
||||
"settings.notices_card": { zh: "提示", en: "Notices" },
|
||||
"settings.older_turns_fold_to_a_line": { zh: "较早的对话折叠成一行", en: "Earlier turns fold into one line" },
|
||||
|
||||
"settings.generated_files_as_clickable_cards": { zh: "生成的文件显示成可以点开的卡片", en: "Generated files appear as cards you can open" },
|
||||
"settings.tabs_and_navigation_card": { zh: "标签与导航", en: "Tabs and navigation" },
|
||||
"settings.while_running_card": { zh: "运行中", en: "While running" },
|
||||
|
||||
@@ -111,7 +111,7 @@ export const USAGE: Messages = {
|
||||
"usage.spend": { zh: "支出", en: "Spend" },
|
||||
"usage.spend_2": { zh: "花费", en: "Spend" },
|
||||
"usage.start_date": { zh: "开始日期", en: "Start date" },
|
||||
"usage.subscription_plan_estimated_at_the": { zh: "订阅套餐(按价格折算的估算)", en: "Subscription plan (estimated at the prices above)" },
|
||||
"usage.subscription_plan_estimated_at_the": { zh: "订阅套餐(估算)", en: "Subscription estimate" },
|
||||
"usage.subscription_use_priced_at_api": { zh: "按 API 价格估算的订阅用量,不是实际扣费", en: "Subscription use priced at API rates; not a charge" },
|
||||
"usage.subscription_use_priced_at_api_2": { zh: "订阅用量按 API 价格估算,不是实际扣费,也不计入上面的实付", en: "Subscription use priced at API rates. It is not a charge and is not part of the paid figure above." },
|
||||
"usage.the_backend_system_one_asks": { zh: "system_one 询问的后端", en: "The backend system_one asks" },
|
||||
|
||||
@@ -170,8 +170,6 @@ export const WORKBENCH: Messages = {
|
||||
"workbench.will_send": { zh: "将发送", en: "Will send" },
|
||||
"workbench.you_do_not_have_permission": { zh: "没有权限读取这个文件。", en: "You do not have permission to read this file." },
|
||||
"workbench.changes_history_and_files": { zh: "改动、提交历史和文件", en: "Changes, history and files" },
|
||||
"workbench.find_a_file": { zh: "文件…", en: "Find a file…" },
|
||||
"workbench.find_a_file_by_name": { zh: "按名字找项目里的文件", en: "Find a file in the project by name" },
|
||||
"workbench.new_tab_label": { zh: "新标签", en: "New tab" },
|
||||
"workbench.line_n": { zh: "第 {n} 行", en: "Line {n}" },
|
||||
"workbench.unsaved": { zh: "未保存", en: "Unsaved" },
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { fromHistory, initialState, reduce, type Item, type SessionEvent } from "./session";
|
||||
import { thoughtStartedAt } from "./say";
|
||||
|
||||
beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(1_700_000_000_000); });
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
const ev = (kind: string, fields: Record<string, unknown> = {}) => ({ kind, ...fields }) as SessionEvent;
|
||||
const tool = { id: "call-1", name: "bash", issuer: "model" };
|
||||
const says = (items: Item[]) => items.filter((item): item is Extract<Item, { t: "say" }> => item.t === "say");
|
||||
const thinking = () => [ev("turn_started"), ev("reasoning", { text: "Inspect first. " })].reduce(reduce, initialState);
|
||||
|
||||
describe("one assistant response across interleaved events", () => {
|
||||
it("stops thinking when tool arguments start, then settles the original card with the message frame", () => {
|
||||
let state = thinking();
|
||||
const id = says(state.items)[0].id;
|
||||
vi.advanceTimersByTime(1200);
|
||||
state = reduce(state, ev("tool_dispatch", { tool: { ...tool, partial: true } }));
|
||||
expect(says(state.items)[0]).toMatchObject({ id, done: false, thoughtMs: 1200 });
|
||||
expect(thoughtStartedAt(id)).toBeUndefined();
|
||||
vi.advanceTimersByTime(30_000);
|
||||
state = reduce(state, ev("text", { text: "I will inspect the files." }));
|
||||
state = reduce(state, ev("message", { reasoning: "Inspect first.", text: "I will inspect the files.", thoughtMs: 1100 }));
|
||||
expect(says(state.items)).toHaveLength(1);
|
||||
expect(says(state.items)[0]).toMatchObject({ id, done: true, thoughtMs: 1100, reasoning: "Inspect first." });
|
||||
state = reduce(state, ev("tool_dispatch", { tool }));
|
||||
state = reduce(state, ev("tool_result", { tool: { ...tool, output: "files" } }));
|
||||
state = reduce(state, ev("reasoning", { text: "Now edit." }));
|
||||
expect(says(state.items)).toHaveLength(2);
|
||||
expect(says(state.items)[1]).toMatchObject({ done: false, reasoning: "Now edit." });
|
||||
});
|
||||
|
||||
it("keeps the active response across notices, pending input and child tool events", () => {
|
||||
let state = thinking();
|
||||
state = reduce(state, ev("notice", { text: "A background task finished." }));
|
||||
state = reduce(state, ev("__user", { text: "Also check styles", pending: true }));
|
||||
state = reduce(state, ev("tool_dispatch", { tool: { ...tool, parentId: "worker" } }));
|
||||
vi.advanceTimersByTime(800);
|
||||
state = reduce(state, ev("reasoning", { text: "Then compare." }));
|
||||
expect(says(state.items)).toHaveLength(1);
|
||||
expect(says(state.items)[0]).toMatchObject({ reasoning: "Inspect first. Then compare.", done: false });
|
||||
expect(says(state.items)[0].thoughtMs).toBeUndefined();
|
||||
state = reduce(state, ev("text", { text: "Compared." }));
|
||||
expect(says(state.items)[0].thoughtMs).toBe(800);
|
||||
});
|
||||
|
||||
it("seals the response at full dispatch even when its message frame was missed", () => {
|
||||
let state = thinking();
|
||||
vi.advanceTimersByTime(900);
|
||||
state = reduce(state, ev("tool_dispatch", { tool }));
|
||||
expect(says(state.items)[0]).toMatchObject({ done: true, thoughtMs: 900 });
|
||||
state = reduce(state, ev("tool_result", { tool }));
|
||||
state = reduce(state, ev("reasoning", { text: "The next round." }));
|
||||
expect(says(state.items)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("keeps an explicit zero measurement from the host over a local estimate", () => {
|
||||
let state = thinking();
|
||||
vi.advanceTimersByTime(900);
|
||||
state = reduce(state, ev("message", { reasoning: "Inspect first.", thoughtMs: 0 }));
|
||||
expect(says(state.items)[0].thoughtMs).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("canonical idle state after an interrupted event stream", () => {
|
||||
it("seals the thought once without inventing a turn outcome", () => {
|
||||
let state = thinking();
|
||||
vi.advanceTimersByTime(2400);
|
||||
state = reduce(state, ev("__running", { running: false }));
|
||||
expect(says(state.items)[0]).toMatchObject({ done: true, thoughtMs: 2400 });
|
||||
expect(state.terminal).toBeNull();
|
||||
const settled = state;
|
||||
vi.advanceTimersByTime(1_800_000);
|
||||
state = reduce(state, ev("__running", { running: false }));
|
||||
expect(state).toBe(settled);
|
||||
expect(says(state.items)[0].thoughtMs).toBe(2400);
|
||||
});
|
||||
|
||||
it("also seals a stream whose turn-start event was missed", () => {
|
||||
let state = reduce(initialState, ev("reasoning", { text: "Already streaming." }));
|
||||
expect(state.running).toBe(false);
|
||||
state = reduce(state, ev("__running", { running: false }));
|
||||
expect(says(state.items)[0].done).toBe(true);
|
||||
});
|
||||
|
||||
it("does not close an ongoing response or start a clock on history", () => {
|
||||
const state = thinking();
|
||||
expect(reduce(state, ev("__running", { running: true }))).toBe(state);
|
||||
const history = fromHistory([{ role: "assistant", content: "Done.", reasoning: "Checked.", thoughtMs: 1600 }]);
|
||||
const restored = reduce(state, { kind: "__restore", ...history });
|
||||
const idle = reduce(restored, ev("__running", { running: false }));
|
||||
expect(says(idle.items)).toHaveLength(1);
|
||||
expect(says(idle.items)[0]).toMatchObject({ done: true, thoughtMs: 1600 });
|
||||
expect(thoughtStartedAt(says(idle.items)[0].id)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -12,23 +12,37 @@ export function thoughtStartedAt(id: string): number | undefined {
|
||||
return thoughtSince.get(id);
|
||||
}
|
||||
|
||||
const isWaitingLine = (item: Item) => item.t === "user" && !!item.pending;
|
||||
function openSayIndex(items: Item[]): number {
|
||||
for (let i = items.length - 1; i >= 0; i--) {
|
||||
const item = items[i];
|
||||
if (item.t === "say") return item.done ? -1 : i;
|
||||
if (item.t === "user" && !item.pending) return -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// A partial tool dispatch ends thinking but its response still owes a message frame.
|
||||
export function stopThought(items: Item[]): Item[] {
|
||||
const at = openSayIndex(items);
|
||||
if (at < 0) return items;
|
||||
const item = items[at] as Extract<Item, { t: "say" }>;
|
||||
if (!item.reasoning || item.thoughtMs !== undefined) return items;
|
||||
const thoughtMs = Math.max(0, Date.now() - (thoughtSince.get(item.id) ?? Date.now()));
|
||||
thoughtSince.delete(item.id);
|
||||
const next = items.slice();
|
||||
next[at] = { ...item, thoughtMs };
|
||||
return next;
|
||||
}
|
||||
|
||||
export function appendText(items: Item[], text: string, field: "text" | "reasoning", source?: string, model?: string): Item[] {
|
||||
// A line typed while the model is still writing waits in the transcript after
|
||||
// the open message without being part of it, so the message continues past it.
|
||||
let at = items.length - 1;
|
||||
while (at >= 0 && isWaitingLine(items[at])) at--;
|
||||
const at = openSayIndex(items);
|
||||
if (field === "text") items = stopThought(items);
|
||||
const last = items[at];
|
||||
if (last && last.t === "say" && !last.done) {
|
||||
const next = items.slice();
|
||||
// Thinking runs until the first answer token, so the clock is the gap
|
||||
// between the two streams, not the length of either.
|
||||
const stop = field === "text" && last.thoughtMs === undefined && last.reasoning;
|
||||
next[at] = {
|
||||
...last,
|
||||
[field]: (last[field] ?? "") + text,
|
||||
...(stop ? { thoughtMs: Date.now() - (thoughtSince.get(last.id) ?? Date.now()) } : null),
|
||||
};
|
||||
return next;
|
||||
}
|
||||
@@ -69,16 +83,10 @@ export function sealSay(items: Item[], all = false, cut?: ThoughtCut): Item[] {
|
||||
// agree. A stream that lost a chunk is therefore repaired here, never
|
||||
// preserved; an empty field is not content and overwrites nothing.
|
||||
export function foldMessage(items: Item[], ev: WireEvent): Item[] {
|
||||
let at = -1;
|
||||
for (let i = items.length - 1; i >= 0; i--) {
|
||||
if (items[i].t === "say" && !(items[i] as Extract<Item, { t: "say" }>).done) {
|
||||
at = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const at = openSayIndex(items);
|
||||
// The kernel's measure wins over this window's: it is the one the transcript
|
||||
// keeps, so a reopened turn says the same thing.
|
||||
const measured = ev.thoughtMs || undefined;
|
||||
const measured = ev.thoughtMs;
|
||||
if (at < 0) {
|
||||
if (!ev.text && !ev.reasoning) return items;
|
||||
return [...items, { t: "say", id: nextId(), text: ev.text ?? "", reasoning: ev.reasoning, done: true, source: ev.source, thoughtMs: measured }];
|
||||
|
||||
@@ -22,7 +22,7 @@ export { currentStep, stepDone, stepLabel };
|
||||
import { askAnnounced, askEngaged, askFromCall, promptOpen, prompted, sealByReceipt } from "./prompts";
|
||||
import { stampPlan } from "./plan_card";
|
||||
import { nameTurnStart, othersSteer } from "./turn_start";
|
||||
import { appendText, foldMessage, sealSay } from "./say";
|
||||
import { appendText, foldMessage, sealSay, stopThought } from "./say";
|
||||
import { nextId } from "./ids";
|
||||
import { worked } from "./worked";
|
||||
import { chipLabel, IDLE, RUNNING } from "./chip";
|
||||
@@ -349,7 +349,10 @@ function apply(s: SessionState, ev: SessionEvent): SessionState {
|
||||
// /status's own answer, because the record cannot end a turn: the kernel
|
||||
// commits a text-only round before it decides whether the turn continues,
|
||||
// so only the kernel saying idle can close one. Equal answers fold to s.
|
||||
if (ev.kind === "__running") return s.running === ev.running ? s : { ...s, running: ev.running };
|
||||
if (ev.kind === "__running") {
|
||||
const items = ev.running ? s.items : sealSay(s.items, true);
|
||||
return s.running === ev.running && items === s.items ? s : { ...s, running: ev.running, items };
|
||||
}
|
||||
// A wait only text or reasoning could end outlived every turn whose first
|
||||
// packet was a tool call: the retry line and its clock stayed up for the rest
|
||||
// of the turn, over calls that were running fine.
|
||||
@@ -400,7 +403,7 @@ function apply(s: SessionState, ev: SessionEvent): SessionState {
|
||||
|
||||
case "tool_dispatch":
|
||||
return ev.tool
|
||||
? { ...s, doing: ev.tool.name, executions: noteTool(s.executions, ev.tool, false), items: foldTool(s.items, ev.tool, true) }
|
||||
? { ...s, doing: ev.tool.name, executions: noteTool(s.executions, ev.tool, false), items: foldTool(ev.tool.parentId ? s.items : ev.tool.partial ? stopThought(s.items) : sealSay(s.items), ev.tool, true) }
|
||||
: s;
|
||||
|
||||
case "tool_progress":
|
||||
|
||||
@@ -63,6 +63,13 @@ and selected states, while long titles truncate within the remaining width.
|
||||
hierarchy to weight alone. `.side .lbl` is listed there for specificity: the
|
||||
right column's same-named rule comes later in the file.
|
||||
|
||||
## Select alignment
|
||||
|
||||
Styleable native selects center their value on the cross axis. Their fixed
|
||||
control height and centered background arrow must share the same text center,
|
||||
including settings rows and enlarged interface fonts. The browser still owns
|
||||
the picker and keyboard navigation.
|
||||
|
||||
## Wallpaper and sky
|
||||
|
||||
- `body::before` is the theme's picture, when a pack ships one. It sits behind
|
||||
@@ -97,6 +104,9 @@ and selected states, while long titles truncate within the remaining width.
|
||||
|
||||
## Title bar and chrome
|
||||
|
||||
- Navigation selection uses the theme's tinted surface and readable accent
|
||||
ink, with a side marker and a light inset border. Theme packs use the same
|
||||
treatment to distinguish page position from solid primary-action buttons.
|
||||
- `-webkit-app-region` is Chromium's drag property. Inside the shell this row
|
||||
*is* the title bar, so it has to be the drag handle, and every control on it
|
||||
opts back out. Membership is what the markup says is interactive, not a list of the
|
||||
@@ -218,6 +228,12 @@ and selected states, while long titles truncate within the remaining width.
|
||||
|
||||
## Composer shelf
|
||||
|
||||
- Send and stop use 18px Lucide icons inside their circular hit targets. Stop
|
||||
keeps its filled square on every screen size; a tinted surface distinguishes
|
||||
it without an outline competing with the symbol.
|
||||
- Model choices reserve the first line for the model name. Protocol and image
|
||||
capability share the second line; the capability badge and connection status
|
||||
keep their width, while long names and endpoints truncate inside the menu.
|
||||
- A mode is this turn's capability, not a preference, which is why it lives on
|
||||
the composer and not in settings. A pill is its label's width or it is nothing:
|
||||
shrinking one squeezed the shelf's own controls, and Chinese wraps.
|
||||
@@ -853,81 +869,35 @@ what exists.
|
||||
mixed text. The "get a key" entry sits at the label's right end with tracking
|
||||
zeroed, because the label's .06em is for Latin small labels.
|
||||
|
||||
## The settings sheet
|
||||
## Full-screen settings
|
||||
|
||||
- One title bar exists — `.chrome` — and it owns the window buttons and the drag
|
||||
region; this layer starts below it, so the window buttons still work while
|
||||
settings is open.
|
||||
- The backdrop dims and softens but stays thin: the sheet over it is glass, and
|
||||
what shows through glass has to be light.
|
||||
- A floating layer fades in rather than sliding: a full-bleed blurred backdrop
|
||||
shows its edge the moment it moves. The sheet is conditionally rendered, so it
|
||||
had an entrance and no way out — it simply stopped existing. A view transition
|
||||
gives it both ends. Only the named element takes part; without naming the root
|
||||
explicitly, every swap also cross-fades the whole window.
|
||||
- The change preview uses the same pair — a dimmed, blurred backdrop and a
|
||||
raised surface starting below the title bar — for the same reason, including
|
||||
the same view transition. The diff inside it drops the top margin it carries
|
||||
for use inside a card, where it is one element among several; here it is the
|
||||
only content.
|
||||
- A very faint gilt line under the header says this is the window's own thing,
|
||||
not content.
|
||||
- The header's expand/restore and close buttons share a 32px hit-region height,
|
||||
matching the search control's minimum height. Inline-flex centres SVG and text
|
||||
content independently of font fallback; native window controls retain their own sizing.
|
||||
- The search entry sits beside the title. That position used to hold one
|
||||
explanation addressed to two of the three tiers; that sentence now lives on
|
||||
each settings block (`.apply`). Results are laid under the header without
|
||||
repainting settings itself: looking for something should not take away the
|
||||
page you are reading.
|
||||
- Search results give the setting name a full-width first row. Section and
|
||||
scope/apply metadata share a wrapping second row; neither can consume the
|
||||
name's width. Results retain their content height inside the scrolling list.
|
||||
- What changing a setting costs is a fact, not a warning: one colour for all
|
||||
three tiers, only lighter than body text. What deserves colour is a present
|
||||
state such as "cannot be changed while a turn is running".
|
||||
- A search landing flashes once: it is navigation feedback, not a changed
|
||||
setting.
|
||||
- The nav column is 224px because of the Chinese names: "Tools and permissions"
|
||||
needs 138px at that width while a row leaves the name 99.6px. The extra 16px
|
||||
does not close the gap but decides how many characters of the value survive
|
||||
beside it. It is named `.navsec`, not `.navgrp`: the rail already owns that
|
||||
name for its columns of buttons.
|
||||
- The nav doubles as a readout: the current value hangs at the right, so opening
|
||||
settings shows at a glance what this session runs under — which was the only
|
||||
reason the separate status bar existed.
|
||||
- The value gives up its width first. Chinese names fit this column and English
|
||||
ones do not, and a row that truncates the name says nothing. The name's basis
|
||||
is its content width rather than 0: at 0 the row never "overflows", the name
|
||||
simply takes what is left and is clipped, and the value is never asked to
|
||||
yield. The value's shrink factor is 99, not 1: with both shrinkable the browser
|
||||
splits the overflow in proportion to their bases and takes a slice off the name
|
||||
too — 38px off "工具与权限" in the English build.
|
||||
- A bar props the selected row up from the left. In a column of equal pills a
|
||||
fill alone gives out on one side or the other.
|
||||
- `.nv` is real information, not decoration: `--ghost` at 10.5px measures 2:1 and
|
||||
cannot be read.
|
||||
- The settings column hugs the nav rather than centring: with only a left rail,
|
||||
centring suspends the content as an island. 760px is a reading measure for
|
||||
prose; the extensions page is a table of five columns and is the one page
|
||||
widened, or its descriptions are clipped.
|
||||
- Changing section is one navigation: what changes is the column, not each block
|
||||
arriving separately. Four blocks each flying in is four announcements of "here
|
||||
is something new" when nothing happened — you walked to another page. The
|
||||
section mark is the section's mark, not its state: it appears on every page, so
|
||||
tinting it amber says every page needs you.
|
||||
- A state is said in the vocabulary of the separation it modifies: with no line
|
||||
to tint, hover lifts the fill and focus rings.
|
||||
- The advanced fold hides three complete settings blocks, not a note, so its
|
||||
summary is drawn at a settings row's weight. Collapsed, `.ds` says whether
|
||||
anything inside has been changed: hiding a setting that is in force leaves the
|
||||
page disagreeing with the window.
|
||||
- The page heading says where you are; block headings stay strong enough to scan
|
||||
the individual decisions. A command named in a note is set in the interface's
|
||||
own mono, because a bare `<code>` inherits the browser's default family.
|
||||
- Settings fills the application window and owns a draggable header with a
|
||||
labelled return action and native window controls. The header remains outside
|
||||
the settings scroller and search results, inside its focus boundary.
|
||||
- Desktop navigation stays 240px wide. Content uses the remaining width with
|
||||
fixed 24px side gutters; every section card fills one column without a cap.
|
||||
Phone navigation becomes a horizontal strip with 14px content gutters.
|
||||
- Categories keep their editors expanded. Pill navigation scrolls below the
|
||||
sticky index without replacing editors or discarding drafts. Leaving a
|
||||
category retains the existing unsaved-change guard.
|
||||
- Page, card and group headings use distinct sizes; groups use h4 beneath card
|
||||
h3 headings. Descriptions wrap and related controls share a card.
|
||||
- `settings-fullscreen.mjs` checks every category in both schemes and widths,
|
||||
including enlarged text; `settings-page-column.mjs` checks pill landing.
|
||||
|
||||
- Search overlays the content column while preserving its editors. Results use
|
||||
a full-width name followed by wrapping category, scope and effect metadata.
|
||||
- Scope and effect are facts with neutral styling. Landing briefly marks the
|
||||
located setting; changing category resets only the content scroller.
|
||||
- Navigation labels keep their content width before secondary counts yield.
|
||||
Search aliases never become separate settings or controls.
|
||||
|
||||
## Settings rows
|
||||
|
||||
Log paths and account notes wrap within their rows, including long unbroken
|
||||
paths at enlarged text sizes. Their content never relies on the card gutters
|
||||
to absorb horizontal overflow.
|
||||
|
||||
- One option per row, shared with the ask card as one control: two copies drift
|
||||
apart. `.prow`'s only differences are its own disabled and danger states.
|
||||
- Risk is present before you click, not red after you select: with "allow
|
||||
@@ -1597,7 +1567,7 @@ A sub-agent row, by the list's own width:
|
||||
|
||||
## Dialogs centre on the window
|
||||
|
||||
- A veil (settings, feedback, a full screenshot, a checkpoint preview, a
|
||||
- A veil (feedback, a full screenshot, a checkpoint preview, a
|
||||
sub-agent transcript) starts below the title bar so the bar stays usable.
|
||||
- Centred in that box alone, a dialog would sit half a bar below the window's
|
||||
middle, which reads as off balance against the whole window.
|
||||
@@ -1605,14 +1575,13 @@ A sub-agent row, by the list's own width:
|
||||
bar, so the box it centres in is symmetric about the window's middle. A border
|
||||
rather than padding because padding on these veils is set per breakpoint, and
|
||||
percentage heights inside then leave room for it on their own.
|
||||
- The phone layout (`data-fold~="scene"`) shows settings full screen, so it is
|
||||
excluded.
|
||||
- The phone layout (`data-fold~="scene"`) is excluded. Settings owns a full
|
||||
screen at every width and is outside the dialog-centering selector.
|
||||
|
||||
## Settings and feedback veils
|
||||
## Feedback veil
|
||||
|
||||
- `.prefs` and `.fbk-veil` use the same flat, 93% page-colour scrim in both
|
||||
themes, not a blurred backdrop. Settings stays visually separate from live
|
||||
content without filtering that content on every frame.
|
||||
- `.fbk-veil` uses a flat, 93% page-colour scrim in both themes without
|
||||
filtering the live content on every frame.
|
||||
- A backdrop filter over the whole window is redone every frame while anything
|
||||
beneath it moves; a running turn's spinners are enough.
|
||||
- The scrim is opaque enough that what shows through reads as dimmed, which is
|
||||
@@ -1756,3 +1725,25 @@ A sub-agent row, by the list's own width:
|
||||
- Appearance → Conversation display offers “Show collapsed plan progress,” off by default and persisted on this machine. Changing it immediately updates mounted plan cards.
|
||||
- A nonempty collapsed plan draws the expanded plan's `.prog2` track below its summary, using completed steps over total steps from `stepDone`. Pending and running steps do not count as complete.
|
||||
- Opening the card replaces the optional track with the existing expanded progress. Goal-only, empty and hidden cards have no progress track; collapse, Escape, outside dismissal and focus return remain unchanged.
|
||||
|
||||
## Provider form fields
|
||||
|
||||
- A compound input gives its editable text the space left after the unit label.
|
||||
Only standalone setting-row and threshold controls use a fixed numeric width.
|
||||
- Provider inputs and selects share a 32px height. Model names occupy their own
|
||||
row; capacity and price fields fill the remaining grid width. Container queries
|
||||
reduce columns using the form's available width and scaled text size.
|
||||
- Billing choices use short names, with their meaning explained beneath the
|
||||
picker. The provider form's selects fill their column and reserve arrow space.
|
||||
- `provider-fields.mjs` checks full numeric values, inheritance hints, currency
|
||||
and billing choices at wide, intermediate and phone widths, both languages,
|
||||
both schemes and enlarged text.
|
||||
|
||||
## Sidebar tool card
|
||||
|
||||
- The footer keeps Settings and Feedback as labelled rows aligned at the left.
|
||||
Enabled shortcuts appear in a card with library icons and names, preserving
|
||||
their saved order and visibility. Hiding every shortcut removes the chevron.
|
||||
- Hover waits briefly before opening and closing so the pointer can cross to
|
||||
the card. Click and keyboard activation also work; Escape restores focus.
|
||||
The portaled card follows its anchor and stays inside the viewport.
|
||||
@@ -104,7 +104,7 @@
|
||||
}
|
||||
|
||||
:root[data-boot] .app { animation: m-card 1.2s cubic-bezier(.4, 0, .2, 1) both }
|
||||
|
||||
|
||||
:root[data-boot] .chrome::after {
|
||||
content: ""; position: absolute; inset: 0; z-index: 2; pointer-events: none;
|
||||
background: linear-gradient(105deg, transparent 34%,
|
||||
@@ -122,7 +122,7 @@
|
||||
:root[data-boot] .chrome::after { display: none }
|
||||
}
|
||||
|
||||
|
||||
|
||||
[data-titlebar="app"] .chrome { -webkit-app-region: drag }
|
||||
[data-titlebar="app"] .chrome
|
||||
*:not(:is(button, a[href], input, select, textarea, summary, .picker,
|
||||
@@ -160,7 +160,7 @@
|
||||
display: grid; place-items: center; width: 44px; border: 0; background: transparent;
|
||||
color: var(--faint); cursor: pointer; transition: background var(--m-line), color var(--m-line);
|
||||
}
|
||||
.winctl .wc svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 1.1 }
|
||||
.winctl .wc svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 2 }
|
||||
.winctl .wc:hover { background: var(--overlay); color: var(--text) }
|
||||
.winctl .wc.close:hover { background: var(--err); color: #fff }
|
||||
.winctl .wc:focus-visible { outline: 2px solid var(--focus); outline-offset: -2px }
|
||||
@@ -180,7 +180,7 @@
|
||||
font-weight: var(--w-med); color: var(--muted);
|
||||
flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
.picker { position: relative; flex: none }
|
||||
.isolab { flex: none; white-space: nowrap; font: var(--w-med) var(--fs-micro) var(--ui); letter-spacing: var(--track-cap); color: var(--muted); padding: 1.5px 6px; border-radius: var(--r-pill); background: var(--overlay) }
|
||||
.isolab[hidden] { display: none }
|
||||
@@ -331,7 +331,7 @@
|
||||
}
|
||||
.mode:hover .lb, .mode[aria-expanded="true"] .lb { color: var(--muted) }
|
||||
.mode:hover .vl, .mode[aria-expanded="true"] .vl { color: var(--text) }
|
||||
|
||||
|
||||
.mode .ic { display: grid; place-items: center; width: 12px; height: 12px; flex: none }
|
||||
.mode .ic svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round }
|
||||
.mode.danger { color: var(--err-ink); border-color: color-mix(in srgb, var(--err) 45%, var(--border)) }
|
||||
@@ -342,7 +342,7 @@
|
||||
background: color-mix(in srgb, var(--err) 12%, transparent);
|
||||
animation: m-pulse var(--t-live) var(--ease) infinite;
|
||||
}
|
||||
|
||||
|
||||
.sep { width: 1px; height: 14px; flex: none; margin-inline: -1px; background: var(--border); opacity: .75 }
|
||||
|
||||
.side .block { transition: opacity var(--m-panel) , filter var(--m-panel) }
|
||||
@@ -362,7 +362,7 @@
|
||||
.thbtn svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round }
|
||||
|
||||
.pip { width: 6px; height: 6px; border-radius: 50%; background: var(--ghost); flex: none }
|
||||
|
||||
|
||||
.rmark { width: 15px; height: 15px; flex: none; display: block; color: var(--ghost); transition: color var(--m-panel) }
|
||||
.rmark path {
|
||||
fill: none; stroke: currentColor; stroke-width: 1.7;
|
||||
@@ -374,7 +374,7 @@
|
||||
.pane[data-run="running"] .rmark path { stroke-dashoffset: 0 }
|
||||
|
||||
.pane[data-run="halt"] .rmark { color: var(--accent-ink) }
|
||||
|
||||
|
||||
.pane[data-run="done"] .rmark { color: var(--ok) }
|
||||
|
||||
.queue {
|
||||
@@ -433,7 +433,7 @@
|
||||
width: 5px; height: 5px; flex: none; border-radius: 50%;
|
||||
background: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 11%, transparent);
|
||||
}
|
||||
|
||||
|
||||
.qi .pill {
|
||||
margin-left: 0; padding: 2px 7px; border-radius: var(--r-xs);
|
||||
font: var(--w-med) var(--fs-micro) var(--mono); letter-spacing: .03em;
|
||||
@@ -473,7 +473,7 @@
|
||||
}
|
||||
.qi .x:hover, .qi .x:focus-visible { color: var(--text); background: var(--overlay) }
|
||||
|
||||
|
||||
|
||||
.call[data-pending] > .c { opacity: .7 }
|
||||
|
||||
.app {
|
||||
@@ -597,7 +597,7 @@
|
||||
.ws .where .tag { flex: none; color: var(--accent-ink) }
|
||||
.ws[data-iso="on"] .where .tag { color: var(--ok-ink) }
|
||||
|
||||
|
||||
|
||||
.main { position: relative; display: flex; flex-direction: column; min-height: 0; background: var(--surface) }
|
||||
|
||||
.vtabs { display: flex; align-items: center; gap: 2px; min-width: 0 }
|
||||
@@ -657,10 +657,11 @@
|
||||
.btab { max-width: 220px; overflow: hidden; text-overflow: ellipsis }
|
||||
.bnav { display: flex; align-items: center; gap: 4px }
|
||||
.bbtn {
|
||||
flex: none; width: 28px; height: 28px; border: 0; border-radius: var(--r-sm);
|
||||
background: none; color: var(--faint); cursor: pointer; font: var(--w-med) var(--fs-ui) var(--ui);
|
||||
flex: none; display: grid; place-items: center; width: 28px; height: 28px; padding: 0; border: 0; border-radius: var(--r-sm);
|
||||
background: none; color: var(--faint); cursor: pointer;
|
||||
}
|
||||
.bbtn:hover { color: var(--text); background: var(--overlay) }
|
||||
.bbtn:hover:not(:disabled) { color: var(--text); background: var(--overlay) }
|
||||
.bbtn:disabled { opacity: .3; cursor: default }
|
||||
.bbtn:focus-visible { outline: 2px solid var(--focus); outline-offset: 1px }
|
||||
.baddr { flex: 1; min-width: 0 }
|
||||
.baddr input {
|
||||
@@ -827,7 +828,6 @@
|
||||
.call[data-k="host"] > .c .hl .nm { font-weight: var(--w-reg); color: var(--muted) }
|
||||
|
||||
.call[data-k="say"] > .g .sym { background: none; box-shadow: none; color: var(--text) }
|
||||
.call[data-k="say"] > .g .sym svg { fill: currentColor }
|
||||
|
||||
.call[data-k="say"] > .g {
|
||||
background: linear-gradient(color-mix(in srgb, var(--border) 88%, transparent) 0 0)
|
||||
@@ -856,7 +856,7 @@
|
||||
.hl { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; transition: opacity var(--m-exit) }
|
||||
.hl[data-swap] { opacity: 0 }
|
||||
.enterbox[data-enter] .hl .cost { animation: m-panel var(--t-card) var(--ease-out) both }
|
||||
|
||||
|
||||
.call > .g .sym {
|
||||
transition: opacity var(--t-card) var(--ease-out),
|
||||
box-shadow var(--t-card) var(--ease-out);
|
||||
@@ -1178,12 +1178,12 @@
|
||||
.comp details { margin-top: var(--y-close) }
|
||||
.comp-l { margin-top: 5px; display: flex; flex-direction: column; gap: 3px }
|
||||
.comp-l .row { display: flex; gap: 8px; font: var(--w-reg) var(--fs-meta) var(--mono); color: var(--faint) }
|
||||
.comp-l .row .x { color: var(--del); flex: none; width: 10px }
|
||||
.comp-l .row .k { color: var(--ok); flex: none; width: 10px; font-size: var(--fs-micro) }
|
||||
.comp-l .row .x { color: var(--del); flex: none; width: 16px }
|
||||
.comp-l .row .k { color: var(--ok); flex: none; width: 16px }
|
||||
|
||||
.steps { margin-top: 6px; display: flex; flex-direction: column; gap: 4px }
|
||||
.steps .s { display: flex; align-items: baseline; gap: 9px; font-size: var(--fs-ui); color: var(--muted) }
|
||||
.steps .s .b { font: var(--w-reg) var(--fs-micro)/1.5 var(--mono); width: 10px; flex: none; color: var(--ghost) }
|
||||
.steps .s .b { display: inline-flex; align-items: center; justify-content: center; font: var(--w-reg) var(--fs-micro)/1.5 var(--mono); width: 16px; flex: none; color: var(--ghost) }
|
||||
.steps .s[data-done] { color: var(--faint) }
|
||||
.steps .s[data-done] .b { color: var(--ok) }
|
||||
.steps .s[data-now] { color: var(--text) }
|
||||
@@ -1364,7 +1364,7 @@
|
||||
.intake[data-tone="ref"] { color: var(--net) }
|
||||
.intake[data-tone="insert"] { color: var(--ok-ink) }
|
||||
.intake[data-tone="reject"] { color: var(--faint) }
|
||||
|
||||
|
||||
.shots {
|
||||
list-style: none; margin: 0; display: flex; flex-wrap: nowrap; align-items: center; gap: 7px;
|
||||
padding: 0 2px 8px; overflow-x: auto; overscroll-behavior-inline: contain; scrollbar-width: thin;
|
||||
@@ -1556,7 +1556,7 @@
|
||||
.go { display: inline-flex; align-items: center; gap: 7px; flex: none }
|
||||
|
||||
.compose .row > * { position: relative; z-index: 8; animation: m-card var(--m-panel) both }
|
||||
|
||||
|
||||
.compose .row [data-pending] { pointer-events: none; opacity: .5; transition: opacity var(--m-line) }
|
||||
|
||||
.compose .row:has([data-pending])::after {
|
||||
@@ -1575,7 +1575,7 @@
|
||||
|
||||
.policy .vl .polrisk { color: var(--err-ink); font-weight: var(--w-med) }
|
||||
.policy .vl .polwarn { margin-right: 4px; font-style: normal; display: inline-grid; vertical-align: -1.5px }
|
||||
.policy .vl .polwarn svg, .cachemoved i svg {
|
||||
.policy .vl .polwarn svg, .cachemoved > .studio-icon {
|
||||
width: 12px; height: 12px; fill: none; stroke: currentColor;
|
||||
stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round;
|
||||
}
|
||||
@@ -1587,7 +1587,7 @@
|
||||
color: var(--err-ink);
|
||||
box-shadow: var(--lit), 0 0 0 1px color-mix(in srgb, var(--err) 42%, transparent);
|
||||
}
|
||||
|
||||
|
||||
.polmenu {
|
||||
min-width: 296px; padding: 13px 14px 12px; left: auto; right: 0;
|
||||
display: flex; flex-direction: column; gap: 16px;
|
||||
@@ -1793,7 +1793,7 @@
|
||||
display: flex; align-items: baseline; gap: 5px; margin: 8px 0 0;
|
||||
font: var(--w-reg) var(--fs-meta) var(--ui); color: var(--warn-ink);
|
||||
}
|
||||
.cachemoved i { font-style: normal; display: inline-grid; vertical-align: -1.5px }
|
||||
.cachemoved > .studio-icon { align-self: center }
|
||||
|
||||
.bar { display: flex; height: 3px; overflow: hidden; background: var(--hair); gap: 1px }
|
||||
.bar i { display: block; transition: flex-grow var(--t-live) var(--ease); position: relative }
|
||||
@@ -1985,7 +1985,7 @@
|
||||
.mode .vl, .mode.danger::before, .compose .row > *, .call[data-running] > .g .sym { animation: none !important }
|
||||
|
||||
.gutter[data-edge]::after { animation: m-pulse var(--t-card) var(--ease) }
|
||||
|
||||
|
||||
.compose .row:has([data-pending])::after { animation: none; background: var(--net); opacity: .5 }
|
||||
.scroll { scroll-behavior: auto }
|
||||
.btn:active, .navbtn:active,
|
||||
@@ -2109,7 +2109,7 @@
|
||||
animation: m-panel var(--t-panel) var(--ease-spring) both;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@supports (view-transition-name: none) {
|
||||
:root { view-transition-name: none }
|
||||
.prefs { view-transition-name: prefsveil; animation: none }
|
||||
@@ -2336,7 +2336,7 @@
|
||||
.advset:not(.appearance-advanced) > .grp:last-child { border-bottom: 0 }
|
||||
.grp-hd { display: flex; align-items: baseline; gap: 12px }
|
||||
|
||||
.grp > .grp-hd h3, .grp > h3.grp-hd { margin: 0; font: var(--w-semi) var(--fs-lead)/1.35 var(--ui); color: var(--text); letter-spacing: -.005em }
|
||||
.grp > .grp-hd :is(h3, h4), .grp > h3.grp-hd { margin: 0; font: var(--w-semi) var(--fs-lead)/1.35 var(--ui); color: var(--text); letter-spacing: -.005em }
|
||||
.grp .now {
|
||||
margin-left: auto; flex: none; padding: 2px 7px; border-radius: var(--r-pill);
|
||||
background: var(--overlay); font: var(--w-reg) var(--fs-meta) var(--ui); color: var(--faint);
|
||||
@@ -2771,7 +2771,7 @@
|
||||
.addp-head strong { display: block; font: var(--w-semi) var(--fs-lead) var(--ui); color: var(--text) }
|
||||
.addp-head p { margin: 4px 0 0; font: var(--w-reg) var(--fs-meta)/1.5 var(--ui); color: var(--muted) }
|
||||
.addp-close {
|
||||
flex: none; width: 28px; height: 28px; border: 0; border-radius: var(--r-xs);
|
||||
flex: none; width: 28px; height: 28px; display: grid; place-items: center; border: 0; border-radius: var(--r-xs);
|
||||
background: transparent; color: var(--faint); font: var(--w-reg) var(--fs-title)/1 var(--ui); cursor: pointer;
|
||||
}
|
||||
.addp-close:hover { background: var(--overlay); color: var(--text) }
|
||||
@@ -3296,7 +3296,7 @@
|
||||
.probes { margin-top: 14px; display: flex; flex-direction: column }
|
||||
.probe { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; padding: 7px 0; border-top: 1px solid var(--hair) }
|
||||
.probe:last-of-type { border-bottom: 1px solid var(--hair) }
|
||||
.probe .mk { flex: none; width: 14px; font-style: normal; color: var(--err) }
|
||||
.probe .mk { flex: none; width: 16px; font-style: normal; color: var(--err) }
|
||||
.probe[data-ok] .mk { color: var(--ok) }
|
||||
.probe .lb { flex: none; min-width: 68px; font: var(--w-reg) var(--fs-ui) var(--ui); color: var(--muted) }
|
||||
.probe .dt { flex: 1; min-width: 0; font: var(--w-reg) var(--fs-ui) var(--mono); color: var(--text); word-break: break-all }
|
||||
@@ -3575,7 +3575,7 @@
|
||||
.acct-who { display: flex; align-items: baseline; gap: 9px }
|
||||
.acct-who .nm { font: var(--w-med) var(--fs-ui) var(--ui); color: var(--text) }
|
||||
.acct-who .em { font: var(--w-reg) var(--fs-ui) var(--mono); color: var(--muted) }
|
||||
.acct-note { font: var(--w-reg) var(--fs-ui)/1.6 var(--ui); color: var(--muted); margin: 0 }
|
||||
.acct-note { font: var(--w-reg) var(--fs-ui)/1.6 var(--ui); color: var(--muted); margin: 0; overflow-wrap: anywhere }
|
||||
.acct-note[data-err] { color: var(--err-ink) }
|
||||
|
||||
.bk { display: flex; flex-direction: column; gap: 12px }
|
||||
@@ -3821,7 +3821,7 @@
|
||||
}
|
||||
.movemenu button:hover { background: var(--float-hi); color: var(--text) }
|
||||
.movemenu button[aria-checked="true"] { color: var(--text) }
|
||||
.movemenu button[aria-checked="true"]::after { content: " ·" }
|
||||
.movemenu button[aria-checked="true"] { display: flex; align-items: center; justify-content: space-between; gap: 8px }
|
||||
|
||||
.drawnby { margin-top: 6px; font: var(--w-reg) var(--fs-meta) var(--ui); color: var(--ghost) }
|
||||
|
||||
@@ -3894,7 +3894,7 @@
|
||||
.oobe[data-play] .oobe-sub { opacity: 0; animation: m-card .8s var(--ease-out) 2.1s forwards }
|
||||
@keyframes oobebrand { from { opacity: 0; transform: translateY(10px); filter: blur(6px) }
|
||||
to { opacity: 1; transform: none; filter: none } }
|
||||
|
||||
|
||||
.oobe-lines { position: relative; height: 96px; width: 100%; margin-top: 4px }
|
||||
.oobe-line { position: absolute; inset: 0; display: grid; place-items: center; opacity: 0 }
|
||||
.oobe-line b { display: block; font: var(--w-reg) calc(30px * var(--fs-scale, 1))/1.35 var(--ui); color: #fff }
|
||||
@@ -4020,7 +4020,7 @@
|
||||
[data-vt="tab"]::view-transition-new(tabbody) { animation: m-card var(--m-card) both }
|
||||
[data-vt="pane"]::view-transition-old(pane) { animation: m-exit var(--t-card) var(--ease-in) both }
|
||||
[data-vt="pane"]::view-transition-new(pane) { animation: m-card var(--m-panel) both }
|
||||
|
||||
|
||||
.panetabs {
|
||||
position: relative;
|
||||
flex: none; display: flex; align-items: stretch; gap: 2px;
|
||||
@@ -4045,7 +4045,8 @@
|
||||
.ptab-ws, .ptab-host { flex: 0 1 auto; min-width: 0; max-width: 40%; overflow: hidden; text-overflow: ellipsis }
|
||||
.ptab-ws { font: var(--w-reg) var(--fs-micro) var(--mono); color: var(--ghost) }
|
||||
.ptab-host { font: var(--w-reg) var(--fs-micro) var(--mono); color: var(--net) }
|
||||
.ptab-host::before { content: "◦"; margin-right: 2px; opacity: .7 }
|
||||
.ptab-host { display: inline-flex; align-items: center; gap: 3px }
|
||||
.ptab-host .studio-icon { width: 12px; height: 12px; opacity: .7 }
|
||||
.ptab .pip { flex: none; background: var(--ghost) }
|
||||
|
||||
.ptab[data-run="running"] .pip { background: var(--net); animation: m-pulse var(--t-live) ease-in-out infinite }
|
||||
@@ -4208,8 +4209,9 @@
|
||||
.movess-cols h3 { margin: 0 0 4px; font: var(--w-med) var(--fs-meta) var(--ui); color: var(--text) }
|
||||
.movess-cols ul { margin: 0; padding: 0; list-style: none; display: flex; flex-direction: column; gap: 3px }
|
||||
.movess-cols li { position: relative; padding-left: 16px; font: var(--w-reg) var(--fs-meta)/1.6 var(--ui); color: var(--muted) }
|
||||
.movess-cols section[data-part="travels"] li::before { content: "✓"; position: absolute; left: 0; color: var(--ok-ink) }
|
||||
.movess-cols section[data-part="stays"] li::before { content: "×"; position: absolute; left: 2px; color: var(--warn-ink) }
|
||||
.movess-cols li .studio-icon { position: absolute; left: 0; top: 3px; width: 12px; height: 12px }
|
||||
.movess-cols section[data-part="travels"] .studio-icon { color: var(--ok-ink) }
|
||||
.movess-cols section[data-part="stays"] .studio-icon { color: var(--warn-ink) }
|
||||
.movess .picknote { margin: 10px 0 0 }
|
||||
.movess-act { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px }
|
||||
.movess-empty { display: flex; flex-direction: column; align-items: center; gap: 10px; padding: 14px 0 }
|
||||
@@ -4328,6 +4330,7 @@
|
||||
.rmtcands { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin: 10px 0 2px }
|
||||
.rmtcands .cap { font: var(--w-reg) var(--fs-meta) var(--ui); color: var(--ghost) }
|
||||
.rmtcands .rmtcand {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
padding: 2px 9px; border: 1px dashed var(--border); border-radius: var(--r-pill);
|
||||
background: transparent; color: var(--faint); cursor: pointer;
|
||||
font: var(--w-reg) var(--fs-meta) var(--mono);
|
||||
@@ -5184,7 +5187,7 @@
|
||||
.prefs-pagetitle .desc { display: inline-flex; gap: 0; color: var(--faint); font: var(--w-reg) var(--fs-meta)/1.5 var(--ui); }
|
||||
.prefs-pagetitle .desc .what + .count::before { content: "·"; margin: 0 6px; }
|
||||
.prefs-pagetitle .group { color: var(--faint); font: var(--w-reg) var(--fs-meta)/1.4 var(--ui); }
|
||||
.prefs-pagetitle .group::after { content: "›"; margin-left: 8px; }
|
||||
.prefs-pagetitle .group { display: inline-flex; align-items: center; gap: 8px; }
|
||||
.prefs-pagetitle h2 { margin: 0; color: var(--text); font: var(--w-semi) var(--fs-title)/1.3 var(--ui); }
|
||||
.prefs-card { margin-bottom: 16px; border: 1px solid var(--border); border-radius: var(--r-lg); background: var(--surface); overflow: hidden; }
|
||||
.prefs-card > header { padding: 16px 20px 12px; }
|
||||
@@ -5199,7 +5202,7 @@
|
||||
.prefs-card > .grp > .grp-hd, .prefs-card > .grp > .hint, .prefs-card > .grp > .apply { grid-column: 1; }
|
||||
.prefs-card > .grp:has(> .grp-items > :is(.seg, select)) > .grp-items { grid-column: 2; grid-row: 1 / span 3; margin-top: 0; align-items: flex-end; text-align: right; }
|
||||
.prefs-card > .grp:has(> .grp-items > :is(.seg, select)) > .grp-hd .now { display: none; }
|
||||
.prefs-card > .grp .grp-hd h3 { font: var(--w-semi) var(--fs-ui)/1.4 var(--ui); letter-spacing: 0; }
|
||||
.prefs-card > .grp .grp-hd :is(h3, h4) { font: var(--w-semi) var(--fs-ui)/1.4 var(--ui); letter-spacing: 0; }
|
||||
.prefs-card > .grp .hint { margin: 2px 0 0; color: var(--faint); font: var(--w-reg) var(--fs-meta)/1.5 var(--ui); }
|
||||
.prefs-card > .grp .apply { margin: 4px 0 0; padding: 0; border: 0; color: var(--faint); font: var(--w-reg) var(--fs-micro)/1.4 var(--ui); }
|
||||
.prefs-sheet { position: relative; display: grid; grid-template-columns: 240px minmax(0, 1fr); grid-template-rows: auto auto minmax(0, 1fr); }
|
||||
@@ -5208,7 +5211,6 @@
|
||||
.prefs-hd h1 { grid-column: 1; grid-row: 1; padding: 18px 18px 0; background: var(--surface); }
|
||||
.prefs-find { grid-column: 1; grid-row: 2; max-width: none; margin: 0; border-radius: var(--r-md); border-width: 0 0 0 0; box-shadow: inset 0 0 0 1px var(--border); background: var(--raised); }
|
||||
.prefs-sheet > .prefs-hd > .prefs-find { margin: 0; align-self: start; }
|
||||
.prefs-actions { grid-column: 2; grid-row: 1; justify-self: end; display: flex; align-items: center; gap: 8px; padding: 12px 14px; }
|
||||
.prefs-nav { grid-column: 1; grid-row: 3; }
|
||||
.prefs-main { grid-column: 2; grid-row: 2 / span 2; }
|
||||
.prefs-found { top: 96px; left: 12px; right: auto; width: min(460px, calc(100% - 24px)); border-top: 1px solid var(--border); border-radius: var(--r-md); }
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
display: inline-flex; align-items: center; gap: 3px; padding: 0 5px; border: 1px solid var(--border); border-radius: 999px; background: transparent; cursor: pointer;
|
||||
color: var(--muted); font: var(--w-reg) var(--fs-micro)/1.5 var(--mono);
|
||||
}
|
||||
.chrome-context-parts .chrome-context-more::after { content: "›"; transition: transform var(--t-line); }
|
||||
.chrome-context-parts .chrome-context-more .studio-icon { width: 12px; height: 12px; transition: transform var(--t-line); }
|
||||
.chrome-context-parts .chrome-context-more[aria-expanded="true"] { color: var(--text); background: var(--overlay); }
|
||||
.chrome-context-parts .chrome-context-more[aria-expanded="true"]::after { transform: rotate(90deg); }
|
||||
.chrome-context-parts .chrome-context-more[aria-expanded="true"] .studio-icon { transform: rotate(90deg); }
|
||||
.chrome-context-parts .chrome-context-more:focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
|
||||
.chrome-context-items { list-style: none; margin: 6px 0 0; padding: 0 0 0 8px; border-left: 1px solid var(--border); display: grid; grid-template-columns: minmax(0, 1fr); gap: 3px; }
|
||||
.chrome-context-items li { min-width: 0; display: flex; align-items: center; gap: 8px; color: var(--muted); font: var(--w-reg) var(--fs-micro)/1.4 var(--ui); }
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
:root:not([data-fold~="scene"]) :is(.prefs, .fbk-veil, .image-preview, .cpv, .agent-tx-scrim) {
|
||||
:root:not([data-fold~="scene"]) :is(.fbk-veil, .image-preview, .cpv, .agent-tx-scrim) {
|
||||
border-bottom: var(--chrome-h, 39px) solid transparent;
|
||||
}
|
||||
@@ -4,5 +4,6 @@ const CSS = import.meta.glob("./dialogcenter.css", { query: "?raw", import: "def
|
||||
const css = CSS["./dialogcenter.css"]!;
|
||||
|
||||
it("keeps the transparent bottom border that centres a dialog under the title bar", () => {
|
||||
expect(css).toMatch(/:is\(\.prefs, \.fbk-veil, \.image-preview, \.cpv, \.agent-tx-scrim\)\s*\{\s*border-bottom:\s*var\(--chrome-h, 39px\) solid transparent;/);
|
||||
expect(css).not.toContain(".prefs");
|
||||
expect(css).toMatch(/:is\(\.fbk-veil, \.image-preview, \.cpv, \.agent-tx-scrim\)\s*\{\s*border-bottom:\s*var\(--chrome-h, 39px\) solid transparent;/);
|
||||
});
|
||||
@@ -66,6 +66,7 @@ select[data-size="sm"] { height: 24px; padding: 0 26px 0 8px; font-size: var(--f
|
||||
|
||||
@supports (appearance: base-select) {
|
||||
select:not([multiple]), select:not([multiple])::picker(select) { appearance: base-select; }
|
||||
select:not([multiple]) { align-items: center; }
|
||||
select:not([multiple])::picker-icon { display: none; }
|
||||
select:not([multiple])::picker(select) {
|
||||
box-sizing: border-box; margin-block: 4px; padding: 4px; max-height: min(320px, 50vh);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
.modlist li { display: grid; gap: 2px; padding: 8px 10px; border: 1px solid var(--hair); border-radius: var(--r-sm); background: var(--surface) }
|
||||
.modlist .modhead { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 4px 12px }
|
||||
.modlist b { font-weight: var(--w-med); overflow-wrap: anywhere }
|
||||
.modlist .modstate { font: var(--w-reg) var(--fs-meta) var(--ui); color: var(--muted) }
|
||||
.modlist .modstate { display: inline-flex; align-items: center; gap: 4px; font: var(--w-reg) var(--fs-meta) var(--ui); color: var(--muted) }
|
||||
.modlist li[data-state="running"] .modstate { color: var(--ok, var(--text)) }
|
||||
.modlist li[data-state="disabled"] .modstate { color: var(--danger, var(--text)) }
|
||||
.modlist li[data-state="restarting"] .modstate { color: var(--warn, var(--text)) }
|
||||
|
||||
@@ -20,11 +20,6 @@ const SOURCES = import.meta.glob(["../**/*.tsx", "!../**/*.test.*"], {
|
||||
// welcome screen is the one deliberate exception — its STUDIO line and tagline
|
||||
// are set in the terminal's own face because that is the product's vernacular.
|
||||
const MARKS: Record<string, string> = {
|
||||
x: "× — the close mark, sized to hold its cell beside ✓",
|
||||
"ptab-x": "× — same mark on a pane tab",
|
||||
ah: "› — the affordance arrow",
|
||||
"mode-toggle": "Aa and .* — the glyphs of the search flags they switch, which are the flags' own notation",
|
||||
prompt: "› — the composer's own prompt character",
|
||||
esc: "Esc — a keycap, which is lettering on a key rather than prose",
|
||||
"oobe-brand": "the lockup's STUDIO line, deliberately set in the terminal's face",
|
||||
"oobe-sub": "the lockup's tagline, set with it",
|
||||
@@ -37,8 +32,8 @@ const MARKS: Record<string, string> = {
|
||||
// while the work is outstanding. Never widen this to land a change.
|
||||
const MIXED = new Set([
|
||||
"c", "cnt", "cost", "cur", "d", "delta", "dt", "fail", "k",
|
||||
"lb", "meta", "mk", "n", "nm", "ntimes", "path", "pill", "pt", "r", "rmtcand", "rt",
|
||||
"sc", "st", "sym", "sz", "tag", "u", "uspan", "v",
|
||||
"lb", "meta", "n", "nm", "ntimes", "path", "pill", "pt", "r", "rmtcand", "rt",
|
||||
"sc", "st", "sz", "tag", "u", "uspan", "v",
|
||||
]);
|
||||
|
||||
const TRANSLATORS = ["t", "tx", "plural"];
|
||||
|
||||
@@ -93,8 +93,6 @@
|
||||
.chrome-context-foot .compact-now > .btn { height: 24px; padding: 0 10px; border: 1px solid transparent; border-radius: var(--r-pill); background: transparent; color: var(--muted); font: var(--w-reg) var(--fs-meta)/1 var(--ui); }
|
||||
.chrome-context-foot .compact-now > .btn:hover { background: var(--overlay); color: var(--text); }
|
||||
.chrome-context-card .chrome-context-more { display: inline-flex; align-items: center; gap: 5px; height: auto; padding: 0; border: 0; background: none; cursor: pointer; }
|
||||
.chrome-context-card .chrome-context-more::after { content: ""; width: 5px; height: 5px; border-top: 1.45px solid var(--faint); border-right: 1.45px solid var(--faint); transform: rotate(45deg); }
|
||||
.chrome-context-card .chrome-context-more[aria-expanded="true"]::after { transform: rotate(135deg); }
|
||||
.chrome-context-card:not(.studio-overview-card) > .chrome-context-items { margin: 2px 14px 4px; padding: 2px 0 2px 10px; gap: 2px; }
|
||||
@media (pointer: coarse) {
|
||||
.chrome-context-card:not(.studio-overview-card) > .chrome-context-parts > div { height: 36px; }
|
||||
|
||||
@@ -51,4 +51,8 @@ describe("a select's popup list", () => {
|
||||
it("gives every option a finger-sized row on a touch screen", () => {
|
||||
expect(block()).toMatch(/@media \(pointer: coarse\)\s*\{[^}]*select:not\(\[multiple\]\) option\s*\{[^}]*min-height:\s*40px/);
|
||||
});
|
||||
|
||||
it("keeps the chosen text centred in the box, which the engine's own layout leaves at the top", () => {
|
||||
expect(rule(block(), "select:not([multiple])")).toMatch(/align-items:\s*center/);
|
||||
});
|
||||
});
|
||||
@@ -108,8 +108,8 @@ body { background: var(--page); }
|
||||
.navgrp { gap: 3px; }
|
||||
.navbtn { width: 36px; height: 36px; border-radius: var(--r-sm); }
|
||||
.navbtn:hover { background: var(--overlay); box-shadow: none; }
|
||||
.navbtn[aria-current] { color: var(--text); background: var(--overlay); }
|
||||
.navbtn[aria-current]::before { left: -5px; top: 10px; bottom: 10px; background: var(--text); }
|
||||
.navbtn[aria-current] { color: var(--accent-ink); background: var(--accent-wash); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 18%, var(--border)); }
|
||||
.navbtn[aria-current]::before { left: -5px; top: 10px; bottom: 10px; background: var(--accent-ink); }
|
||||
.navhint { left: calc(100% + 8px); }
|
||||
.rail {
|
||||
background: color-mix(in srgb, var(--page) 94%, var(--surface));
|
||||
@@ -453,13 +453,13 @@ body { background: var(--page) !important; }
|
||||
|
||||
/* Prototype sidebar */
|
||||
.studio-icon {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
flex: 0 0 auto;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.5;
|
||||
stroke-width: 1.8;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
@@ -596,13 +596,9 @@ body { background: var(--page) !important; }
|
||||
}
|
||||
.flow .call[data-k="say"] > .g .sym {
|
||||
width: 21px; height: 21px; margin-top: 0; border-radius: var(--r-sm);
|
||||
background: var(--accent); color: transparent; box-shadow: none; font-size: 0;
|
||||
background: var(--accent); color: var(--accent-fg); box-shadow: none;
|
||||
}
|
||||
.flow .call[data-k="say"] > .g .sym::after {
|
||||
content: "R"; width: 100%; height: 100%; display: grid; place-items: center;
|
||||
color: var(--accent-fg); font: var(--w-med) var(--fs-micro)/1 var(--ui);
|
||||
}
|
||||
.flow .call[data-k="say"] > .g .sym svg { display: none; }
|
||||
.flow .call[data-k="say"] > .g .sym svg { width: 15px; height: 15px; display: block; }
|
||||
.flow .call[data-k="say"][data-quiet] > .g,
|
||||
.flow .call[data-k="say"][data-quiet] > .c > .hl { display: none; }
|
||||
.flow .call[data-k="say"] > .c { padding: 0 0 var(--y-say); }
|
||||
@@ -1117,7 +1113,7 @@ body { background: var(--page) !important; }
|
||||
border: 0; border-radius: var(--r-sm); background: transparent; color: var(--faint); cursor: pointer;
|
||||
}
|
||||
.studio-browser-toolbar > button:hover:not(:disabled) { color: var(--text); background: var(--float); }
|
||||
.studio-browser-toolbar svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.35; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.studio-browser-toolbar svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
|
||||
.studio-browser-toolbar {
|
||||
min-height: 46px; flex: none; display: flex; align-items: center; gap: 4px; padding: 8px 9px;
|
||||
border-bottom: 1px solid var(--hair); background: var(--surface);
|
||||
@@ -1133,6 +1129,12 @@ body { background: var(--page) !important; }
|
||||
.studio-browser-address input { width: 100%; min-width: 0; padding: 0; border: 0; outline: 0; background: transparent; color: var(--text); font: var(--w-reg) var(--fs-micro)/1 var(--ui); }
|
||||
.studio-browser-toolbar .studio-browser-go { width: auto; padding: 0 8px; color: #adb9d8; font: var(--w-med) var(--fs-micro)/1 var(--ui); }
|
||||
.studio-browser-frame { width: 100%; min-height: 0; flex: 1; border: 0; background: var(--page); }
|
||||
.studio-browser-start { display: grid; place-items: center; padding: 32px; color: var(--text); text-align: center; }
|
||||
.studio-browser-start > div { width: min(520px, 100%); }
|
||||
.studio-browser-start .browser-start-mark { box-sizing: content-box; padding: 10px; margin: 0 auto 18px; border: 1px solid var(--border); border-radius: var(--r-md); background: var(--surface); color: var(--muted); }
|
||||
.studio-browser-start h1 { margin: 0 0 8px; font-size: var(--fs-title); }
|
||||
.studio-browser-start p { margin: 0; color: var(--faint); font-size: var(--fs-ui); line-height: 1.7; }
|
||||
.studio-browser-start .browser-start-hint { margin-top: 22px; padding: 11px 14px; border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--surface); text-align: left; }
|
||||
.studio-browser-note,
|
||||
.studio-browser-error { flex: none; margin: 0; padding: 7px 12px; border-top: 1px solid var(--hair); background: var(--surface); color: var(--ghost); font: var(--w-reg) var(--fs-micro)/1.45 var(--ui); }
|
||||
.studio-browser-note { display: flex; align-items: center; gap: 10px; }
|
||||
@@ -1301,7 +1303,7 @@ body { background: var(--page) !important; }
|
||||
:root[data-theme="light"] .pane,
|
||||
:root[data-theme="light"] .pane > .scroll { background: var(--reading-surface, var(--page)) !important; }
|
||||
:root[data-theme="light"] .flow .call[data-k="say"] > .g .sym { background: var(--accent); }
|
||||
:root[data-theme="light"] .flow .call[data-k="say"] > .g .sym::after { color: var(--accent-fg); }
|
||||
:root[data-theme="light"] .flow .call[data-k="say"] > .g .sym { color: var(--accent-fg); }
|
||||
:root[data-theme="light"] .flow .call[data-k="say"] > .c > .hl .nm { color: var(--muted); }
|
||||
:root[data-theme="light"] .flow .call[data-k="say"] > .c > .out > .txt { color: var(--text); }
|
||||
:root[data-theme="light"] .flow .call[data-k="say"] .copy[data-icon]:hover { color: var(--text); background: var(--overlay); border-color: var(--border); }
|
||||
@@ -2024,7 +2026,7 @@ button, input, textarea { font-size: var(--fs-ui); }
|
||||
.portmenu .mi .dot { display: none; }
|
||||
.portmenu .mi[data-on] { background: var(--hair); }
|
||||
.portmenu .mi[data-on]::before { display: none; }
|
||||
.portmenu .mi[data-on]::after { content: "✓"; align-self: center; margin-left: 8px; color: var(--text); font: var(--w-med) var(--fs-micro)/1 var(--ui); }
|
||||
.portmenu .menu-current { align-self: center; margin-left: 8px; color: var(--text); }
|
||||
.portmenu .mi.head { gap: 8px; min-height: 34px; align-items: center; padding-bottom: 8px; margin-bottom: 3px; border-bottom: 1px solid var(--border); border-radius: 0; }
|
||||
.portmenu .mi.head .lb { color: var(--text); font-size: var(--fs-micro); }
|
||||
.portmenu .mi.head .rt { color: var(--ghost); font: var(--w-reg) var(--fs-micro)/1 var(--ui); }
|
||||
@@ -2074,14 +2076,9 @@ button, input, textarea { font-size: var(--fs-ui); }
|
||||
.studio-effort-menu .studio-effort-strength i:nth-child(3) { height: 10px; }
|
||||
.studio-effort-menu .studio-effort-strength i:nth-child(4) { height: 13px; }
|
||||
.studio-effort-menu .studio-effort-strength i[data-on] { background: #9a9f94; }
|
||||
.studio-effort-menu .mi[data-recommended] .studio-effort-strength::before {
|
||||
content: "✦";
|
||||
color: var(--faint);
|
||||
font: var(--w-reg) var(--fs-micro)/1 var(--ui);
|
||||
}
|
||||
.studio-effort-menu .mi[data-recommended] .studio-effort-strength i { display: none; }
|
||||
.studio-effort-menu .studio-effort-strength .studio-icon { color: var(--faint); }
|
||||
.studio-effort-menu .mi[data-strength="0"] .studio-effort-strength { visibility: hidden; }
|
||||
.studio-effort-menu .mi[data-on]::after { margin-left: 0; }
|
||||
.studio-effort-menu .menu-current { margin-left: 0; }
|
||||
|
||||
.polmenu {
|
||||
width: 310px;
|
||||
@@ -2534,7 +2531,7 @@ button, input, textarea { font-size: var(--fs-ui); }
|
||||
.flow .ask-hint,
|
||||
.flow .opt .ds { color: var(--muted); }
|
||||
.flow .call[data-k="say"] > .g .sym { background: var(--text); }
|
||||
.flow .call[data-k="say"] > .g .sym::after { color: var(--page); }
|
||||
.flow .call[data-k="say"] > .g .sym { color: var(--page); }
|
||||
|
||||
.flow .ask { background: var(--raised) !important; border-color: var(--border) !important; }
|
||||
.tool-disclosure > summary.hl:hover,
|
||||
@@ -3542,8 +3539,6 @@ button, input, textarea { font-size: var(--fs-ui); }
|
||||
|
||||
:root[data-pack] .chrome-context-card section > button { color: var(--link); }
|
||||
:root[data-pack] .studio-brand-accent { color: var(--brand); }
|
||||
:root[data-pack] .navbtn[aria-current],
|
||||
:root[data-pack] .navbtn[aria-current]::before { background: var(--accent); }
|
||||
:root[data-pack] .btn.send:not(:disabled) { background: var(--accent); color: var(--accent-fg); }
|
||||
:root[data-pack] .studio-effort-menu .studio-effort-strength i[data-on] { background: var(--accent); }
|
||||
:root[data-pack] .compose:hover::after,
|
||||
|
||||
@@ -1,23 +1,20 @@
|
||||
// @vitest-environment jsdom
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import "./testkit";
|
||||
import { MockPort } from "../port/mock";
|
||||
import type { AccountState, AgentPort } from "../port/port";
|
||||
import { AccountSettings } from "./AccountSettings";
|
||||
import { SettingDeepContext } from "./SettingDeep";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
function draw(state: AccountState | null) {
|
||||
const go = vi.fn();
|
||||
const port = new MockPort() as unknown as AgentPort;
|
||||
render(
|
||||
<SettingDeepContext.Provider value={{ pages: true, open: null, section: "账号与关于", go }}>
|
||||
<>
|
||||
<AccountSettings port={port} homePort={port} state={state} unread="" reload={() => {}} />
|
||||
</SettingDeepContext.Provider>,
|
||||
</>,
|
||||
);
|
||||
return go;
|
||||
}
|
||||
|
||||
it("is account, version and diagnostics, and about: one row per setting", () => {
|
||||
@@ -27,16 +24,16 @@ it("is account, version and diagnostics, and about: one row per setting", () =>
|
||||
});
|
||||
|
||||
it("offers sign-in when signed out and the account when signed in", () => {
|
||||
const go = draw({ signedIn: false });
|
||||
fireEvent.click(screen.getByRole("button", { name: "登录" }));
|
||||
expect(go).toHaveBeenLastCalledWith("account");
|
||||
draw({ signedIn: false });
|
||||
expect(document.querySelector('[data-action="account.sign-in"]')).toBeTruthy();
|
||||
cleanup();
|
||||
draw({ signedIn: true, user: { handle: "demo", email: "[email protected]", label: "demo" } });
|
||||
expect(screen.getByRole("button", { name: "账号信息" })).toBeTruthy();
|
||||
expect(screen.getByText("[email protected]")).toBeTruthy();
|
||||
expect(document.querySelector('[data-action="account.sign-out"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it("opens versions and logs from their pills", () => {
|
||||
const go = draw({ signedIn: false });
|
||||
fireEvent.click(screen.getByRole("button", { name: "版本与更新" }));
|
||||
expect(go).toHaveBeenLastCalledWith("versions");
|
||||
it("shows versions and diagnostics inline", async () => {
|
||||
draw({ signedIn: false });
|
||||
await waitFor(() => expect(document.querySelector("#set-versions .vnow")).toBeTruthy());
|
||||
expect(document.querySelector('#set-logs .grp-items')).toBeTruthy();
|
||||
});
|
||||
@@ -22,15 +22,15 @@ export function AccountSettings({ port, homePort, state, unread, reload }: Props
|
||||
return (
|
||||
<>
|
||||
<SectionCard title={t("settings.account")}>
|
||||
<Group id="account" pill={t(state?.signedIn ? "settings.account_info" : "settings.sign_in")} title={t("settings.account")} hint={t("settings.account_hint")}>
|
||||
<Group id="account" title={t("settings.account")} hint={t("settings.account_hint")}>
|
||||
<Account port={port} state={state} unread={unread} reload={reload} />
|
||||
</Group>
|
||||
</SectionCard>
|
||||
<SectionCard title={t("settings.version_and_diagnostics")}>
|
||||
<Group id="versions" pill={t("settings.versions_and_updates")} title={t("common.version")} hint={t("settings.versions_hint")}>
|
||||
<Group id="versions" title={t("common.version")} hint={t("settings.versions_hint")}>
|
||||
<Versions port={homePort} />
|
||||
</Group>
|
||||
<Group id="logs" pill={t("safety.details")} title={t("about.logs_and_abnormal_exit_records")} hint={t("settings.logs_hint")}>
|
||||
<Group id="logs" title={t("about.logs_and_abnormal_exit_records")} hint={t("settings.logs_hint")}>
|
||||
<Logs port={homePort} />
|
||||
</Group>
|
||||
<TraceExport port={port} />
|
||||
@@ -38,10 +38,10 @@ export function AccountSettings({ port, homePort, state, unread, reload }: Props
|
||||
<SectionCard title={t("settings.about")}>
|
||||
<p className="srow-desc">{t("settings.about_note")}</p>
|
||||
<AuthorLine port={homePort} />
|
||||
<Group id="community" pill={t("settings.look_at")} title={t("common.community_and_contributors")} hint={t("about.join_the_community_for_help")}>
|
||||
<Group id="community" title={t("common.community_and_contributors")} hint={t("about.join_the_community_for_help")}>
|
||||
<Community port={homePort} />
|
||||
</Group>
|
||||
<Group id="sponsor" pill={t("settings.look_at")} title={t("about.support_the_project")}>
|
||||
<Group id="sponsor" title={t("about.support_the_project")}>
|
||||
<Sponsor port={homePort} />
|
||||
</Group>
|
||||
</SectionCard>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { StudioIcon } from "./StudioIcon";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { t } from "../i18n";
|
||||
import type { Protocol, ProviderEntry, ProviderPreset, ProviderProbe } from "../port/port";
|
||||
@@ -200,7 +201,7 @@ export function AddProvider({
|
||||
<strong>{t("models.add_a_model_source")}</strong>
|
||||
<p>{t("models.use_the_service_documentation_connection")}</p>
|
||||
</div>
|
||||
<button className="addp-close" onClick={onCancel} disabled={busy || testing} aria-label={t("common.cancel")}>×</button>
|
||||
<button className="addp-close" onClick={onCancel} disabled={busy || testing} aria-label={t("common.cancel")}><StudioIcon name="close" /></button>
|
||||
</div>
|
||||
{baseUrl.trim() === "" && <PresetGallery load={loadPresets} onPick={setPreset} disabled={busy || checkingModel !== ""} />}
|
||||
<div className="fields">
|
||||
|
||||
@@ -5,16 +5,15 @@ import "./testkit";
|
||||
import { MockPort } from "../port/mock";
|
||||
import type { AgentPort } from "../port/port";
|
||||
import { AgentSettings } from "./AgentSettings";
|
||||
import { SettingDeepContext } from "./SettingDeep";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
function draw() {
|
||||
const go = vi.fn();
|
||||
render(
|
||||
<SettingDeepContext.Provider value={{ pages: true, open: null, section: "Agent 行为", go }}>
|
||||
<AgentSettings port={new MockPort() as unknown as AgentPort} hub={{ tree: async () => [] }} persona={false} onChanged={() => {}} />
|
||||
</SettingDeepContext.Provider>,
|
||||
<>
|
||||
<AgentSettings port={new MockPort() as unknown as AgentPort} hub={{ tree: async () => [] }} persona={false} onPersona={go} onChanged={() => {}} />
|
||||
</>,
|
||||
);
|
||||
return go;
|
||||
}
|
||||
@@ -28,23 +27,20 @@ it("is four cards of single rows, plan mode and the display switches living else
|
||||
for (const id of ["plan-mode", "changed-files", "fold-work", "feedback-level-notices"]) expect(document.querySelector(`[data-setting="${id}"]`), id).toBeNull();
|
||||
});
|
||||
|
||||
it("opens the presets page from both the persona row and the presets row", () => {
|
||||
it("opens the persona editor while keeping presets inline", async () => {
|
||||
const go = draw();
|
||||
fireEvent.click(screen.getByRole("button", { name: "编辑人设" }));
|
||||
expect(go).toHaveBeenLastCalledWith("agent-presets");
|
||||
fireEvent.click(screen.getByRole("button", { name: "管理预设" }));
|
||||
expect(go).toHaveBeenLastCalledWith("agent-presets");
|
||||
expect(go).toHaveBeenCalledExactlyOnceWith();
|
||||
await waitFor(() => expect(document.querySelector('[data-action="agent-preset.reload"]')).toBeTruthy());
|
||||
});
|
||||
|
||||
it("opens the hooks and the scheduled-runs pages from the automation card", async () => {
|
||||
const go = draw();
|
||||
const card = [...document.querySelectorAll<HTMLElement>(".prefs-card")].find((c) => c.querySelector("header h3")?.textContent === "自动化")!;
|
||||
await waitFor(() => expect([...card.querySelectorAll("button")].filter((b) => b.textContent === "管理")).toHaveLength(2));
|
||||
const buttons = [...card.querySelectorAll("button")].filter((b) => b.textContent === "管理");
|
||||
fireEvent.click(buttons[0]);
|
||||
expect(go).toHaveBeenLastCalledWith("hooks");
|
||||
fireEvent.click(buttons[1]);
|
||||
expect(go).toHaveBeenLastCalledWith("scheduled-runs");
|
||||
expect(card.textContent).toContain("在工具调用前后运行你自己的命令");
|
||||
expect(card.textContent).toContain("到点让 Agent 只读地跑一遍并留下报告");
|
||||
it("exposes hooks and scheduled runs directly in the automation card", async () => {
|
||||
draw();
|
||||
await waitFor(() => {
|
||||
for (const id of ["hooks", "scheduled-runs"]) {
|
||||
const region = document.querySelector(`#set-${id}`)!;
|
||||
expect(region.querySelector(".grp-items")).toBeTruthy();
|
||||
expect(region.querySelector("button")).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { t } from "../i18n";
|
||||
import type { HubPort } from "../port/hub";
|
||||
import type { AgentPort } from "../port/port";
|
||||
@@ -13,7 +13,6 @@ import { PlanApproval } from "./PlanApproval";
|
||||
import { ProgressWatch } from "./ProgressWatch";
|
||||
import { SectionCard } from "./SectionCard";
|
||||
import { SessionRef } from "./SessionRef";
|
||||
import { SettingDeep, SettingDeepContext } from "./SettingDeep";
|
||||
import { SubagentClaims } from "./SubagentClaims";
|
||||
import { TurnNarrationSetting } from "./TurnNarrationSetting";
|
||||
|
||||
@@ -21,6 +20,7 @@ interface Props {
|
||||
port: AgentPort;
|
||||
hub: Pick<HubPort, "tree">;
|
||||
persona: boolean;
|
||||
onPersona: () => void;
|
||||
onChanged: () => void;
|
||||
}
|
||||
|
||||
@@ -37,8 +37,7 @@ function useAutomationCounts(port: AgentPort) {
|
||||
return counts;
|
||||
}
|
||||
|
||||
export function AgentSettings({ port, hub, persona, onChanged }: Props) {
|
||||
const deep = useContext(SettingDeepContext);
|
||||
export function AgentSettings({ port, hub, persona, onPersona, onChanged }: Props) {
|
||||
const counts = useAutomationCounts(port);
|
||||
const hooksHint = t("settings.hooks_hint") + (counts.hooks ? ` · ${t("settings.n_hooks", { n: counts.hooks })}` : "");
|
||||
const runsHint = t("settings.scheduled_runs_hint") + (counts.schedules
|
||||
@@ -47,19 +46,17 @@ export function AgentSettings({ port, hub, persona, onChanged }: Props) {
|
||||
return (
|
||||
<>
|
||||
<SectionCard title={t("settings.persona_and_presets")}>
|
||||
<div className="srow" data-setting="persona">
|
||||
<div className="srow" id="set-persona" data-setting="persona">
|
||||
<div className="srow-meta">
|
||||
<span className="srow-name">{t("persona.system_persona")}</span>
|
||||
<span className="srow-desc">{t("settings.persona_hint")}</span>
|
||||
<ApplyNote id="persona" />
|
||||
</div>
|
||||
<div className="srow-ctl">
|
||||
<button type="button" className="spill" data-action="settings.open-page" data-target="agent-presets" onClick={() => deep.go("agent-presets")}>{t("settings.edit_persona")}</button>
|
||||
<button type="button" className="spill" data-action="settings.edit-persona" onClick={() => onPersona()}>{t("settings.edit_persona")}</button>
|
||||
</div>
|
||||
</div>
|
||||
<SettingDeep id="agent-presets" title={t("persona.agent_presets")} hint={t("settings.presets_hint")} act={t("settings.manage_presets")}>
|
||||
<AgentPresets port={port} focusPersona={persona} />
|
||||
</SettingDeep>
|
||||
<AgentPresets port={port} focusPersona={persona} />
|
||||
</SectionCard>
|
||||
<SectionCard title={t("settings.plans_and_questions")}>
|
||||
<PlanApproval port={port} />
|
||||
@@ -73,10 +70,10 @@ export function AgentSettings({ port, hub, persona, onChanged }: Props) {
|
||||
<PeersSection port={port} hub={hub} />
|
||||
</SectionCard>
|
||||
<SectionCard title={t("settings.automation")}>
|
||||
<Group id="hooks" title={t("settings.hooks")} hint={hooksHint} pill={t("settings.manage_storage")}>
|
||||
<Group id="hooks" title={t("settings.hooks")} hint={hooksHint}>
|
||||
<Hooks port={port} onChanged={onChanged} />
|
||||
</Group>
|
||||
<ScheduledRuns port={port} hint={runsHint} pill={t("settings.manage_storage")} />
|
||||
<ScheduledRuns port={port} hint={runsHint} />
|
||||
</SectionCard>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { StudioIcon } from "./StudioIcon";
|
||||
import { useBranchSession } from "./useBranchSession";
|
||||
import { CompositionNewContext } from "./CompositionNewContext";
|
||||
import { keepSame } from "./stable";
|
||||
@@ -722,9 +723,7 @@ export function App({ hub }: { hub: HubPort }) {
|
||||
})}
|
||||
{runtimes.length === 0 && (
|
||||
<div className="panes-empty">
|
||||
<span className="mk" aria-hidden="true">
|
||||
⌘
|
||||
</span>
|
||||
<span className="mk" aria-hidden="true"><StudioIcon name="chat" /></span>
|
||||
<p className="t">{t("app.no_session_open")}</p>
|
||||
{!treeRead ? null : tree.length === 0 ? (
|
||||
<>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { StudioIcon } from "./StudioIcon";
|
||||
import { ApplyNote } from "./Group";
|
||||
import { SettingDeep, SettingDeepContext } from "./SettingDeep";
|
||||
import { useCallback, useContext, useMemo, useRef, useState, useSyncExternalStore, type CSSProperties } from "react";
|
||||
import { useCallback, useMemo, useRef, useState, useSyncExternalStore, type CSSProperties } from "react";
|
||||
import { effectsMode, onEffectsChange, setEffectsMode } from "../state/prefs";
|
||||
import type { AgentPort, Appearance as Look, ThemePack } from "../port/port";
|
||||
import { SIZE_STEPS, ZOOM_PRESETS } from "./zoom";
|
||||
import { ZOOM_PRESETS } from "./zoom";
|
||||
import { chord } from "./keys";
|
||||
import { host } from "../port/host";
|
||||
import { MONO_FAMILIES, TEXT_STEPS, UI_FAMILIES, installed, readSizeOf, readSteps } from "./look";
|
||||
@@ -14,7 +14,7 @@ import { useCrop } from "./useCrop";
|
||||
import { WindowSection } from "./WindowSection";
|
||||
import { GraphicsSection } from "./GraphicsSection";
|
||||
import { SectionCard } from "./SectionCard";
|
||||
import { PillButton, SegChoice, SettingRow } from "./SettingRow";
|
||||
import { SegChoice, SettingRow } from "./SettingRow";
|
||||
import { Switch } from "./Switch";
|
||||
import { ThemeModeRow } from "./ThemeModeRow";
|
||||
import { ThemeImport } from "./ThemeImport";
|
||||
@@ -80,7 +80,6 @@ export const WALLPAPER_ACCEPT = "image/png,image/jpeg,image/webp,image/avif,imag
|
||||
export function Appearance({ port, theme, onTheme, contrast, onContrast, weight, onWeight, reloadThemes, look, onLook }: Props) {
|
||||
const { packs, unread, load } = useThemeInventory(port);
|
||||
const effects = useSyncExternalStore(onEffectsChange, effectsMode);
|
||||
const deep = useContext(SettingDeepContext);
|
||||
|
||||
// Activating repaints through App's own theme effect, so this only refreshes
|
||||
// the list and asks App to re-read which pack is active.
|
||||
@@ -178,7 +177,6 @@ export function Appearance({ port, theme, onTheme, contrast, onContrast, weight,
|
||||
const uiFonts = useMemo(() => installed(UI_FAMILIES), []);
|
||||
const monoFonts = useMemo(() => installed(MONO_FAMILIES), []);
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{(failed || unread) && (
|
||||
@@ -188,11 +186,10 @@ export function Appearance({ port, theme, onTheme, contrast, onContrast, weight,
|
||||
)}
|
||||
<SectionCard title={t("settings.theme_card")}>
|
||||
<ThemeModeRow theme={theme} onTheme={onTheme} />
|
||||
<SettingDeep id="scheme" title={t("settings.palette")} hint={t("settings.accent_and_overall_tone")}
|
||||
act={packs.find((p) => p.active)?.name ?? t("common.default")}>
|
||||
|
||||
<section className="grp" id="set-scheme" data-setting="scheme">
|
||||
<div className="grp-hd">
|
||||
<h3>{t("look.interface_theme")}</h3>
|
||||
<h4>{t("look.interface_theme")}</h4>
|
||||
<span className="now">{t("look.in_effect_now")}</span>
|
||||
</div>
|
||||
<p className="hint">{t("look.choose_the_workbench_surfaces_and")}</p>
|
||||
@@ -202,7 +199,7 @@ export function Appearance({ port, theme, onTheme, contrast, onContrast, weight,
|
||||
{featured.map(cell)}
|
||||
</div>
|
||||
{illustrated.length > 0 && (
|
||||
<details className="theme-more">
|
||||
<details className="theme-more" open>
|
||||
<summary>
|
||||
<span>
|
||||
<b>{t("look.more_installed_themes")}</b>
|
||||
@@ -232,12 +229,11 @@ export function Appearance({ port, theme, onTheme, contrast, onContrast, weight,
|
||||
</div>
|
||||
<ApplyNote id="scheme" />
|
||||
</section>
|
||||
</SettingDeep>
|
||||
|
||||
<SettingDeep id="wallpaper" title={t("look.wallpaper")} hint={t("settings.chat_background_image_or_video")} act={t("settings.choose_wallpaper")}>
|
||||
|
||||
<section className="grp" id="set-wallpaper" data-setting="wallpaper">
|
||||
<div className="grp-hd">
|
||||
<h3>{t("look.wallpaper")}</h3>
|
||||
<h4>{t("look.wallpaper")}</h4>
|
||||
{look.wallpaper && (
|
||||
<button className="now nowbtn" data-action="wallpaper.remove" onClick={dropPaper}>
|
||||
{t("common.remove")}
|
||||
@@ -259,9 +255,7 @@ export function Appearance({ port, theme, onTheme, contrast, onContrast, weight,
|
||||
/>
|
||||
<button className="paperpick" data-busy={busy ? "" : undefined} onClick={() => file.current?.click()}>
|
||||
<span className="plus" aria-hidden="true">
|
||||
<svg viewBox="0 0 16 16">
|
||||
<path d="M8 3.7v8.6M3.7 8h8.6" />
|
||||
</svg>
|
||||
<StudioIcon name="plus" />
|
||||
</span>
|
||||
{t(look.wallpaper ? "look.choose_another" : "look.choose_an_image_or_video")}
|
||||
</button>
|
||||
@@ -382,22 +376,16 @@ export function Appearance({ port, theme, onTheme, contrast, onContrast, weight,
|
||||
|
||||
|
||||
|
||||
</SettingDeep>
|
||||
<SettingRow id="effects" title={t("look.visual_effects")} desc={t("settings.frosted_glass_glow_effects")}>
|
||||
<Switch on={effects === "full"} label={t("look.visual_effects")} data-action="appearance.effects" data-value={effects}
|
||||
onClick={() => setEffectsMode(effects === "full" ? "reduced" : "full")} />
|
||||
</SettingRow>
|
||||
{deep.pages && (
|
||||
<SettingRow id="advanced-look" title={t("look.advanced_look")} desc={t("look.import_themes_custom_colours")}>
|
||||
<PillButton action="settings.open-page" target="scheme" onClick={() => deep.go("scheme")}>{t("look.advanced_look")}</PillButton>
|
||||
</SettingRow>
|
||||
)}
|
||||
</SectionCard>
|
||||
<SectionCard title={t("settings.text_card")}>
|
||||
<SettingDeep id="font" title={t("look.typeface")} hint={t("settings.interface_and_code_typeface")} act={t("settings.change")}>
|
||||
|
||||
<section className="grp" id="set-font" data-setting="font">
|
||||
<div className="grp-hd">
|
||||
<h3>{t("look.typeface")}</h3>
|
||||
<h4>{t("look.typeface")}</h4>
|
||||
</div>
|
||||
<p className="hint">{t("look.enter_a_typeface_name_or")}</p>
|
||||
<div className="grp-items">
|
||||
@@ -420,11 +408,10 @@ export function Appearance({ port, theme, onTheme, contrast, onContrast, weight,
|
||||
</div>
|
||||
<ApplyNote id="font" />
|
||||
</section>
|
||||
</SettingDeep>
|
||||
<SettingDeep id="size" title={t("look.size")} hint={t("settings.overall_text_size")} act={t("settings.adjust")} ctl={<SegChoice<string> label={t("look.interface_size")} value={String(look.zoom || 1)} action="appearance.zoom" onPick={(v) => set({ zoom: Number(v) })} options={SIZE_STEPS.map(([v, name]) => [String(v), t(name)] as const)} />}>
|
||||
|
||||
<section className="grp" id="set-size" data-setting="size">
|
||||
<div className="grp-hd">
|
||||
<h3>{t("look.size")}</h3>
|
||||
<h4>{t("look.size")}</h4>
|
||||
</div>
|
||||
<p className="hint">{t("look.interface_scales_spacing_and_controls")}</p>
|
||||
{host().inShell() && <p className="hint">{t("look.keyboard_up_larger_down_smaller", { up: chord("+"), down: chord("-"), reset: chord("0") })}</p>}
|
||||
@@ -508,7 +495,6 @@ export function Appearance({ port, theme, onTheme, contrast, onContrast, weight,
|
||||
<ApplyNote id="size" />
|
||||
</section>
|
||||
|
||||
</SettingDeep>
|
||||
<SettingRow id="weight" title={t("look.text_weight")} desc={t("settings.weight_of_text")}>
|
||||
<SegChoice<string> label={t("look.text_weight")} value={weight} action="appearance.weight" onPick={onWeight}
|
||||
options={WEIGHTS.map(([id, name]) => [id, t(name)] as const)} />
|
||||
@@ -519,9 +505,9 @@ export function Appearance({ port, theme, onTheme, contrast, onContrast, weight,
|
||||
</SettingRow>
|
||||
</SectionCard>
|
||||
<SectionCard title={t("common.window")}>
|
||||
<SettingDeep id="window" title={t("common.window")} hint={t("settings.remember_window_position_and_size")} act={t("settings.change")}>
|
||||
|
||||
<WindowSection port={port} />
|
||||
</SettingDeep>
|
||||
|
||||
<GraphicsSection />
|
||||
</SectionCard>
|
||||
</>
|
||||
@@ -552,6 +538,7 @@ function FontPick({
|
||||
<span className="tx">{label}</span>
|
||||
<input
|
||||
className="fontown"
|
||||
aria-label={label}
|
||||
list={list}
|
||||
value={value}
|
||||
placeholder={options.length ? t("look.default_n_available_on_this", { n: options.length }) : t("common.default")}
|
||||
|
||||
@@ -9,7 +9,9 @@ afterEach(cleanup);
|
||||
describe("BrowserPanel", () => {
|
||||
it("opens a typed address when Enter is pressed", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<BrowserPanel id="b0" scheme="light" onAddress={vi.fn()} onExternal={vi.fn()} />);
|
||||
const external = vi.fn();
|
||||
render(<BrowserPanel id="b0" scheme="light" onAddress={vi.fn()} onExternal={external} />);
|
||||
expect(screen.getByTitle("Reasonix 内置 Browser").querySelector(".lucide-globe")).not.toBeNull();
|
||||
|
||||
const address = screen.getByRole("textbox", { name: "网页地址" });
|
||||
await user.clear(address);
|
||||
@@ -17,6 +19,17 @@ describe("BrowserPanel", () => {
|
||||
|
||||
expect((address as HTMLInputElement).value).toBe("https://example.com/docs");
|
||||
expect(screen.getByTitle("Reasonix 内置 Browser").getAttribute("src")).toBe("https://example.com/docs");
|
||||
const toolbar = document.querySelector(".studio-browser-toolbar")!;
|
||||
const outside = toolbar.querySelector<HTMLButtonElement>('[data-action="browser.external"]')!;
|
||||
expect(outside.querySelector(".lucide-external-link")).not.toBeNull();
|
||||
await user.click(outside);
|
||||
expect(external).toHaveBeenCalledWith("https://example.com/docs");
|
||||
const frame = screen.getByTitle("Reasonix 内置 Browser");
|
||||
await user.click(toolbar.querySelector('[data-action="browser.reload"]')!);
|
||||
expect(screen.getByTitle("Reasonix 内置 Browser")).not.toBe(frame);
|
||||
await user.click(toolbar.querySelector('[data-action="browser.back"]')!);
|
||||
expect((address as HTMLInputElement).value).toBe("reasonix://start");
|
||||
expect(outside.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps invalid addresses in the field and explains the error", async () => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState, type FormEvent, type KeyboardEvent } from "react";
|
||||
import { useEffect, useLayoutEffect, useRef, useState, type FormEvent, type KeyboardEvent } from "react";
|
||||
import { ArrowLeft, ArrowRight, ExternalLink, Globe, RotateCw } from "lucide-react";
|
||||
import { t } from "../i18n";
|
||||
import { reason } from "../i18n/kernel";
|
||||
import type { AgentPort, BrowserTab } from "../port/port";
|
||||
@@ -28,20 +29,14 @@ function normalise(value: string): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
function startPage(scheme: "light" | "dark"): string {
|
||||
const light = scheme === "light";
|
||||
const page = light ? "#f6f7f4" : "#171817";
|
||||
const text = light ? "#242622" : "#e9eae5";
|
||||
const muted = light ? "#747970" : "#90958d";
|
||||
const surface = light ? "#ffffff" : "#1c1d1b";
|
||||
const border = light ? "#dfe1db" : "#2c2f2b";
|
||||
return `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>
|
||||
:root{color-scheme:${scheme};font-family:Inter,"Noto Sans SC",system-ui,sans-serif;background:${page};color:${text}}
|
||||
*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:32px;background:${page}}
|
||||
main{width:min(520px,100%);text-align:center}.mark{width:42px;height:42px;margin:0 auto 18px;display:grid;place-items:center;border:1px solid ${border};border-radius:12px;background:${surface};color:#7188c4;font-size:22px}
|
||||
h1{margin:0 0 8px;font-size:18px;font-weight:620;letter-spacing:-.02em}p{margin:0;color:${muted};font-size:13px;line-height:1.7}
|
||||
.hint{margin-top:22px;padding:11px 14px;border:1px solid ${border};border-radius:9px;background:${surface};color:${muted};font-size:12px;text-align:left}
|
||||
</style></head><body><main><div class="mark">◎</div><h1>${t("common.built_in_browser")}</h1><p>${t("workbench.enter_an_address_above_to")}</p><div class="hint">${t("workbench.some_sites_do_not_allow")}</div></main></body></html>`;
|
||||
function BrowserStartPage({ scheme }: { scheme: "light" | "dark" }) {
|
||||
return <div className="studio-browser-frame studio-browser-start" title={t("workbench.reasonix_built_in_browser")} style={{ colorScheme: scheme }}>
|
||||
<div><Globe className="browser-start-mark" size={26} aria-hidden="true" />
|
||||
<h1>{t("common.built_in_browser")}</h1>
|
||||
<p>{t("workbench.enter_an_address_above_to")}</p>
|
||||
<p className="browser-start-hint">{t("workbench.some_sites_do_not_allow")}</p>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
interface ManualProps {
|
||||
@@ -63,7 +58,6 @@ export function ManualBrowserPanel({ id, hidden, onAddress, onExternal, scheme }
|
||||
const [loading, setLoading] = useState(false);
|
||||
const current = history[index] ?? START;
|
||||
const internal = current === START;
|
||||
const srcDoc = useMemo(() => startPage(scheme), [scheme]);
|
||||
useEffect(() => {
|
||||
onAddress(id, internal ? "" : new URL(current).hostname);
|
||||
}, [id, current, internal, onAddress]);
|
||||
@@ -113,32 +107,31 @@ export function ManualBrowserPanel({ id, hidden, onAddress, onExternal, scheme }
|
||||
<section className="studio-browser" hidden={hidden} aria-label={t("workbench.built_in_browser")}>
|
||||
<form className="studio-browser-toolbar" onSubmit={submit} data-action-submit="browser.navigate">
|
||||
<button type="button" data-action="browser.back" onClick={() => move(index - 1)} disabled={index === 0} aria-label={t("workbench.back")} title={t("workbench.back")}>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="m9.5 3-5 5 5 5M5 8h7" /></svg>
|
||||
<ArrowLeft size={16} />
|
||||
</button>
|
||||
<button type="button" data-action="browser.reload" onClick={() => setReload((value) => value + 1)} aria-label={t("common.re_freeze")} title={t("common.re_freeze")}>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M12.7 6A5 5 0 1 0 13 9.5M12.7 6V2.8M12.7 6H9.5" /></svg>
|
||||
<RotateCw size={16} />
|
||||
</button>
|
||||
<label className="studio-browser-address" data-invalid={invalid ? "" : undefined}>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="5.3" /><path d="M2.8 8h10.4M8 2.7c1.5 1.5 2.2 3.2 2.2 5.3S9.5 11.8 8 13.3C6.5 11.8 5.8 10.1 5.8 8S6.5 4.2 8 2.7Z" /></svg>
|
||||
<Globe size={16} />
|
||||
<input data-action-change="browser.address" data-action-keydown="browser.navigate" value={input} onKeyDown={commitAddress} onChange={(event) => { setInput(event.target.value); setInvalid(false); }} aria-label={t("workbench.web_address")} spellCheck={false} autoCapitalize="off" />
|
||||
</label>
|
||||
<button type="submit" className="studio-browser-go" aria-label={t("workbench.open_page")}>{t("common.open")}</button>
|
||||
<button type="button" data-action="browser.external" onClick={() => !internal && onExternal(current)} disabled={internal} aria-label={t("workbench.open_in_external_browser")} title={t("workbench.open_in_external_browser")}>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M9 3h4v4M13 3 7.5 8.5" /><path d="M7 4H3v9h9V9" /></svg>
|
||||
<ExternalLink size={16} />
|
||||
</button>
|
||||
</form>
|
||||
{invalid && <p className="studio-browser-error" role="alert">{t("workbench.enter_a_valid_http_or")}</p>}
|
||||
<iframe
|
||||
{internal ? <BrowserStartPage scheme={scheme} /> : <iframe
|
||||
key={`${current}:${reload}`}
|
||||
className="studio-browser-frame"
|
||||
title={t("workbench.reasonix_built_in_browser")}
|
||||
src={internal ? undefined : current}
|
||||
srcDoc={internal ? srcDoc : undefined}
|
||||
src={current}
|
||||
sandbox="allow-scripts allow-forms allow-popups"
|
||||
referrerPolicy="no-referrer"
|
||||
onLoad={() => setLoading(false)}
|
||||
onError={() => setLoading(false)}
|
||||
/>
|
||||
/>}
|
||||
{/* Not a hint at an icon somewhere else: a site that refuses to be framed
|
||||
shows the shell's own "refused to connect", and the way out has to be
|
||||
beside it. */}
|
||||
@@ -312,10 +305,10 @@ function AgentBrowserView({ tabs, shown, showTabs = true, onOpenDocument }: { ta
|
||||
))}
|
||||
</div>}
|
||||
<div className="bnav">
|
||||
<button className="bbtn" data-action="browser.control" data-target={current?.id} data-value="back" aria-label={t("workbench.back")} onClick={() => control("back")}>←</button>
|
||||
<button className="bbtn" data-action="browser.control" data-target={current?.id} data-value="forward" aria-label={t("workbench.forward")} onClick={() => control("forward")}>→</button>
|
||||
<button className="bbtn" data-action="browser.control" data-target={current?.id} data-value="reload" aria-label={t("common.reload")} onClick={() => control("reload")}>↻</button>
|
||||
<button className="bbtn" data-action="browser.external" disabled={!documentPath && !/^https?:\/\//i.test(current?.url ?? "")} aria-label={documentPath ? t("workbench.open_this_file_with_the") : t("workbench.open_this_page_in_the")} title={documentPath ? t("workbench.open_this_file_with_the") : t("workbench.open_this_page_in_the")} onClick={() => (documentPath ? openDocument() : current && void host().openExternal(current.url))}>↗</button>
|
||||
<button className="bbtn" data-action="browser.control" data-target={current?.id} data-value="back" aria-label={t("workbench.back")} title={t("workbench.back")} onClick={() => control("back")}><ArrowLeft size={16} /></button>
|
||||
<button className="bbtn" data-action="browser.control" data-target={current?.id} data-value="forward" aria-label={t("workbench.forward")} title={t("workbench.forward")} onClick={() => control("forward")}><ArrowRight size={16} /></button>
|
||||
<button className="bbtn" data-action="browser.control" data-target={current?.id} data-value="reload" aria-label={t("common.reload")} title={t("common.reload")} onClick={() => control("reload")}><RotateCw size={16} /></button>
|
||||
<button className="bbtn" data-action="browser.external" disabled={!documentPath && !/^https?:\/\//i.test(current?.url ?? "")} aria-label={documentPath ? t("workbench.open_this_file_with_the") : t("workbench.open_this_page_in_the")} title={documentPath ? t("workbench.open_this_file_with_the") : t("workbench.open_this_page_in_the")} onClick={() => (documentPath ? openDocument() : current && void host().openExternal(current.url))}><ExternalLink size={16} /></button>
|
||||
<form className="baddr" data-action="browser.navigate" data-target={current?.id} onSubmit={go}>
|
||||
<input data-action="browser.navigate" data-target={current?.id} value={address} spellCheck={false} aria-label={t("workbench.address")} onChange={(event) => { setAddress(event.target.value); setRefused(""); }} />
|
||||
</form>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { StudioIcon } from "./StudioIcon";
|
||||
import { useState } from "react";
|
||||
import { t } from "../i18n";
|
||||
import type { CapabilityScope } from "../port/port";
|
||||
@@ -27,7 +28,7 @@ export function ScopePill({
|
||||
<span className="scopeanchor">
|
||||
<button type="button" className="spill who" data-action="ext.scope" aria-expanded={open} onClick={() => setOpen(!open)}>
|
||||
{scope.label || scope.name}
|
||||
<i className="cv" aria-hidden>▾</i>
|
||||
<i className="cv" aria-hidden><StudioIcon name="down" /></i>
|
||||
</button>
|
||||
{open && (
|
||||
<ScopePicker
|
||||
|
||||
@@ -10,16 +10,13 @@ import { OfficePreviewSection } from "./OfficePreviewSection";
|
||||
import { PlanProgressSection } from "./PlanProgressSection";
|
||||
import { PresentFiles } from "./PresentFiles";
|
||||
import { SectionCard } from "./SectionCard";
|
||||
import { SettingDeep } from "./SettingDeep";
|
||||
|
||||
export function ChatDisplaySettings({ port }: { port: AgentPort }) {
|
||||
return (
|
||||
<>
|
||||
<SectionCard title={t("settings.process_card")}>
|
||||
<FoldWorkSetting port={port} />
|
||||
<SettingDeep id="folding" title={t("chat.transcript_folding")} hint={t("settings.older_turns_fold_to_a_line")} act={t("safety.details")}>
|
||||
<section className="prefs-card"><Folding /></section>
|
||||
</SettingDeep>
|
||||
<Folding />
|
||||
<PlanProgressSection />
|
||||
</SectionCard>
|
||||
<SectionCard title={t("settings.files_and_previews_card")}>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { StudioIcon } from "./StudioIcon";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { t } from "../i18n";
|
||||
import { host as hostBridge } from "../port/host";
|
||||
@@ -93,7 +94,7 @@ export function Chrome({ port, status, workspace: ws, title, steer, account, hos
|
||||
disabled={!canOpenProject}
|
||||
onClick={openProject}
|
||||
>
|
||||
<svg viewBox="0 0 16 16" width="16" height="16" aria-hidden="true"><path d="M2.5 4.5h4l1.2 1.4h5.8v6.6h-11z" /></svg>
|
||||
<StudioIcon name="folder" />
|
||||
</button>
|
||||
<span className="crumb-proj" title={root}>
|
||||
{project}
|
||||
@@ -126,7 +127,7 @@ export function Chrome({ port, status, workspace: ws, title, steer, account, hos
|
||||
<div className="r" ref={group}>
|
||||
{gathered && (
|
||||
<button ref={trigger} className="thbtn r-more" data-action="chrome.more" aria-haspopup="true" aria-controls="chrome-fold" aria-expanded={more} aria-label={t("common.more")} onClick={() => setMore((v) => !v)}>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 8h.01M8 8h.01M12.5 8h.01" /></svg>
|
||||
<StudioIcon name="more" />
|
||||
</button>
|
||||
)}
|
||||
<div className="r-fold" id="chrome-fold" data-open={more ? "" : undefined}>
|
||||
@@ -140,7 +141,7 @@ export function Chrome({ port, status, workspace: ws, title, steer, account, hos
|
||||
aria-keyshortcuts={ariaChord("f")}
|
||||
title={t("common.find_in_this_conversation")} shortcut={chord("F")}
|
||||
>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M7.2 3.1a4.1 4.1 0 1 0 0 8.2 4.1 4.1 0 0 0 0-8.2M10.3 10.3 13 13" /></svg>
|
||||
<StudioIcon name="search" />
|
||||
</TooltipButton>
|
||||
)}
|
||||
{hub && onError && <PhonePop hub={hub} />}
|
||||
@@ -152,9 +153,9 @@ export function Chrome({ port, status, workspace: ws, title, steer, account, hos
|
||||
title={theme === "dark" ? t("app.switch_to_light_theme") : t("app.switch_to_dark_theme")}
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M12.9 9.7A5.3 5.3 0 0 1 6.3 3.1a5.3 5.3 0 1 0 6.6 6.6Z" /></svg>
|
||||
<StudioIcon name="sun" />
|
||||
) : (
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true"><circle cx="8" cy="8" r="2.7" /><path d="M8 1.7v1.4M8 12.9v1.4M1.7 8h1.4M12.9 8h1.4M3.5 3.5l1 1M11.5 11.5l1 1M3.5 12.5l1-1M11.5 4.5l1-1" /></svg>
|
||||
<StudioIcon name="moon" />
|
||||
)}
|
||||
</TooltipButton>
|
||||
{port && <ViewSwitch view={view} rows={rows} surfaces={surfaces} onView={onView} />}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { StudioIcon } from "./StudioIcon";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { t } from "../i18n";
|
||||
import type { AgentPort, Completion, CompletionItem } from "../port/port";
|
||||
@@ -291,7 +292,7 @@ export function CompletionMenu({ id, items, active, kind, query, kb, onPick, onH
|
||||
{before}
|
||||
{hit && <b className="hit">{hit}</b>}
|
||||
{after}
|
||||
{it.descend && <i className="ah" aria-hidden="true">›</i>}
|
||||
{it.descend && <i className="ah" aria-hidden="true"><StudioIcon name="chevron" /></i>}
|
||||
</span>
|
||||
{it.hint && <span className="ds">{it.hint}</span>}
|
||||
{it.args?.map((a) => (
|
||||
|
||||
@@ -63,10 +63,13 @@ describe("the composer's tool row on a phone", () => {
|
||||
});
|
||||
|
||||
describe("the stop button", () => {
|
||||
it("carries the board's pause mark as well as the square, and the stylesheet picks one", () => {
|
||||
it.each(["", PHONE])("keeps the same stop and send symbols when the layout is %s", (fold) => {
|
||||
document.documentElement.dataset.fold = fold;
|
||||
const box = draw(true);
|
||||
const stop = box.querySelector(".btn.stop")!;
|
||||
expect(stop.querySelector(".ic-stop")).not.toBeNull();
|
||||
expect(stop.querySelector(".ic-pause")).not.toBeNull();
|
||||
const stop = screen.getByRole("button", { name: "停下" });
|
||||
expect(stop.getAttribute("data-action")).toBe("session.stop");
|
||||
expect(stop.querySelectorAll("svg")).toHaveLength(1);
|
||||
expect(stop.querySelector(".lucide-square")).not.toBeNull();
|
||||
expect(box.querySelector('.btn.send .lucide-arrow-up')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useId, useRef, useState, useSyncExternalStore, type ReactNode } from "react";
|
||||
import { createElement, useCallback, useEffect, useId, useRef, useState, useSyncExternalStore, type ReactNode } from "react";
|
||||
import { ArrowUp } from "lucide-react";
|
||||
import { useFolded } from "./usefold";
|
||||
import { StopButton } from "./StopButton";
|
||||
import { createPortal } from "react-dom";
|
||||
@@ -27,7 +28,8 @@ import { useFitHeight } from "./fitHeight";
|
||||
import { onComposerHintChange, showsComposerHint, useRunningMessageIntent, useSendShortcut } from "../state/prefs";
|
||||
import { runningSendIntent, sendAria, sendHint, sendsOnKey } from "./sendkey";
|
||||
import { countLines, pasteIsLong, planTone, planVerb } from "./intake";
|
||||
import { kindOf, nameOf, previewURL } from "./chipfile";
|
||||
import { nameOf, previewURL } from "./chipfile";
|
||||
import { fileAppearance } from "./document/FileTypeIcon";
|
||||
import { useIntake } from "./useIntake";
|
||||
import type { Dropped } from "./filedrop";
|
||||
import type { Quote } from "./cards/SayCard";
|
||||
@@ -479,7 +481,7 @@ export function Composer({ port, status, running, queued = false, quote, restore
|
||||
<span aria-hidden="true" style={c.url ? { backgroundImage: `url(${c.url})` } : undefined} />
|
||||
</ImagePreview>
|
||||
) : (
|
||||
<span className="glyph" aria-hidden="true">{kindOf(c.a?.path ?? c.name)}</span>
|
||||
<span className="glyph" aria-hidden="true">{createElement(fileAppearance(c.a?.path ?? c.name).icon, { size: 20, strokeWidth: 1.8 })}</span>
|
||||
)}
|
||||
<span className="meta">
|
||||
<span className="nm" title={c.a?.path ?? c.name}>{c.name}</span>
|
||||
@@ -503,7 +505,7 @@ export function Composer({ port, status, running, queued = false, quote, restore
|
||||
</>
|
||||
) : c.k === "session" ? (
|
||||
<>
|
||||
<span className="glyph" aria-hidden="true">{"#"}</span>
|
||||
<span className="glyph" aria-hidden="true"><StudioIcon name="history" /></span>
|
||||
<span className="meta">
|
||||
<span className="nm" title={c.title}>{c.title}</span>
|
||||
<span className="sz">{t("composer.past_conversation_read_only_reference")}</span>
|
||||
@@ -511,7 +513,7 @@ export function Composer({ port, status, running, queued = false, quote, restore
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="glyph" aria-hidden="true">{c.k === "quote" ? "❝" : "TXT"}</span>
|
||||
<span className="glyph" aria-hidden="true"><StudioIcon name={c.k === "quote" ? "quote" : "file"} /></span>
|
||||
<span className="meta">
|
||||
<span className="nm" title={c.body}>{chipName(c)}</span>
|
||||
<button
|
||||
@@ -532,7 +534,7 @@ export function Composer({ port, status, running, queued = false, quote, restore
|
||||
aria-label={t("common.remove_name", { name: chipName(c) })}
|
||||
onClick={() => discard(c)}
|
||||
>
|
||||
×
|
||||
<StudioIcon name="close" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
@@ -552,7 +554,7 @@ export function Composer({ port, status, running, queued = false, quote, restore
|
||||
{queueStrip}
|
||||
<div className="fmain">
|
||||
<span className="prompt" aria-hidden="true">
|
||||
›
|
||||
<StudioIcon name="chevron" />
|
||||
</span>
|
||||
<textarea
|
||||
data-action-keydown="session.send" data-window-keys=""
|
||||
@@ -709,7 +711,7 @@ export function Composer({ port, status, running, queued = false, quote, restore
|
||||
{ value: "plan", label: "Plan", desc: t("composer.plan_steps_before_changing_files") },
|
||||
]}
|
||||
onPick={(value) => change("plan", () => port.setPlanMode(value === "plan"))}
|
||||
label={<><StudioIcon name="agent" /><span className="studio-sr-label">{t("common.plan")}</span><span>{status?.plan ? "Plan" : "Agent"}</span><StudioIcon name="down" /></>}
|
||||
label={<><StudioIcon name={status?.plan ? "tasks" : "agent"} /><span className="studio-sr-label">{t("common.plan")}</span><span>{status?.plan ? "Plan" : "Agent"}</span><StudioIcon name="down" /></>}
|
||||
/>
|
||||
</div>
|
||||
{git?.repo && (worktree.sources || status?.workspaceRoot) ? <WorktreeChip port={port} workspace={workspace} onChanged={onChanged} onSwitched={onTreeChanged} onError={onError} changeCount={countElsewhere ? 0 : changeCount} sources={worktree.sources ?? { available: false, sourceRoot: status?.workspaceRoot ?? "", sources: [] }} canCreate={fresh && !!worktree.sources?.available} selection={worktree.selection} branch={git.branch} busy={submitting || running} onSelect={worktree.setSelection} refresh={worktree.refresh} /> : <BranchChip port={port} git={git} changeCount={changeCount} countElsewhere={countElsewhere} onChanged={onChanged} onSwitched={onTreeChanged} onError={onError} />}
|
||||
@@ -763,7 +765,7 @@ export function Composer({ port, status, running, queued = false, quote, restore
|
||||
</div>
|
||||
{phone && (
|
||||
<button type="button" className="mode plain turn-more" data-action="composer.more-tools" aria-expanded={toolsOpen} aria-label={t("composer.more_tools")} onClick={() => setToolsOpen((on) => !on)}>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M3.5 8h.01M8 8h.01M12.5 8h.01" /></svg>
|
||||
<StudioIcon name="more" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -780,9 +782,7 @@ export function Composer({ port, status, running, queued = false, quote, restore
|
||||
onClick={send}
|
||||
>
|
||||
<span className="ic" aria-hidden="true">
|
||||
<svg viewBox="0 0 16 16">
|
||||
<path d="M8 12.8V3.4M4.2 7.2 8 3.4l3.8 3.8" />
|
||||
</svg>
|
||||
<ArrowUp />
|
||||
</span>
|
||||
<span>{t(submitting ? "common.sending" : running ? runningIntent === "followup" ? "composer.queue" : "common.steer" : "common.send")}</span>
|
||||
</button>
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
// @vitest-environment jsdom
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import "./testkit";
|
||||
import { MockHub } from "../port/mock_hub";
|
||||
import { MockPort } from "../port/mock";
|
||||
import type { AgentPort } from "../port/port";
|
||||
import { ConnectSettings } from "./ConnectSettings";
|
||||
import { SettingDeepContext } from "./SettingDeep";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
function draw(remote: boolean) {
|
||||
const go = vi.fn();
|
||||
const port = new MockPort() as unknown as AgentPort;
|
||||
render(
|
||||
<SettingDeepContext.Provider value={{ pages: true, open: null, section: "连接", go }}>
|
||||
<>
|
||||
<ConnectSettings port={port} networkPort={port} host="" hub={new MockHub()} onError={() => {}} remote={remote} />
|
||||
</SettingDeepContext.Provider>,
|
||||
</>,
|
||||
);
|
||||
return go;
|
||||
}
|
||||
|
||||
it("is a network card and an other-machines card of one row per setting", () => {
|
||||
@@ -27,12 +24,10 @@ it("is a network card and an other-machines card of one row per setting", () =>
|
||||
for (const id of ["network", "remote", "phone"]) expect(document.querySelector(`[data-setting="${id}"]`), id).toBeTruthy();
|
||||
});
|
||||
|
||||
it("opens each page from its pill", () => {
|
||||
const go = draw(true);
|
||||
for (const [name, id] of [["详细设置", "network"], ["远程机器", "remote"], ["设置手机访问", "phone"]] as const) {
|
||||
fireEvent.click(screen.getByRole("button", { name }));
|
||||
expect(go).toHaveBeenLastCalledWith(id);
|
||||
}
|
||||
it("expands network and machine controls in their cards", async () => {
|
||||
draw(true);
|
||||
expect(await screen.findByRole("radiogroup", { name: "代理模式" })).toBeTruthy();
|
||||
for (const id of ["remote", "phone"]) expect(document.querySelector(`#set-${id} .grp-items`)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("draws no machines card where remote access is not offered", () => {
|
||||
|
||||
@@ -23,16 +23,16 @@ export function ConnectSettings({ port, networkPort, host, hub, onError, remote
|
||||
return (
|
||||
<>
|
||||
<SectionCard title={t("common.network")}>
|
||||
<Group id="network" pill={t("safety.details")} title={t("common.network")} hint={t("settings.network_hint")}>
|
||||
<Group id="network" title={t("common.network")} hint={t("settings.network_hint")}>
|
||||
<Network port={networkPort} host={host} />
|
||||
</Group>
|
||||
</SectionCard>
|
||||
{remote && (
|
||||
<SectionCard title={t("settings.other_machines")}>
|
||||
<Group id="remote" pill={t("settings.remote_machines")} title={t("settings.remote")} hint={t("settings.remote_hint")}>
|
||||
<Group id="remote" title={t("settings.remote")} hint={t("settings.remote_hint")}>
|
||||
<Remotes hub={hub} onError={onError} />
|
||||
</Group>
|
||||
<Group id="phone" pill={t("settings.set_up_phone_access")} title={t("remote.phone_access")} hint={t("settings.phone_hint")}>
|
||||
<Group id="phone" title={t("remote.phone_access")} hint={t("settings.phone_hint")}>
|
||||
<PhoneAccess hub={hub} onError={onError} />
|
||||
<PhoneNotifications hub={hub} port={port} onError={onError} />
|
||||
{remoteDeviceId() && <PhoneReminders host={remoteDeviceId()!} />}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { StudioIcon } from "./StudioIcon";
|
||||
import { useState } from "react";
|
||||
import { t } from "../i18n";
|
||||
import type { ContextBreakdown, McpEntry } from "../port/port";
|
||||
@@ -39,7 +40,7 @@ export function ContextSummaryCard({ context, mcp, percent, onManage, compact: a
|
||||
<div key={key}>
|
||||
<i data-part={key} /><span>{label}</span>
|
||||
{holdersOf(key).length > 0
|
||||
? <button type="button" className="chrome-context-more" data-action="context.largest" data-value={key} aria-expanded={listed === key} aria-label={t("composer.show_the_largest_items_in", { name: label })} onClick={() => setListed(listed === key ? null : key)}>{compact(value)}</button>
|
||||
? <button type="button" className="chrome-context-more" data-action="context.largest" data-value={key} aria-expanded={listed === key} aria-label={t("composer.show_the_largest_items_in", { name: label })} onClick={() => setListed(listed === key ? null : key)}>{compact(value)}<StudioIcon name="chevron" /></button>
|
||||
: <b>{compact(value)}</b>}
|
||||
</div>
|
||||
))}</div>
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
// @vitest-environment jsdom
|
||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { cleanup, render, waitFor } from "@testing-library/react";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import "./testkit";
|
||||
import { MockHub } from "../port/mock_hub";
|
||||
import { MockPort } from "../port/mock";
|
||||
import type { AgentPort } from "../port/port";
|
||||
import { DataSettings } from "./DataSettings";
|
||||
import { SettingDeepContext } from "./SettingDeep";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
function draw(signedIn = false) {
|
||||
const go = vi.fn();
|
||||
const port = new MockPort() as unknown as AgentPort;
|
||||
render(
|
||||
<SettingDeepContext.Provider value={{ pages: true, open: null, section: "数据", go }}>
|
||||
<>
|
||||
<DataSettings port={port} homePort={port} localPort={port} hub={new MockHub()} workspace="/w" cwd="/w" busy="" signedIn={signedIn}
|
||||
memories={7} onIsolate={() => {}} onChanged={() => {}} onRecovered={() => {}} />
|
||||
</SettingDeepContext.Provider>,
|
||||
onIsolate={() => {}} onChanged={() => {}} onRecovered={() => {}} />
|
||||
</>,
|
||||
);
|
||||
return go;
|
||||
}
|
||||
|
||||
it("is three cards: memory and usage, session storage, backup, each setting one row", async () => {
|
||||
@@ -31,12 +28,13 @@ it("is three cards: memory and usage, session storage, backup, each setting one
|
||||
expect(document.querySelector('[data-setting="backup"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("opens the memory, usage and storage pages from pills, with the memory count", () => {
|
||||
const go = draw();
|
||||
for (const [name, id] of [["打开记忆(7)", "memory"], ["查看用量", "usage"], ["管理", "storage"], ["导出与导入…", "backup-file"]] as const) {
|
||||
fireEvent.click(screen.getByRole("button", { name }));
|
||||
expect(go).toHaveBeenLastCalledWith(id);
|
||||
}
|
||||
it("expands memory, usage, storage and export controls together", async () => {
|
||||
draw();
|
||||
await waitFor(() => {
|
||||
for (const id of ["memory", "usage", "storage", "backup-file"]) {
|
||||
expect(document.querySelector(`#set-${id} .grp-items`), id).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("offers the cloud backup only when signed in", () => {
|
||||
|
||||
@@ -21,17 +21,16 @@ interface Props {
|
||||
cwd?: string;
|
||||
busy: string;
|
||||
signedIn: boolean;
|
||||
memories?: number;
|
||||
onIsolate: () => void;
|
||||
onChanged: () => void;
|
||||
onRecovered: () => void;
|
||||
}
|
||||
|
||||
export function DataSettings({ port, homePort, localPort, hub, workspace, cwd, busy, signedIn, memories, onIsolate, onChanged, onRecovered }: Props) {
|
||||
export function DataSettings({ port, homePort, localPort, hub, workspace, cwd, busy, signedIn, onIsolate, onChanged, onRecovered }: Props) {
|
||||
return (
|
||||
<>
|
||||
<SectionCard title={t("data.memory_and_usage")} hint={t("data.what_the_agent_remembers_and")}>
|
||||
<Group id="memory" pill={memories === undefined ? t("settings.open_memory") : `${t("settings.open_memory")}(${memories})`}
|
||||
<Group id="memory"
|
||||
title={t("data.memory")}
|
||||
hint={t("settings.what_it_remembers")}
|
||||
>
|
||||
@@ -40,15 +39,15 @@ export function DataSettings({ port, homePort, localPort, hub, workspace, cwd, b
|
||||
<UsageSettings port={homePort} onChanged={onChanged} />
|
||||
</SectionCard>
|
||||
<SectionCard title={t("data.session_storage")} hint={t("data.where_sessions_and_indexes_live")}>
|
||||
<Group id="storage" pill={t("settings.manage_storage")} title={t("common.storage")} hint={t("settings.storage_hint")}><Storage port={homePort} hub={hub} workspace={workspace} onRecovered={onRecovered} /></Group>
|
||||
<Group id="storage" title={t("common.storage")} hint={t("settings.storage_hint")}><Storage port={homePort} hub={hub} workspace={workspace} onRecovered={onRecovered} /></Group>
|
||||
<AutoArchive port={port} />
|
||||
<SessionDirSettings cwd={cwd} busy={busy} onIsolate={onIsolate} />
|
||||
<LegacySessionsSection />
|
||||
</SectionCard>
|
||||
<SectionCard title={t("data.backup")} hint={t("data.take_your_configuration_to_another")}>
|
||||
<Group id="backup-file" pill={t("settings.export_and_import")} title={t("data.export_and_import_config")} hint={t("settings.config_in_one_file")}><BackupFile port={port} /></Group>
|
||||
<Group id="backup-file" title={t("data.export_and_import_config")} hint={t("settings.config_in_one_file")}><BackupFile port={port} /></Group>
|
||||
{signedIn && (
|
||||
<Group id="backup" pill={t("safety.details")} title={t("data.cloud_backup")} hint={t("settings.back_up_to_the_cloud")}>
|
||||
<Group id="backup" title={t("data.cloud_backup")} hint={t("settings.back_up_to_the_cloud")}>
|
||||
<Backup port={port} />
|
||||
</Group>
|
||||
)}
|
||||
|
||||
@@ -55,6 +55,7 @@ describe("how a connection is billed", () => {
|
||||
|
||||
it("says what the subscription figures mean", async () => {
|
||||
await open(entry());
|
||||
expect(screen.getByRole("option", { name: "订阅套餐(估算)" })).toBeTruthy();
|
||||
expect(screen.getByText("订阅套餐的用量按上面的价格折算成估算值,单独显示,不会计入实际花费。")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
import { StudioIcon } from "./StudioIcon";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { t } from "../i18n";
|
||||
import type { FindScope } from "../state/find";
|
||||
@@ -89,9 +90,7 @@ function Bar({
|
||||
role="search"
|
||||
data-missed={missed ? "" : undefined}
|
||||
>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M7.2 3.1a4.1 4.1 0 1 0 0 8.2 4.1 4.1 0 0 0 0-8.2M10.3 10.3 13 13" />
|
||||
</svg>
|
||||
<StudioIcon name="search" />
|
||||
<input
|
||||
ref={box}
|
||||
type="search"
|
||||
@@ -140,9 +139,7 @@ function Bar({
|
||||
title={t("chat.previous_match")}
|
||||
onClick={() => onStep(-1)}
|
||||
>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M4.4 9.6 8 6l3.6 3.6" />
|
||||
</svg>
|
||||
<StudioIcon name="up" />
|
||||
</button>
|
||||
<button
|
||||
className="st"
|
||||
@@ -153,9 +150,7 @@ function Bar({
|
||||
title={t("chat.next_match")}
|
||||
onClick={() => onStep(1)}
|
||||
>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M4.4 6.4 8 10l3.6-3.6" />
|
||||
</svg>
|
||||
<StudioIcon name="down" />
|
||||
</button>
|
||||
<span className="sep" aria-hidden="true" />
|
||||
<button
|
||||
@@ -167,7 +162,7 @@ function Bar({
|
||||
title={t("common.match_case")}
|
||||
onClick={() => setMode({ caseSensitive: !caseSensitive })}
|
||||
>
|
||||
Aa
|
||||
<StudioIcon name="matchCase" />
|
||||
</button>
|
||||
<button
|
||||
className="mode-toggle"
|
||||
@@ -178,7 +173,7 @@ function Bar({
|
||||
title={t("chat.read_the_query_as_a")}
|
||||
onClick={() => setMode({ regex: !regex })}
|
||||
>
|
||||
.*
|
||||
<StudioIcon name="regex" />
|
||||
</button>
|
||||
<button
|
||||
className="st"
|
||||
@@ -188,9 +183,7 @@ function Bar({
|
||||
title={t("chat.close_find")}
|
||||
onClick={onClose}
|
||||
>
|
||||
<svg viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M4.5 4.5 11.5 11.5M11.5 4.5 4.5 11.5" />
|
||||
</svg>
|
||||
<StudioIcon name="close" />
|
||||
</button>
|
||||
</div>
|
||||
{missed ? (
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
// Settings, drawn as a gear. A circle with rays is the sun, which is what the
|
||||
// light-or-dark control and the appearance page already are.
|
||||
export const GEAR_PATH = "M6.36 3.28 L6.71 1.53 L9.29 1.53 L9.64 3.28 L10.18 3.50 L11.67 2.51 L13.49 4.33 L12.50 5.82 L12.72 6.36 L14.47 6.71 L14.47 9.29 L12.72 9.64 L12.50 10.18 L13.49 11.67 L11.67 13.49 L10.18 12.50 L9.64 12.72 L9.29 14.47 L6.71 14.47 L6.36 12.72 L5.82 12.50 L4.33 13.49 L2.51 11.67 L3.50 10.18 L3.28 9.64 L1.53 9.29 L1.53 6.71 L3.28 6.36 L3.50 5.82 L2.51 4.33 L4.33 2.51 L5.82 3.50Z";
|
||||
|
||||
export const GearMark = (
|
||||
<>
|
||||
<path d={GEAR_PATH} />
|
||||
<circle cx="8" cy="8" r="2" />
|
||||
</>
|
||||
);
|
||||
@@ -1,22 +1,20 @@
|
||||
import { t } from "../i18n";
|
||||
import { SETTING_AT, type SettingScope } from "./prefsnav";
|
||||
import type { ReactNode } from "react";
|
||||
import { SettingDeep } from "./SettingDeep";
|
||||
|
||||
// id is required, and that is the whole enforcement: a settings block cannot
|
||||
// be written without naming itself, and a name the catalogue does not carry
|
||||
// fails the gate. So "which setting is this, and when does changing it take
|
||||
// effect" has no default and no way to be left unanswered.
|
||||
export function Group({
|
||||
id, title, hint, now, action, pill, inline, children,
|
||||
id, title, hint, now, action, inline, children,
|
||||
}: {
|
||||
id: string; title: string; hint?: string; now?: string; action?: ReactNode; pill?: string; inline?: boolean; children?: ReactNode;
|
||||
id: string; title: string; hint?: string; now?: string; action?: ReactNode; inline?: boolean; children?: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<SettingDeep id={id} title={title} now={now} hint={hint} act={pill}>
|
||||
<section className="grp" id={`set-${id}`} data-setting={id} data-inline={inline ? "" : undefined}>
|
||||
<div className="grp-hd">
|
||||
<h3>{title}</h3>
|
||||
<h4>{title}</h4>
|
||||
<ScopeTag id={id} />
|
||||
{now && <span className="now">{now}</span>}
|
||||
{action}
|
||||
@@ -28,7 +26,6 @@ export function Group({
|
||||
as three alarms. */}
|
||||
<ApplyNote id={id} />
|
||||
</section>
|
||||
</SettingDeep>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Loaded 100 of 287 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user