Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5207d6f89b |
No files matched your search
@@ -10,7 +10,6 @@
|
||||
|
||||
# User-facing guides and references.
|
||||
/docs/ @SivanCola @esengine
|
||||
/docs/THEME_AUTHOR_GUIDE.md @esengine @SivanCola
|
||||
/sdk/ @SivanCola @esengine
|
||||
/README.md @SivanCola @esengine
|
||||
/README.zh-CN.md @SivanCola @esengine
|
||||
@@ -23,6 +22,7 @@
|
||||
/docs/*_CONTRACT.md @esengine @SivanCola
|
||||
/docs/TOOL_APPROVAL_MODES.md @esengine @SivanCola
|
||||
/docs/CAPABILITY_DIAGNOSTICS.md @esengine @SivanCola
|
||||
/docs/CODE_SIGNING_POLICY.md @esengine @SivanCola
|
||||
/docs/CHECKPOINTS.md @esengine @SivanCola
|
||||
/docs/EXTENSION_*.md @esengine @SivanCola
|
||||
/docs/SUBAGENT_*.md @esengine @SivanCola
|
||||
|
||||
@@ -6,9 +6,3 @@ contact_links:
|
||||
- name: Questions & discussions
|
||||
url: https://github.com/esengine/DeepSeek-Reasonix/discussions
|
||||
about: For usage questions, ideas, and general discussion — please use Discussions instead of opening an issue.
|
||||
- name: Discord
|
||||
url: https://discord.gg/XF78rEME2D
|
||||
about: Bilingual community chat for setup help, workflow showcases, and feature ideas.
|
||||
- name: QQ group (DeepSeek-Reasonix官方群, 1093562660)
|
||||
url: https://qm.qq.com/q/i59b0z2R8s
|
||||
about: Chinese-language community chat for setup help and everyday questions.
|
||||
@@ -8,22 +8,6 @@ only, on `main-v2`.
|
||||
|
||||
-
|
||||
|
||||
## Cause
|
||||
|
||||
-
|
||||
|
||||
## Blast radius
|
||||
|
||||
-
|
||||
|
||||
## Neighbouring behaviours tested
|
||||
|
||||
-
|
||||
|
||||
## Why this layer
|
||||
|
||||
-
|
||||
|
||||
## Issues
|
||||
|
||||
<!--
|
||||
|
||||
|
Before Width: | Height: | Size: 138 KiB |
@@ -62,7 +62,7 @@ jobs:
|
||||
# both paths must trigger the sdk job.
|
||||
if echo "$files" | grep -qE '^(sdk/|internal/ext/extension/)'; then sdk=true; fi
|
||||
# The release-note tests render every committed Studio note; npm/ holds the publish tests.
|
||||
if echo "$files" | grep -qE '^(scripts/|npm/|release-notes/studio/|\.github/workflows/release-studio\.yml$)'; then scripts=true; fi
|
||||
if echo "$files" | grep -qE '^(scripts/|npm/|release-notes/studio/)'; then scripts=true; fi
|
||||
fi
|
||||
fi
|
||||
{ echo "code=$code"; echo "desktop=$desktop"; echo "sdk=$sdk"; echo "scripts=$scripts"; } >> "$GITHUB_OUTPUT"
|
||||
@@ -80,11 +80,7 @@ jobs:
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: "22"
|
||||
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: true
|
||||
- run: node --test scripts/cli-archive-build.test.mjs scripts/release-credits.test.mjs scripts/update-contributors.test.mjs scripts/markdown-refs.test.mjs scripts/studio-release-notes.test.mjs scripts/publish-cli-pointer.test.mjs scripts/brew-cask-guard.test.mjs scripts/feedback-sync.test.mjs npm/publish.test.mjs
|
||||
- run: node --test scripts/release-credits.test.mjs scripts/markdown-refs.test.mjs scripts/studio-release-notes.test.mjs npm/publish.test.mjs
|
||||
|
||||
# The ruleset requires the per-OS check names (test (ubuntu-latest) etc.).
|
||||
# A matrix job skipped at job level reports no per-leg checks at all, so
|
||||
@@ -179,12 +175,12 @@ jobs:
|
||||
# budget above normal 7-9 minute runner variance; each package test binary
|
||||
# remains independently bounded by Go's per-package timeout below.
|
||||
#
|
||||
# That timeout is 12m, not 3m: across four consecutive runs of the same
|
||||
# That timeout is 8m, not 3m: across four consecutive runs of the same
|
||||
# code, internal/assembly/boot measured 133s, 146s, 162s and then hit 180s, and
|
||||
# internal/runtime/agent 82s, 92s, 144s and 180s — a 2.2x spread from runner
|
||||
# variance alone, with four heavy packages sharing four vCPUs. The boot package
|
||||
# has since grown to 422s and then ran past 480s on two consecutive runs of the
|
||||
# same code, so 8m no longer leaves room. A hang still trips the job budget.
|
||||
# variance alone, with four heavy packages sharing four vCPUs. At 3m the
|
||||
# step failed on slow runners without a code change; a hang still trips
|
||||
# the 15 minute job budget above.
|
||||
#
|
||||
# Every root-module `go test` that runs on Windows passes -count=1. With
|
||||
# the result cache on, go test records each file and environment access
|
||||
@@ -195,11 +191,11 @@ jobs:
|
||||
# internal/base/testenv holds every such step to it.
|
||||
- name: test (Windows smoke)
|
||||
if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request'
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
REASONIX_RELEASE_CACHE_GUARD: "1"
|
||||
WINDOWS_SANDBOX_WAIT_MS: "20000"
|
||||
run: go test -count=1 -p 4 -timeout=12m ./internal/runtime/agent/... ./internal/platform/appidentity/... ./internal/assembly/boot/... ./internal/state/checkpoint/... ./internal/frontend/cli/... ./internal/session/control/... ./internal/base/filelock/... ./internal/base/fileutil/... ./internal/ext/hook/... ./internal/state/instruction/... ./internal/ext/mcplaunch/... ./internal/platform/notify/... ./internal/base/proc/... ./internal/platform/remote/... ./internal/platform/repair/... ./internal/safety/sandbox/... ./internal/base/sysproxy/... ./internal/state/workspacelease/... ./cmd/...
|
||||
run: go test -count=1 -p 4 -timeout=8m ./internal/runtime/agent/... ./internal/platform/appidentity/... ./internal/assembly/boot/... ./internal/state/checkpoint/... ./internal/frontend/cli/... ./internal/session/control/... ./internal/base/filelock/... ./internal/base/fileutil/... ./internal/ext/hook/... ./internal/state/instruction/... ./internal/ext/mcplaunch/... ./internal/platform/notify/... ./internal/base/proc/... ./internal/platform/remote/... ./internal/platform/repair/... ./internal/safety/sandbox/... ./internal/base/sysproxy/... ./internal/state/workspacelease/... ./cmd/...
|
||||
|
||||
# The general Windows PR smoke list intentionally omits internal/contract/tool.
|
||||
# Keep the session-temp portability contract covered without widening the
|
||||
@@ -218,7 +214,7 @@ jobs:
|
||||
timeout-minutes: 5
|
||||
run: go test -count=1 -timeout=3m -run '^TestBashPowerShellExecuteDetailedContract$|^TestBashPowerShell51PreflightRejectsAndAndDetailed$|^TestBashPowerShellOutputIsUTF8$|^TestBashPowerShellSurfacesNonZeroExit$|^TestBashPowerShellRejectsChaining$' ./internal/tools/builtin
|
||||
|
||||
# Full push suite still uses the same 12m package budget as Windows smoke:
|
||||
# Full push suite still uses the same 8m package budget as Windows smoke:
|
||||
# agent/boot/control already hit 180s package timeouts under runner load,
|
||||
# so 3m fails on slow machines without a code regression. It runs with
|
||||
# -count=1 for the reason given at the smoke step. A hang still dies here
|
||||
@@ -234,7 +230,7 @@ jobs:
|
||||
# Bound sandbox helper children in Windows tests so a failed OS-level
|
||||
# launch cannot pin the Actions step after Go's package timeout fires.
|
||||
WINDOWS_SANDBOX_WAIT_MS: "20000"
|
||||
run: go test -count=1 -p 4 -timeout=12m ./...
|
||||
run: go test -count=1 -p 4 -timeout=8m ./...
|
||||
|
||||
race:
|
||||
needs: changes
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
name: Deploy ops events relay
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: deploy-ops-events
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
if: github.ref == 'refs/heads/platform'
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: workers/ops-events/package-lock.json
|
||||
- name: Validate
|
||||
working-directory: workers/ops-events
|
||||
run: |
|
||||
npm ci
|
||||
npm run typecheck
|
||||
npm test
|
||||
- name: Deploy
|
||||
working-directory: workers/ops-events
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
run: npx wrangler deploy
|
||||
- name: Sync secrets
|
||||
working-directory: workers/ops-events
|
||||
env:
|
||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
GITHUB_WEBHOOK_SECRET: ${{ secrets.OPS_GITHUB_WEBHOOK_SECRET }}
|
||||
OPS_LISTEN_TOKEN: ${{ secrets.OPS_LISTEN_TOKEN }}
|
||||
OPS_EMIT_TOKEN: ${{ secrets.OPS_EMIT_TOKEN }}
|
||||
run: |
|
||||
node -e 'process.stdout.write(JSON.stringify({GITHUB_WEBHOOK_SECRET:process.env.GITHUB_WEBHOOK_SECRET||null,OPS_LISTEN_TOKEN:process.env.OPS_LISTEN_TOKEN||null,OPS_EMIT_TOKEN:process.env.OPS_EMIT_TOKEN||null}))' | npx wrangler secret bulk
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
|
||||
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
|
||||
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
|
||||
with:
|
||||
version: 10
|
||||
run_install: false
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
name: Sync in-app feedback
|
||||
|
||||
# Turns feedback submitted from Reasonix Studio into issues and reports issue
|
||||
# state (in progress, fixed in a tag, won't fix, duplicate) back to the worker,
|
||||
# and mirrors the reporter's replies onto their issue as bot comments.
|
||||
# Text from the public reaches the script only through the worker's JSON and
|
||||
# the GitHub API, never through a shell command line or an expression.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '*/10 * * * *'
|
||||
- cron: '5 3 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: Print what would happen without creating issues or writing back
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
full:
|
||||
description: Re-read every open entry instead of only recently changed issues
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: feedback-sync
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
if: github.repository == 'esengine/DeepSeek-Reasonix'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 8
|
||||
steps:
|
||||
- name: Check secret
|
||||
id: gate
|
||||
env:
|
||||
FEEDBACK_ADMIN_TOKEN: ${{ secrets.FEEDBACK_ADMIN_TOKEN }}
|
||||
run: |
|
||||
if [ -z "$FEEDBACK_ADMIN_TOKEN" ]; then
|
||||
echo "::notice::FEEDBACK_ADMIN_TOKEN is not set; feedback sync skipped"
|
||||
echo "run=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "run=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
- if: steps.gate.outputs.run == 'true'
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
# git tag --contains needs full history; the tag API would cost a request per entry.
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- if: steps.gate.outputs.run == 'true'
|
||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Sync
|
||||
if: steps.gate.outputs.run == 'true'
|
||||
env:
|
||||
FEEDBACK_ADMIN_TOKEN: ${{ secrets.FEEDBACK_ADMIN_TOKEN }}
|
||||
# crash.reasonix.io answers runner IPs with a managed challenge; workers.dev serves only the converter's admin paths.
|
||||
FEEDBACK_BASE_URL: https://reasonix-crash-report.reasonix.workers.dev
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
|
||||
FULL: ${{ github.event.inputs.full || 'false' }}
|
||||
SCHEDULE: ${{ github.event.schedule }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
args=()
|
||||
if [ "$DRY_RUN" = "true" ]; then args+=(--dry-run); fi
|
||||
# The daily schedule sweeps everything the change window could have missed.
|
||||
if [ "$FULL" = "true" ] || [ "$SCHEDULE" = "5 3 * * *" ]; then args+=(--full); fi
|
||||
node scripts/feedback-sync.mjs "${args[@]}"
|
||||
@@ -757,18 +757,14 @@ jobs:
|
||||
- name: Cross-compile and archive
|
||||
env:
|
||||
VERSION: ${{ needs.resolve.outputs.version }}
|
||||
SHA: ${{ needs.resolve.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
mkdir -p dist
|
||||
built="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
pkg=reasonix/internal/tools/productdocs
|
||||
for target in darwin/amd64 darwin/arm64 linux/amd64 linux/arm64 windows/amd64 windows/arm64; do
|
||||
os="${target%/*}"; arch="${target#*/}"; ext=""
|
||||
[ "$os" = windows ] && ext=".exe"
|
||||
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build -trimpath \
|
||||
-ldflags "-s -w -X main.version=$VERSION -X main.gitCommit=${SHA:0:12} -X main.buildTimeUTC=$built -X $pkg.linkedVersion=$VERSION -X $pkg.linkedRevision=$SHA" \
|
||||
-o "reasonix$ext" ./cmd/reasonix
|
||||
-ldflags "-s -w -X main.version=$VERSION" -o "reasonix$ext" ./cmd/reasonix
|
||||
if [ "$os" = windows ]; then
|
||||
zip -qj "dist/reasonix-$os-$arch.zip" "reasonix$ext"
|
||||
else
|
||||
@@ -1111,125 +1107,50 @@ jobs:
|
||||
--cache-control "public, max-age=300, stale-if-error=86400"
|
||||
echo "Studio catalog -> $VERSION"
|
||||
|
||||
# A CLI channel that moves while 1.x still publishes would have two lines
|
||||
# writing it, so every CLI job waits for this check. It runs no repository code.
|
||||
cli-gate:
|
||||
name: require the 1.x line to be frozen
|
||||
needs: resolve
|
||||
if: vars.STUDIO_PUBLISHES_CLI == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions: {}
|
||||
steps:
|
||||
- name: Require CLI_PUBLISH_FROZEN
|
||||
env:
|
||||
CLI_PUBLISH_FROZEN: ${{ vars.CLI_PUBLISH_FROZEN }}
|
||||
run: |
|
||||
if [ "$CLI_PUBLISH_FROZEN" != true ]; then
|
||||
echo "::error::STUDIO_PUBLISHES_CLI is on but CLI_PUBLISH_FROZEN is not: freeze the 1.x line first, or two lines would write the CLI channels"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The v<version> tag the update pointer needs is created here, by the release
|
||||
# identity the 1.x line also uses, because only that identity may create v*
|
||||
# tags. This job checks out nothing and runs no repository code; the one step
|
||||
# that reads RELEASE_TAG_TOKEN is written inline and only runs on the studio
|
||||
# tag or branch, through the studio-release environment's ref policy.
|
||||
cli-tag:
|
||||
name: create the CLI tag
|
||||
needs: [resolve, publish, cli-gate]
|
||||
if: vars.STUDIO_PUBLISHES_CLI == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
environment: studio-release
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Create v<version> on the studio commit
|
||||
env:
|
||||
TAG_TOKEN: ${{ secrets.RELEASE_TAG_TOKEN }}
|
||||
RELEASE_TAG_ACTOR: ${{ vars.RELEASE_TAG_ACTOR }}
|
||||
READ_TOKEN: ${{ github.token }}
|
||||
CLI_PUBLISH_FROZEN: ${{ vars.CLI_PUBLISH_FROZEN }}
|
||||
STUDIO_TAG: ${{ needs.resolve.outputs.tag }}
|
||||
CLI_VERSION: ${{ needs.resolve.outputs.version }}
|
||||
APPROVED_SHA: ${{ needs.resolve.outputs.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
repo=esengine/DeepSeek-Reasonix
|
||||
stable_re='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$'
|
||||
preview_re='^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)-preview\.(0|[1-9][0-9]*)$'
|
||||
tag="v${CLI_VERSION#v}"
|
||||
if [[ ! "$tag" =~ $stable_re && ! "$tag" =~ $preview_re ]]; then
|
||||
echo "$tag publishes no CLI pointer; no tag is created"
|
||||
exit 0
|
||||
fi
|
||||
[ "$CLI_PUBLISH_FROZEN" = true ] || { echo "::error::CLI_PUBLISH_FROZEN must be true"; exit 1; }
|
||||
[ "$GITHUB_REPOSITORY" = "$repo" ] || { echo "::error::the release tag identity is only used in $repo"; exit 1; }
|
||||
case "$GITHUB_REF" in
|
||||
refs/tags/studio-v* | refs/heads/studio) ;;
|
||||
*) echo "::error::the release tag identity is only used on a studio tag or the studio branch, not $GITHUB_REF"; exit 1 ;;
|
||||
esac
|
||||
[[ "$APPROVED_SHA" =~ ^[0-9a-f]{40}$ ]] || { echo "::error::approved SHA is not a full commit SHA"; exit 1; }
|
||||
[[ "$STUDIO_TAG" == "studio-$CLI_VERSION" ]] || { echo "::error::studio tag does not match the version"; exit 1; }
|
||||
[ -n "$TAG_TOKEN" ] || { echo "::error::RELEASE_TAG_TOKEN is not configured; the default Actions token cannot create v* tags"; exit 1; }
|
||||
[[ "$RELEASE_TAG_ACTOR" =~ ^[A-Za-z0-9][A-Za-z0-9-]*$ ]] || { echo "::error::configure RELEASE_TAG_ACTOR with the tag publisher's login"; exit 1; }
|
||||
read_api() { GH_TOKEN="$READ_TOKEN" gh api "$@"; }
|
||||
write_api() { GH_TOKEN="$TAG_TOKEN" gh api "$@"; }
|
||||
actor="$(write_api user)"
|
||||
jq -e --arg login "$RELEASE_TAG_ACTOR" '.type == "User" and (.login | ascii_downcase) == ($login | ascii_downcase)' <<<"$actor" >/dev/null ||
|
||||
{ echo "::error::the release tag credential does not identify RELEASE_TAG_ACTOR"; exit 1; }
|
||||
[ "$(write_api "repos/$repo" --jq .permissions.push)" = true ] ||
|
||||
{ echo "::error::the release tag identity cannot push to $repo"; exit 1; }
|
||||
[ "$(read_api "repos/$repo/commits/$STUDIO_TAG" --jq .sha)" = "$APPROVED_SHA" ] ||
|
||||
{ echo "::error::$STUDIO_TAG does not resolve to $APPROVED_SHA"; exit 1; }
|
||||
case "$(read_api "repos/$repo/compare/studio...$APPROVED_SHA" --jq .status)" in
|
||||
behind | identical) ;;
|
||||
*) echo "::error::$APPROVED_SHA is not on studio history"; exit 1 ;;
|
||||
esac
|
||||
# Prints the commit the tag points at; status 1 only when the tag does not exist.
|
||||
tag_commit() {
|
||||
local ref kind sha
|
||||
ref="$(read_api "repos/$repo/git/ref/tags/$tag" --jq '.object.type + " " + .object.sha' 2>"$RUNNER_TEMP/tag.error")" || {
|
||||
grep -Eiq '404|Not Found' "$RUNNER_TEMP/tag.error" && return 1
|
||||
cat "$RUNNER_TEMP/tag.error" >&2
|
||||
exit 1
|
||||
}
|
||||
read -r kind sha <<<"$ref"
|
||||
[ "$kind" != tag ] || sha="$(read_api "repos/$repo/git/tags/$sha" --jq .object.sha)"
|
||||
echo "$sha"
|
||||
}
|
||||
if existing="$(tag_commit)"; then
|
||||
[ "$existing" = "$APPROVED_SHA" ] || { echo "::error::$tag already exists on $existing, not $APPROVED_SHA"; exit 1; }
|
||||
echo "$tag already points at $APPROVED_SHA"
|
||||
exit 0
|
||||
fi
|
||||
if ! write_api "repos/$repo/git/refs" -f "ref=refs/tags/$tag" -f "sha=$APPROVED_SHA" >/dev/null 2>"$RUNNER_TEMP/create.error"; then
|
||||
# A concurrent run may have created the same tag in the meantime.
|
||||
if existing="$(tag_commit)" && [ "$existing" = "$APPROVED_SHA" ]; then
|
||||
echo "$tag already points at $APPROVED_SHA"
|
||||
exit 0
|
||||
fi
|
||||
cat "$RUNNER_TEMP/create.error" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "created $tag at $APPROVED_SHA"
|
||||
|
||||
# The CLI's install channels, npm's `reasonix` and the Homebrew tap. The 1.x
|
||||
# line owns them until this one takes the CLI over; the repository variable
|
||||
# STUDIO_PUBLISHES_CLI=true hands them here. They run once cli-pointer has
|
||||
# created the CLI release, because the cask points at its archives. A candidate
|
||||
# goes to npm's `next` tag and leaves the tap alone: brew has no prerelease
|
||||
# channel, so a candidate cask would become the default install.
|
||||
# STUDIO_PUBLISHES_CLI=true hands them here. They run once the release
|
||||
# exists, because the cask points at its archives. A candidate goes to npm's
|
||||
# `next` tag and leaves the tap alone: brew has no prerelease channel, so a
|
||||
# candidate cask would become the default install.
|
||||
cli-channels:
|
||||
name: publish the CLI to npm and Homebrew
|
||||
needs: [resolve, publish, cli-gate, cli-tag, cli-pointer]
|
||||
needs: [resolve, publish]
|
||||
if: vars.STUDIO_PUBLISHES_CLI == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
# Owner approval before anything reaches npm or the tap: the switch alone
|
||||
# would otherwise publish on every studio tag.
|
||||
environment: studio-cli-channels
|
||||
# id-token lets npm publish --provenance attach a Sigstore attestation; the
|
||||
# registry token still authenticates the publish itself.
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
# A job naming a missing environment gets an unprotected one, so the
|
||||
# gate is verified rather than assumed; the tokens below exist only in it.
|
||||
- name: Require the approval gate on studio-cli-channels
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
REPO: ${{ github.repository }}
|
||||
run: |
|
||||
set -uo pipefail
|
||||
resp="$(gh api -i "repos/$REPO/environments/studio-cli-channels" 2>&1)"
|
||||
status="$(printf '%s\n' "$resp" | sed -n '1s/^HTTP\/[0-9.]* \([0-9]*\).*/\1/p')"
|
||||
case "$status" in
|
||||
200) ;;
|
||||
404)
|
||||
echo "::error::environment studio-cli-channels does not exist"
|
||||
exit 1 ;;
|
||||
*)
|
||||
echo "::error::reading environment studio-cli-channels as ${GITHUB_ACTOR} failed with HTTP ${status:-unknown}"
|
||||
exit 1 ;;
|
||||
esac
|
||||
reviewers="$(printf '%s\n' "$resp" | sed '1,/^\r\{0,1\}$/d' | jq '[.protection_rules[] | select(.type == "required_reviewers") | .reviewers | length] | add // 0')"
|
||||
if ! [[ "$reviewers" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "::error::environment studio-cli-channels has no required reviewers (or its protection rules were unreadable: '${reviewers}'); refusing to publish"
|
||||
exit 1
|
||||
fi
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ needs.resolve.outputs.sha }}
|
||||
@@ -1243,7 +1164,7 @@ jobs:
|
||||
registry-url: https://registry.npmjs.org
|
||||
- name: Publish to npm
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.STUDIO_NPM_TOKEN }}
|
||||
VERSION: ${{ needs.resolve.outputs.version }}
|
||||
run: node npm/build.mjs "$VERSION" --publish
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
@@ -1254,24 +1175,12 @@ jobs:
|
||||
- name: Update the Homebrew cask
|
||||
if: needs.resolve.outputs.candidate != 'true'
|
||||
env:
|
||||
TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
TAP_TOKEN: ${{ secrets.STUDIO_HOMEBREW_TAP_TOKEN }}
|
||||
VERSION: ${{ needs.resolve.outputs.version }}
|
||||
TAG: ${{ needs.resolve.outputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git clone --depth 1 "https://x-access-token:${TAP_TOKEN}@github.com/esengine/homebrew-reasonix.git" tap
|
||||
decision="$(node scripts/brew-cask-guard.mjs tap/Casks/reasonix.rb "${VERSION#v}")"
|
||||
case "$decision" in
|
||||
write) ;;
|
||||
skip)
|
||||
echo "the tap's cask is newer than ${VERSION#v}; leaving it"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "::error::unexpected cask guard decision: '$decision'"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
bash scripts/render-brew-cask.sh "${VERSION#v}" "$TAG" cli-archives/SHA256SUMS > tap/Casks/reasonix.rb
|
||||
cd tap
|
||||
git add Casks/reasonix.rb
|
||||
@@ -1281,47 +1190,3 @@ jobs:
|
||||
fi
|
||||
git -c user.name=reasonix -c [email protected] commit -m "reasonix ${VERSION#v}"
|
||||
git push origin HEAD:main
|
||||
|
||||
# The 1.x updater accepts only a release tagged vMAJOR.MINOR.PATCH whose assets
|
||||
# sit under that tag, so the archives go to a v<version> release. Only a
|
||||
# -preview.N version moves preview; other prereleases publish nothing.
|
||||
cli-pointer:
|
||||
name: publish the CLI update pointer
|
||||
needs: [resolve, publish, cli-gate, cli-tag]
|
||||
if: vars.STUDIO_PUBLISHES_CLI == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: studio-cli-pointer-${{ contains(needs.resolve.outputs.version, '-') && 'preview' || 'stable' }}
|
||||
cancel-in-progress: false
|
||||
permissions:
|
||||
contents: write # create the CLI release; cli-tag has already made its tag
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
ref: ${{ needs.resolve.outputs.sha }}
|
||||
persist-credentials: false
|
||||
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: cli-archives
|
||||
path: cli-archives
|
||||
- name: Publish the release, its record and the pointer
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
CLI_VERSION: ${{ needs.resolve.outputs.version }}
|
||||
CLI_PUBLISH_FROZEN: ${{ vars.CLI_PUBLISH_FROZEN }}
|
||||
APPROVED_SHA: ${{ needs.resolve.outputs.sha }}
|
||||
STUDIO_TAG: ${{ needs.resolve.outputs.tag }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
ARCHIVES_DIR: cli-archives
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||
AWS_DEFAULT_REGION: auto
|
||||
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
||||
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
||||
HAS_R2: ${{ secrets.R2_ACCESS_KEY_ID != '' && secrets.R2_SECRET_ACCESS_KEY != '' && secrets.R2_ACCOUNT_ID != '' && secrets.R2_BUCKET != '' }}
|
||||
run: |
|
||||
if [ "$HAS_R2" != "true" ]; then
|
||||
echo "::error::R2 credentials are not configured; the CLI pointer was not written"
|
||||
exit 1
|
||||
fi
|
||||
bash scripts/publish-cli-pointer.sh
|
||||
@@ -15,7 +15,6 @@
|
||||
/runtime-resume
|
||||
/extension-protocol-gen
|
||||
/reasonix-plugin-example
|
||||
/mcp-line-counter-kit
|
||||
/reasonix-studio-host
|
||||
/signpath-contract
|
||||
/studio-delta
|
||||
|
||||
@@ -18,8 +18,6 @@
|
||||
<a href="https://esengine.github.io/DeepSeek-Reasonix/">Website</a>
|
||||
·
|
||||
<strong><a href="https://discord.gg/XF78rEME2D">Discord</a></strong>
|
||||
·
|
||||
<strong><a href="https://qm.qq.com/q/i59b0z2R8s">QQ</a></strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -33,7 +31,6 @@
|
||||
<a href="https://github.com/esengine/DeepSeek-Reasonix/graphs/contributors"><img src="https://img.shields.io/github/contributors/esengine/DeepSeek-Reasonix.svg?style=flat-square&color=bc8cff&labelColor=161b22&logo=github&logoColor=white" alt="contributors"/></a>
|
||||
<a href="https://github.com/esengine/DeepSeek-Reasonix/discussions"><img src="https://img.shields.io/github/discussions/esengine/DeepSeek-Reasonix.svg?style=flat-square&color=58a6ff&labelColor=161b22&logo=github&logoColor=white" alt="Discussions"/></a>
|
||||
<a href="https://discord.gg/XF78rEME2D"><img src="https://img.shields.io/badge/discord-join-5865F2.svg?style=flat-square&labelColor=161b22&logo=discord&logoColor=white" alt="Discord"/></a>
|
||||
<a href="https://qm.qq.com/q/i59b0z2R8s"><img src="https://img.shields.io/badge/QQ%20group-1093562660-12B7F5.svg?style=flat-square&labelColor=161b22&logo=tencentqq&logoColor=white" alt="QQ group 1093562660"/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -44,16 +41,12 @@
|
||||
<br/>
|
||||
|
||||
<p align="center"><strong>Open source · MIT · a single Go binary</strong></p>
|
||||
<h3 align="center">A reliable coding agent for complex software engineering tasks.</h3>
|
||||
<h3 align="center">A coding agent you can leave running.</h3>
|
||||
<p align="center">One local engine, four ways in — terminal, desktop app, browser, or your editor over ACP. Plan mode, permissions, a workspace sandbox and per-turn checkpoints keep a long autonomous run something you can still read and undo.</p>
|
||||
<p align="center">Maintained by <strong>Huahui Yu</strong>.</p>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Community · 加入社区** — bilingual Discord for setup help (`#help` / `#求助`), workflow showcases, and feature ideas. → **<https://discord.gg/XF78rEME2D>**
|
||||
>
|
||||
> QQ group **DeepSeek-Reasonix官方群** (`1093562660`) → **<https://qm.qq.com/q/i59b0z2R8s>** ([QR code](./docs/assets/qq-group.svg))
|
||||
|
||||
> Douyin: **做游戏的小鱼** (Douyin ID `22703872788`) · [QR code](./docs/assets/douyin.png)
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -272,9 +265,8 @@ coffee, not a contract — donations don't buy feature priority or change how
|
||||
issues get triaged.
|
||||
|
||||
- **International** — PayPal: [paypal.me/yuhuahui](https://paypal.me/yuhuahui)
|
||||
- **国内** — 微信支付 / 支付宝(扫码)
|
||||
- **国内** — 微信支付(扫码)
|
||||
|
||||
<p align="center">
|
||||
<img src=".github/sponsor/wechat-pay.jpg" alt="WeChat Pay QR code" width="180"/>
|
||||
<img src=".github/sponsor/alipay.jpg" alt="Alipay QR code" width="180"/>
|
||||
</p>
|
||||
@@ -18,8 +18,6 @@
|
||||
<a href="https://esengine.github.io/DeepSeek-Reasonix/">官方网站</a>
|
||||
·
|
||||
<strong><a href="https://discord.gg/XF78rEME2D">Discord</a></strong>
|
||||
·
|
||||
<strong><a href="https://qm.qq.com/q/i59b0z2R8s">QQ</a></strong>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -33,7 +31,6 @@
|
||||
<a href="https://github.com/esengine/DeepSeek-Reasonix/graphs/contributors"><img src="https://img.shields.io/github/contributors/esengine/DeepSeek-Reasonix.svg?style=flat-square&color=bc8cff&labelColor=161b22&logo=github&logoColor=white" alt="contributors"/></a>
|
||||
<a href="https://github.com/esengine/DeepSeek-Reasonix/discussions"><img src="https://img.shields.io/github/discussions/esengine/DeepSeek-Reasonix.svg?style=flat-square&color=58a6ff&labelColor=161b22&logo=github&logoColor=white" alt="Discussions"/></a>
|
||||
<a href="https://discord.gg/XF78rEME2D"><img src="https://img.shields.io/badge/discord-join-5865F2.svg?style=flat-square&labelColor=161b22&logo=discord&logoColor=white" alt="Discord"/></a>
|
||||
<a href="https://qm.qq.com/q/i59b0z2R8s"><img src="https://img.shields.io/badge/QQ%20group-1093562660-12B7F5.svg?style=flat-square&labelColor=161b22&logo=tencentqq&logoColor=white" alt="QQ group 1093562660"/></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -44,16 +41,12 @@
|
||||
<br/>
|
||||
|
||||
<p align="center"><strong>开源 · MIT · 单个 Go 二进制</strong></p>
|
||||
<h3 align="center">面向复杂软件工程任务的可靠编码 Agent。</h3>
|
||||
<h3 align="center">可以一直开着跑的编码 Agent。</h3>
|
||||
<p align="center">一套本地引擎,四个入口——终端、桌面端、浏览器,或通过 ACP 接入你的编辑器。计划模式、权限、工作区沙箱与逐轮 checkpoint,让长时间自治运行始终可读、可撤销。</p>
|
||||
<p align="center">维护者:<strong>Huahui Yu</strong>。</p>
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **加入社区 · Community** — 双语 Discord,提供安装答疑(`#help` / `#求助`)、工作流展示与功能想法。→ **<https://discord.gg/XF78rEME2D>**
|
||||
>
|
||||
> QQ 群 **DeepSeek-Reasonix官方群**(群号 `1093562660`)→ **<https://qm.qq.com/q/i59b0z2R8s>**([二维码](./docs/assets/qq-group.svg))
|
||||
|
||||
> 抖音:**做游戏的小鱼**(抖音号 `22703872788`)· [二维码](./docs/assets/douyin.png)
|
||||
|
||||
## 版本
|
||||
|
||||
@@ -252,10 +245,9 @@ CLI 进阶用法和详细配置见 **[CLI 命令参考](./docs/CLI.zh-CN.md)**
|
||||
如果 Reasonix 帮你省了时间或 token,欢迎请杯咖啡。捐助不会换来 feature
|
||||
优先级,也不会影响 issue 的处理顺序——就是「谢谢」。
|
||||
|
||||
- **国内** — 微信支付 / 支付宝(扫下方二维码)
|
||||
- **国内** — 微信支付(扫下方二维码)
|
||||
- **海外** — PayPal: [paypal.me/yuhuahui](https://paypal.me/yuhuahui)
|
||||
|
||||
<p align="center">
|
||||
<img src=".github/sponsor/wechat-pay.jpg" alt="微信支付收款码" width="180"/>
|
||||
<img src=".github/sponsor/alipay.jpg" alt="支付宝收款码" width="180"/>
|
||||
</p>
|
||||
@@ -320,31 +320,6 @@ Use `go test ./path/to/target/` to detect cycles **before** pushing. A `[setup f
|
||||
- **Keep the PR diff minimal.** Only the files relevant to the PR's purpose — no stray changes from other branches.
|
||||
- **Amend, don't add commits, for review feedback** — keeps the commit history clean.
|
||||
|
||||
## Change discipline
|
||||
|
||||
**Assess before editing.** For every change, humans and agents list callers,
|
||||
consumers of the same state, sibling features, reload/replay/restart paths,
|
||||
error and edge states, TUI/CLI and desktop equivalents, and wire mirrors.
|
||||
|
||||
A fix that breaks a neighbouring behaviour is a failed fix.
|
||||
|
||||
**Test the neighbourhood.** UI changes add or extend interaction tests for
|
||||
adjacent functions of the same component or flow (composer: submit, clear,
|
||||
queue, withdraw, restore, steer, failure, replay after reload) and run the whole
|
||||
affected suite.
|
||||
|
||||
Write tests first; they must fail without the change.
|
||||
|
||||
**Fix at the owning layer.** Use the existing unified pattern: no special-case
|
||||
flags, duplicated paths, or workarounds at the symptom site.
|
||||
|
||||
If the design is duplicated or wrong, make the smallest unifying change:
|
||||
one path instead of two, deleting what becomes dead. Use current language and
|
||||
framework idioms; do not add a new pattern where one exists.
|
||||
|
||||
**PR evidence.** Every PR description carries: Cause, Blast radius,
|
||||
Neighbouring behaviours tested, and Why this layer.
|
||||
|
||||
## Reasonix host checks
|
||||
|
||||
The paths below decide what the agent is allowed to do — a wrong change here
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
Two questions about long-horizon memory, and the harness that makes their
|
||||
answers trustworthy.
|
||||
|
||||
-mode=preflight build all 18 tasks under every arm and check the contract
|
||||
-mode=preflight build all 12 tasks under every arm and check the contract
|
||||
-mode=snippet does a rank-1 hit hand over the whole answer?
|
||||
-mode=adversarial send a model after the answer through every host surface
|
||||
-mode=run-search Experiment R: is searchable canonical a real capability?
|
||||
-mode=run-boundary Experiment I, paired: the 6 efficiency tasks across their boundary
|
||||
-mode=run-index Experiment I, full: the 6 efficiency tasks x 4 budgets
|
||||
-mode=run-boundary Experiment I, paired: 6 tasks across their own cue boundary
|
||||
-mode=run-index Experiment I, full: 6 tasks x 4 index budgets
|
||||
-mode=calibrate find each index task's PlantAfterGen for its cue tier
|
||||
|
||||
`-dry` drives any run mode with a scripted provider, for nothing. Real runs need
|
||||
@@ -31,8 +31,8 @@ The most likely reason is that two of the six index tasks carry heavy-tail
|
||||
stopping behaviour that swamps the effect being measured.
|
||||
|
||||
Existing evidence is not sufficient to tune the shipped 1% budget in either
|
||||
direction. Reopening this wants a new corpus, not more samples: that substrate
|
||||
now exists, two qualifying tasks per cue tier — see "Index corpus v2" below.
|
||||
direction. Reopening this wants a new corpus, not more samples: see
|
||||
"Index corpus v2" below.
|
||||
|
||||
**Things that turned out not to be problems**, each after being measured rather
|
||||
than argued about:
|
||||
@@ -74,83 +74,11 @@ scanned for answer literals before the first provider request and again after
|
||||
the turn. Any answer reaching the model through a tool that is not `recall`
|
||||
marks the run contaminated and takes it out of every statistic.
|
||||
|
||||
## Index corpus v2
|
||||
## Index corpus v2, if this is reopened
|
||||
|
||||
The efficiency substrate is six tasks that hold on both arms: `i37-dispatchlag`
|
||||
and `i118-fencelag` (quarter), `i106-budgetseal` and `i70-ingestfloor` (half),
|
||||
`i47-tenantlease` and `i112-lagquota` (default).
|
||||
|
||||
They came out of five batches of candidates, each screened the same way: one
|
||||
`-mode=run-index` pass over the batch, then two more only for the candidates that
|
||||
were clean on both boundary arms, and only the candidates that held on both arms
|
||||
in all three passes stayed. A candidate that does not hold is dropped, not
|
||||
promoted. The numbers are candidates, per tier:
|
||||
|
||||
| batch | quarter | half | default |
|
||||
| --- | --- | --- | --- |
|
||||
| first pool, `i31`-`i54` | 1 of 8 | 0 of 8 | 2 of 8 |
|
||||
| second pool, `i61`-`i84` | 1 of 4 screened | 4 of 8 | 0 of 8 |
|
||||
| third pool, `i85`-`i108` | 2 of 8 | 8 of 16 | none authored |
|
||||
| replacement pool, `i109`-`i116` | - | - | 4 of 8 |
|
||||
| replacement pool, `i92`, `i117`-`i124` | 3 of 9 | - | - |
|
||||
|
||||
That table counts candidates that held on both arms in all three passes, which is
|
||||
not the size of the substrate. 89 candidates were authored, 77 were screened (the
|
||||
second pool's quarter tier stopped at 4 of 8 and its default tier was never run),
|
||||
and 25 held: 7 quarter, 12 half, 6 default. The substrate keeps two per tier, so
|
||||
the other 19 survivors are spares rather than corpus members. Candidates from the
|
||||
earlier round are not counted here.
|
||||
|
||||
| task | tier | cue present | cue absent |
|
||||
| --- | --- | ---: | ---: |
|
||||
| `i37-dispatchlag` | quarter | 3/3 | 3/3 |
|
||||
| `i118-fencelag` | quarter | 3/3 | 3/3 |
|
||||
| `i106-budgetseal` | half | 3/3 | 3/3 |
|
||||
| `i70-ingestfloor` | half | 3/3 | 3/3 |
|
||||
| `i47-tenantlease` | default | 3/3 | 3/3 |
|
||||
| `i112-lagquota` | default | 3/3 | 3/3 |
|
||||
|
||||
Across their 36 judged cells: `SnippetStop` 23, `SnippetThenRead` 10 and
|
||||
`DefensiveRead` 3 - every cell clean, nothing post-sufficient and nothing
|
||||
`NeverSufficient`.
|
||||
|
||||
Four of the first six were flagged in review for sharing answer values, probe
|
||||
queries and prompt wording within a tier. Separating them and re-screening those
|
||||
four changed two of them: `i37` and `i47` held, `i62` and `i51` did not, so they
|
||||
were replaced - `i90-recoverylag` was tried first and did not hold either, and
|
||||
`i118-fencelag` and `i112-lagquota` are what did. The separation was not
|
||||
cosmetic: the shared wording had been carrying two of the six.
|
||||
|
||||
Nothing in the task shape separates the survivors from the rest: within the half
|
||||
tier, plain-number answers survived 0 of 8 in the first pool and 4 of 8 in the
|
||||
second, and codename answers survived 8 of 16 in the third. An earlier claim here
|
||||
that the answer kind decides whether a candidate holds does not survive this
|
||||
table and is withdrawn. The dirty class was `PostSufficientRetrieval` in most
|
||||
failures - the model re-checking a value it already had - but which tasks hit it
|
||||
was not predictable from their shape at this sample.
|
||||
|
||||
`i01`-`i06` are the frozen study's six tasks, kept as `roleStopping`. Measured
|
||||
once each with `-mode=run-index -task <id>`, per task and per arm:
|
||||
|
||||
| task | tier | cue present | cue absent |
|
||||
| --- | --- | --- | --- |
|
||||
| `i01-transport-fallback` | quarter | SnippetStop | DefensiveRead |
|
||||
| `i02-scheduler-handoff` | quarter | PostSufficientRetrieval | PostSufficientRetrieval |
|
||||
| `i03-coalescing` | half | PostSufficientRetrieval | PostSufficientRunaway |
|
||||
| `i04-recovery-fence` | half | DefensiveRead | PostSufficientRunaway |
|
||||
| `i05-cache-probe` | default | SnippetThenRead | PostSufficientRetrieval |
|
||||
| `i06-dispatch-handoff` | default | SnippetStop | PostSufficientRetrieval |
|
||||
|
||||
Five of the six show post-sufficient behaviour in at least one arm (`i02`-`i06`),
|
||||
and `i01` is clean on both. The cue-present arm is clean for four of them
|
||||
(`i01`, `i04`, `i05`, `i06`), which is why they stay in the corpus as the
|
||||
stopping side rather than being retitled. All six are excluded from `run-index`
|
||||
and `run-boundary` batches and stay measurable one at a time with `-task <id>`. A
|
||||
task added to the corpus declares a role, so it cannot arrive without one.
|
||||
|
||||
A task qualifies only if it is clean on both of its boundary arms - the
|
||||
cue-present run and the cue-absent run - in every run of a screening batch. Clean
|
||||
means a `stopping_class` other than `PostSufficientRetrieval` or
|
||||
The present index tasks are not a fit substrate: two of six carry stopping
|
||||
behaviour that dominates the signal. A task should qualify only if a run-index
|
||||
run reports a `stopping_class` other than `PostSufficientRetrieval` or
|
||||
`PostSufficientRunaway`: `SnippetStop`, `SnippetThenRead` and `DefensiveRead`
|
||||
all qualify, and a defensive read is deliberately not counted as waste. The
|
||||
class comes from the three counts in that report — `searches_after_sufficient`,
|
||||
@@ -158,7 +86,8 @@ class comes from the three counts in that report — `searches_after_sufficient`
|
||||
sufficiency was reached and how many rounds followed it. The remaining
|
||||
requirements still apply — successful tool call, answer in the output rather
|
||||
than the cue, rank 1, `FirstHitCoverage` complete, one or two simple values,
|
||||
nothing in the workspace.
|
||||
nothing in the workspace. Keep i03 and i04 — as a stopping corpus, not an
|
||||
efficiency one.
|
||||
|
||||
`-mode=calibrate` is not a screening tool: it builds and reads back the
|
||||
fixtures, and reads nothing else, so its cue-visibility profile is the same for
|
||||
|
||||
@@ -26,12 +26,6 @@ const (
|
||||
tierDefault = "default" // visible at default only
|
||||
)
|
||||
|
||||
// role places an index task in the efficiency substrate or the stopping corpus.
|
||||
const (
|
||||
roleEfficiency = "efficiency"
|
||||
roleStopping = "stopping"
|
||||
)
|
||||
|
||||
// plantKind is how a task's target enters the transcript.
|
||||
const (
|
||||
plantAssistant = "assistant" // the model's own account: never index-addressed
|
||||
@@ -51,8 +45,6 @@ type contextTask struct {
|
||||
AnswerVars []string
|
||||
CueVar string
|
||||
CueTier string
|
||||
// Role is roleEfficiency or roleStopping; it decides the batch a task enters.
|
||||
Role string
|
||||
// PlantAfterGen ages the target: mergeFoldIndex trims from its oldest end,
|
||||
// so what follows a cue decides which budget addresses it. Frozen, because
|
||||
// a run that searched for it would let the tested code move its goalposts.
|
||||
@@ -129,20 +121,6 @@ func num(name string, lo, hi int) varSpec {
|
||||
return varSpec{Name: name, Kind: varInt, Min: lo, Max: hi}
|
||||
}
|
||||
|
||||
// efficiencyTasks is the substrate a study measures on. Every task in it
|
||||
// qualifies under the corpus bar; the stopping corpus is deliberately excluded,
|
||||
// because a task carrying post-sufficient stopping behaviour swamps the effect
|
||||
// an efficiency batch exists to measure.
|
||||
func efficiencyTasks() []contextTask {
|
||||
var out []contextTask
|
||||
for _, t := range indexTasks() {
|
||||
if t.Role == roleEfficiency {
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func searchTasks() []contextTask {
|
||||
return []contextTask{
|
||||
{
|
||||
@@ -214,104 +192,10 @@ func searchTasks() []contextTask {
|
||||
}
|
||||
}
|
||||
|
||||
// efficiencyIndexTasks is what a study measures on: two tasks per cue tier.
|
||||
func efficiencyIndexTasks() []contextTask {
|
||||
out := indexQuarterTasks()
|
||||
out = append(out, indexHalfTasks()...)
|
||||
out = append(out, indexDefaultTasks()...)
|
||||
return out
|
||||
}
|
||||
|
||||
// indexQuarterTasks holds the quarter-tier literals.
|
||||
func indexQuarterTasks() []contextTask {
|
||||
func indexTasks() []contextTask {
|
||||
return []contextTask{
|
||||
{
|
||||
ID: "i37-dispatchlag", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleEfficiency,
|
||||
Prompt: "早前会话里执行过 ./scripts/dispatchlag,它打印出的 dispatch lag epoch 是多少?",
|
||||
ProbeQuery: "dispatchlag dispatch lag epoch",
|
||||
Vars: []varSpec{code("cue", "dispatchlag"), code("epoch", "cobalt")},
|
||||
AnswerVars: []string{"epoch"}, CueVar: "cue", CueTier: tierQuarter,
|
||||
PlantAfterGen: 4,
|
||||
Plant: plantCall, CallID: "p37", CallTool: "bash",
|
||||
CallArgs: `{"command":"./scripts/dispatchlag {{cue}}"}`,
|
||||
PlantBody: "dispatch lag epoch = {{epoch}}",
|
||||
},
|
||||
{
|
||||
ID: "i118-fencelag", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleEfficiency,
|
||||
Prompt: "早前会话里运行过 ./scripts/fencelag,它打印出的 fence lag 是多少?",
|
||||
ProbeQuery: "fencelag fence lag",
|
||||
Vars: []varSpec{code("cue", "fencelag"), code("v", "slate")},
|
||||
AnswerVars: []string{"v"}, CueVar: "cue", CueTier: tierQuarter,
|
||||
PlantAfterGen: 4,
|
||||
Plant: plantCall, CallID: "x118", CallTool: "bash",
|
||||
CallArgs: `{"command":"./scripts/fencelag {{cue}}"}`,
|
||||
PlantBody: "fence lag = {{v}}",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// indexHalfTasks holds the half-tier literals.
|
||||
func indexHalfTasks() []contextTask {
|
||||
return []contextTask{
|
||||
{
|
||||
ID: "i70-ingestfloor", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleEfficiency,
|
||||
Prompt: "早前会话里执行过 ./scripts/ingestfloor,它打印出的 route quota 是多少?",
|
||||
ProbeQuery: "ingestfloor quota",
|
||||
Vars: []varSpec{code("cue", "ingestfloor"), num("quota", 1200, 9800)},
|
||||
AnswerVars: []string{"quota"}, CueVar: "cue", CueTier: tierHalf,
|
||||
PlantAfterGen: 2,
|
||||
Plant: plantCall, CallID: "q70", CallTool: "bash",
|
||||
CallArgs: `{"command":"./scripts/ingestfloor {{cue}}"}`,
|
||||
PlantBody: "route quota = {{quota}}",
|
||||
},
|
||||
{
|
||||
ID: "i106-budgetseal", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleEfficiency,
|
||||
Prompt: "早前会话里执行过 ./scripts/budgetseal,它打印出的 route quota 是多少?",
|
||||
ProbeQuery: "budgetseal route quota",
|
||||
Vars: []varSpec{code("cue", "budgetseal"), code("quota", "amber")},
|
||||
AnswerVars: []string{"quota"}, CueVar: "cue", CueTier: tierHalf,
|
||||
PlantAfterGen: 2,
|
||||
Plant: plantCall, CallID: "h106", CallTool: "bash",
|
||||
CallArgs: `{"command":"./scripts/budgetseal {{cue}}"}`,
|
||||
PlantBody: "route quota = {{quota}}",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// indexDefaultTasks holds the default-tier literals.
|
||||
func indexDefaultTasks() []contextTask {
|
||||
return []contextTask{
|
||||
{
|
||||
ID: "i47-tenantlease", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleEfficiency,
|
||||
Prompt: "早前会话里执行过 ./scripts/tenantlease,它打印出的 tenant lease(秒)是多少?",
|
||||
ProbeQuery: "tenantlease tenant lease",
|
||||
Vars: []varSpec{code("cue", "tenantlease"), {Name: "lease", Kind: varDuration, Min: 15, Max: 300}},
|
||||
AnswerVars: []string{"lease"}, CueVar: "cue", CueTier: tierDefault,
|
||||
PlantAfterGen: 0,
|
||||
Plant: plantCall, CallID: "p47", CallTool: "bash",
|
||||
CallArgs: `{"command":"./scripts/tenantlease {{cue}}"}`,
|
||||
PlantBody: "tenant lease = {{lease}}",
|
||||
},
|
||||
{
|
||||
ID: "i112-lagquota", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleEfficiency,
|
||||
Prompt: "早前会话里执行过 ./scripts/lagquota,它打印出的 lagquota lease 是多少?",
|
||||
ProbeQuery: "lagquota lagquota lease",
|
||||
Vars: []varSpec{code("cue", "lagquota"), {Name: "lease", Kind: varDuration, Min: 20, Max: 420}},
|
||||
AnswerVars: []string{"lease"}, CueVar: "cue", CueTier: tierDefault,
|
||||
PlantAfterGen: 0,
|
||||
Plant: plantCall, CallID: "d112", CallTool: "bash",
|
||||
CallArgs: `{"command":"./scripts/lagquota {{cue}}"}`,
|
||||
PlantBody: "lagquota lease = {{lease}}",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// stoppingIndexTasks is the frozen study's six tasks, kept out of efficiency
|
||||
// batches and measurable on their own with -task <id>.
|
||||
func stoppingIndexTasks() []contextTask {
|
||||
return []contextTask{
|
||||
{
|
||||
ID: "i01-transport-fallback", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleStopping,
|
||||
ID: "i01-transport-fallback", Experiment: experimentIndex, TargetKind: "tool_input",
|
||||
Prompt: "transport fallback probe 当时报告的 selector 是什么?",
|
||||
ProbeQuery: "transport fallback probe selector",
|
||||
Vars: []varSpec{code("cue", "tfprobe"), code("selector", "fern")},
|
||||
@@ -322,7 +206,7 @@ func stoppingIndexTasks() []contextTask {
|
||||
PlantBody: "fallback selector = {{selector}}",
|
||||
},
|
||||
{
|
||||
ID: "i02-scheduler-handoff", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleStopping,
|
||||
ID: "i02-scheduler-handoff", Experiment: experimentIndex, TargetKind: "tool_input",
|
||||
Prompt: "scheduler handoff 当时记录的 grace 和 marker 是什么?",
|
||||
ProbeQuery: "scheduler handoff grace marker",
|
||||
Vars: []varSpec{
|
||||
@@ -336,7 +220,7 @@ func stoppingIndexTasks() []contextTask {
|
||||
PlantBody: "handoff grace = {{grace}}\nmarker = {{marker}}",
|
||||
},
|
||||
{
|
||||
ID: "i03-coalescing", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleStopping,
|
||||
ID: "i03-coalescing", Experiment: experimentIndex, TargetKind: "tool_input",
|
||||
Prompt: "job completion 的 coalescing probe 最后记录的 window 和 marker 是多少?",
|
||||
ProbeQuery: "coalescing policy completion window",
|
||||
Vars: []varSpec{
|
||||
@@ -350,7 +234,7 @@ func stoppingIndexTasks() []contextTask {
|
||||
PlantBody: "completion coalesce window = {{window}}\nmarker = {{marker}}",
|
||||
},
|
||||
{
|
||||
ID: "i04-recovery-fence", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleStopping,
|
||||
ID: "i04-recovery-fence", Experiment: experimentIndex, TargetKind: "tool_input",
|
||||
Prompt: "recovery fence 那次检查报告的 marker 和两个 epoch 是什么?",
|
||||
ProbeQuery: "recovery fence epoch check",
|
||||
Vars: []varSpec{
|
||||
@@ -364,7 +248,7 @@ func stoppingIndexTasks() []contextTask {
|
||||
PlantBody: "{{marker}}\nexpected epoch {{expected}}\nobserved epoch {{observed}}",
|
||||
},
|
||||
{
|
||||
ID: "i05-cache-probe", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleStopping,
|
||||
ID: "i05-cache-probe", Experiment: experimentIndex, TargetKind: "tool_input",
|
||||
Prompt: "cache probe 当时记录的 prefix salt 和 scope 是什么?",
|
||||
ProbeQuery: "cache probe prefix salt scope",
|
||||
Vars: []varSpec{code("cue", "cacheprobe"), code("salt", "comet"), code("scope", "lineage")},
|
||||
@@ -375,7 +259,7 @@ func stoppingIndexTasks() []contextTask {
|
||||
PlantBody: "prefix salt = {{salt}}\nscope = {{scope}}",
|
||||
},
|
||||
{
|
||||
ID: "i06-dispatch-handoff", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleStopping,
|
||||
ID: "i06-dispatch-handoff", Experiment: experimentIndex, TargetKind: "tool_input",
|
||||
Prompt: "dispatch handoff probe 当时确认的 mode 和 marker 是什么?",
|
||||
ProbeQuery: "dispatch handoff probe mode",
|
||||
Vars: []varSpec{code("cue", "dispatchhandoff"), code("mode", "trigger"), code("marker", "iris")},
|
||||
@@ -388,12 +272,6 @@ func stoppingIndexTasks() []contextTask {
|
||||
}
|
||||
}
|
||||
|
||||
// indexTasks is the whole index corpus, the efficiency substrate first.
|
||||
func indexTasks() []contextTask {
|
||||
out := efficiencyIndexTasks()
|
||||
return append(out, stoppingIndexTasks()...)
|
||||
}
|
||||
|
||||
func allTasks() []contextTask {
|
||||
return append(searchTasks(), indexTasks()...)
|
||||
}
|
||||
@@ -454,9 +332,6 @@ func validateCorpus() error {
|
||||
if t.CueVar == "" {
|
||||
return fmt.Errorf("%s: an index task needs a cue var", t.ID)
|
||||
}
|
||||
if t.Role != roleEfficiency && t.Role != roleStopping {
|
||||
return fmt.Errorf("%s: an index task needs a role (%s or %s)", t.ID, roleEfficiency, roleStopping)
|
||||
}
|
||||
if v, _ := tierScales(t.CueTier); v == nil {
|
||||
return fmt.Errorf("%s: unknown cue tier %q", t.ID, t.CueTier)
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
mathrand "math/rand"
|
||||
"strings"
|
||||
)
|
||||
@@ -91,25 +90,15 @@ func seededRand(parts ...string) *mathrand.Rand {
|
||||
// liveRand is the generator a paid run uses: every run gets values that existed
|
||||
// nowhere before it started.
|
||||
func liveRand() *mathrand.Rand {
|
||||
rng, err := liveRandFrom(rand.Reader)
|
||||
if err != nil {
|
||||
panic("contextbench: no entropy for a live run: " + err.Error())
|
||||
}
|
||||
return rng
|
||||
}
|
||||
|
||||
// liveRandFrom seeds a generator from an entropy source, so what a run's values
|
||||
// are unpredictable from is the source and nothing else.
|
||||
func liveRandFrom(entropy io.Reader) (*mathrand.Rand, error) {
|
||||
var seed [8]byte
|
||||
if _, err := io.ReadFull(entropy, seed[:]); err != nil {
|
||||
return nil, err
|
||||
if _, err := rand.Read(seed[:]); err != nil {
|
||||
panic("contextbench: no entropy for a live run: " + err.Error())
|
||||
}
|
||||
var n int64
|
||||
for _, b := range seed {
|
||||
n = n<<8 | int64(b)
|
||||
}
|
||||
return mathrand.New(mathrand.NewSource(n)), nil
|
||||
return mathrand.New(mathrand.NewSource(n))
|
||||
}
|
||||
|
||||
// instantiate substitutes {{name}} and refuses anything left over. The grammar
|
||||
|
||||
@@ -62,14 +62,7 @@ func main() {
|
||||
case "adversarial":
|
||||
os.Exit(runAdversarial(root))
|
||||
case "run-index":
|
||||
batch := tasks
|
||||
if *only == "" {
|
||||
// A batch measures the efficiency substrate only. Naming one task
|
||||
// with -task runs that task, role included, so the stopping corpus
|
||||
// stays measurable on its own.
|
||||
batch = efficiencyTasks()
|
||||
}
|
||||
os.Exit(runExperiment(experimentIndex, root, *dry, batch))
|
||||
os.Exit(runExperiment(experimentIndex, root, *dry, tasks))
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown mode %q\n", *mode)
|
||||
os.Exit(2)
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"maps"
|
||||
mathrand "math/rand"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -22,24 +19,16 @@ func TestCorpusIsWellFormed(t *testing.T) {
|
||||
if got := len(searchTasks()); got != 6 {
|
||||
t.Errorf("search experiment has %d tasks, want 6", got)
|
||||
}
|
||||
if got := len(indexTasks()); got != 12 {
|
||||
t.Errorf("index experiment has %d tasks, want 12", got)
|
||||
if got := len(indexTasks()); got != 6 {
|
||||
t.Errorf("index experiment has %d tasks, want 6", got)
|
||||
}
|
||||
stopping := map[string]bool{}
|
||||
tiers := map[string]int{}
|
||||
for _, task := range indexTasks() {
|
||||
if task.Role == roleStopping {
|
||||
stopping[task.ID] = true
|
||||
continue
|
||||
}
|
||||
tiers[task.CueTier]++
|
||||
}
|
||||
if !stopping["i03-coalescing"] || !stopping["i04-recovery-fence"] {
|
||||
t.Errorf("the stopping corpus is %v; it must keep i03-coalescing and i04-recovery-fence", stopping)
|
||||
}
|
||||
// Two per tier, over the efficiency substrate, is what makes the staircase
|
||||
// readable: the same task can be compared across the boundary its cue sits
|
||||
// on, instead of two different questions being compared across two arms.
|
||||
// Two per tier is what makes the staircase readable: the same task can be
|
||||
// compared across the boundary its cue sits on, instead of two different
|
||||
// questions being compared across two arms.
|
||||
for _, tier := range []string{tierQuarter, tierHalf, tierDefault} {
|
||||
if tiers[tier] != 2 {
|
||||
t.Errorf("tier %s has %d tasks, want 2", tier, tiers[tier])
|
||||
@@ -47,21 +36,6 @@ func TestCorpusIsWellFormed(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The role decides which batch a task enters; the literal lists are only where
|
||||
// it is declared, so the two must not drift.
|
||||
func TestIndexListsMatchTheirRoles(t *testing.T) {
|
||||
for _, task := range append(append(indexQuarterTasks(), indexHalfTasks()...), indexDefaultTasks()...) {
|
||||
if task.Role != roleEfficiency {
|
||||
t.Errorf("%s sits in an efficiency list with role %q", task.ID, task.Role)
|
||||
}
|
||||
}
|
||||
for _, task := range stoppingIndexTasks() {
|
||||
if task.Role != roleStopping {
|
||||
t.Errorf("%s sits in the stopping list with role %q", task.ID, task.Role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The invariant the whole instantiation layer exists for: no literal a scorer
|
||||
// needs may be readable in this repository before a run starts.
|
||||
func TestNoAnswerLiteralExistsInTheRepository(t *testing.T) {
|
||||
@@ -85,46 +59,20 @@ func TestNoAnswerLiteralExistsInTheRepository(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A run's values are unpredictable from a previous run only if the live
|
||||
// generator is seeded from entropy. Comparing answers across two instances
|
||||
// cannot assert that: a task whose answer is a bounded number collides by
|
||||
// chance (1/8601 to 1/286 per task), so the check is on the seed source.
|
||||
func TestLiveRandDrawsFromEntropy(t *testing.T) {
|
||||
a, b := liveRand(), liveRand()
|
||||
for range 4 {
|
||||
if a.Int63() != b.Int63() {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatal("two live generators produced the same stream: they are seeded alike")
|
||||
}
|
||||
|
||||
// The source alone decides an instance: the same entropy gives the same
|
||||
// answers, different entropy gives different values.
|
||||
func TestLiveRandSourceDecidesTheInstance(t *testing.T) {
|
||||
same := func() *mathrand.Rand {
|
||||
rng, err := liveRandFrom(bytes.NewReader(bytes.Repeat([]byte{7}, 8)))
|
||||
// Two runs of the same task must not share an answer, or one leaked run leaks
|
||||
// every future one.
|
||||
func TestInstancesDifferBetweenRuns(t *testing.T) {
|
||||
for _, task := range allTasks() {
|
||||
a, err := instantiateTask(task, liveRand())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return rng
|
||||
}
|
||||
other, err := liveRandFrom(bytes.NewReader(bytes.Repeat([]byte{9}, 8)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := liveRandFrom(bytes.NewReader(nil)); err == nil {
|
||||
t.Fatal("an empty entropy source was accepted")
|
||||
}
|
||||
for _, task := range allTasks() {
|
||||
a, _ := instantiateTask(task, same())
|
||||
b, _ := instantiateTask(task, same())
|
||||
if !slices.Equal(a.AnswerMarkers, b.AnswerMarkers) || !maps.Equal(a.Vars, b.Vars) {
|
||||
t.Errorf("%s: the same entropy gave different instances", task.ID)
|
||||
b, err := instantiateTask(task, liveRand())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c, _ := instantiateTask(task, other)
|
||||
if maps.Equal(a.Vars, c.Vars) {
|
||||
t.Errorf("%s: different entropy gave the same values", task.ID)
|
||||
if slices.Equal(a.AnswerMarkers, b.AnswerMarkers) {
|
||||
t.Errorf("%s produced the same answers twice: %v", task.ID, a.AnswerMarkers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,7 +232,7 @@ func runBoundaries(root string) int {
|
||||
|
||||
var all []contextMetrics
|
||||
var cueSide, noCueSide []contextMetrics
|
||||
for _, t := range efficiencyTasks() {
|
||||
for _, t := range indexTasks() {
|
||||
cue, noCue := boundaryPair(t.CueTier)
|
||||
for _, side := range []struct {
|
||||
scale string
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
def wire_fee(amount):
|
||||
"""Return the wire fee for `amount`: the greater of 15.0 and 0.1% of it.
|
||||
|
||||
A zero or negative amount has no wire fee, so it returns 0.0.
|
||||
"""
|
||||
if amount <= 0:
|
||||
return 0.0
|
||||
return max(15.0, amount * 0.001)
|
||||
@@ -1,13 +0,0 @@
|
||||
def convert(amount, rate):
|
||||
"""Convert `amount` at `rate` and return the product.
|
||||
|
||||
Both units belong to the caller. A negative amount converts unchanged.
|
||||
"""
|
||||
return amount * rate
|
||||
|
||||
def spread(bid, ask):
|
||||
"""Return `ask - bid`, the spread between the two quoted prices.
|
||||
|
||||
A negative result means the bid is above the ask.
|
||||
"""
|
||||
return ask - bid
|
||||
@@ -1,4 +0,0 @@
|
||||
EARTH_RADIUS_KM = 6371.0
|
||||
|
||||
def dist(left, right):
|
||||
return abs(left - right) * EARTH_RADIUS_KM
|
||||
@@ -1,4 +0,0 @@
|
||||
import geo
|
||||
|
||||
def near_by(point, other):
|
||||
return geo.dist(point, other) < 10
|
||||
@@ -1,16 +0,0 @@
|
||||
import json
|
||||
import sys
|
||||
|
||||
def upgrade(row):
|
||||
parts = row["fullname"].rsplit(" ", 1)
|
||||
out = {k: v for k, v in row.items() if k != "fullname"}
|
||||
out["first"] = parts[0]
|
||||
out["last"] = parts[1] if len(parts) > 1 else ""
|
||||
return out
|
||||
|
||||
def main():
|
||||
for line in sys.stdin:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
print(json.dumps(upgrade(json.loads(line))))
|
||||
@@ -1,7 +0,0 @@
|
||||
import logging
|
||||
|
||||
log = logging.getLogger("orders")
|
||||
|
||||
def place(order, request_id):
|
||||
log.info("placing order %s", order["id"], extra={"request_id": request_id})
|
||||
return {"ok": True}
|
||||
@@ -1,7 +0,0 @@
|
||||
import logging
|
||||
|
||||
log = logging.getLogger("shipping")
|
||||
|
||||
def dispatch(order, request_id):
|
||||
log.info("dispatching %s", order["id"], extra={"request_id": request_id})
|
||||
return {"ok": True}
|
||||
@@ -1,11 +0,0 @@
|
||||
import math
|
||||
|
||||
def p95(sorted_values):
|
||||
if not sorted_values:
|
||||
return 0
|
||||
return sorted_values[math.ceil(len(sorted_values) * 0.95) - 1]
|
||||
|
||||
def rate(events, seconds):
|
||||
if seconds <= 0:
|
||||
return 0.0
|
||||
return len(events) / seconds
|
||||
@@ -1,9 +0,0 @@
|
||||
import core
|
||||
|
||||
_cache = None
|
||||
|
||||
def list_records_cached():
|
||||
global _cache
|
||||
if _cache is None:
|
||||
_cache = core.list_records()
|
||||
return _cache
|
||||
@@ -1,5 +0,0 @@
|
||||
import cache
|
||||
|
||||
def main():
|
||||
for k, v in sorted(cache.list_records_cached().items()):
|
||||
print(f"{k}={v}")
|
||||
@@ -1,4 +0,0 @@
|
||||
DATA = {"a": 1, "b": 2}
|
||||
|
||||
def list_records():
|
||||
return dict(DATA)
|
||||
@@ -1 +0,0 @@
|
||||
DEFAULTS = {"host": "localhost", "port": 8080}
|
||||
@@ -1,6 +0,0 @@
|
||||
import json
|
||||
import defaults
|
||||
|
||||
def load(text):
|
||||
data = json.loads(text) if text.strip() else {}
|
||||
return dict(defaults.DEFAULTS, **data)
|
||||
@@ -1,4 +0,0 @@
|
||||
import json
|
||||
|
||||
def dump(cfg):
|
||||
return json.dumps(cfg, sort_keys=True)
|
||||
@@ -1,2 +0,0 @@
|
||||
STATES = ("new", "paid", "delivered", "cancelled")
|
||||
TERMINAL = ("delivered", "cancelled")
|
||||
@@ -1,11 +0,0 @@
|
||||
import states
|
||||
|
||||
LEGAL = {
|
||||
"new": ("paid", "cancelled"),
|
||||
"paid": ("delivered", "cancelled"),
|
||||
"delivered": (),
|
||||
"cancelled": (),
|
||||
}
|
||||
|
||||
def can_move(a, b):
|
||||
return b in LEGAL[a]
|
||||
@@ -1,5 +0,0 @@
|
||||
import schema, serializer
|
||||
|
||||
def display_name(line):
|
||||
rec = serializer.load(line)
|
||||
return rec["handle"] or rec["email"]
|
||||
@@ -1,4 +0,0 @@
|
||||
FIELDS = ("handle", "email")
|
||||
|
||||
def blank():
|
||||
return {f: "" for f in FIELDS}
|
||||
@@ -1,4 +0,0 @@
|
||||
import timings
|
||||
|
||||
def is_slow(entry):
|
||||
return entry["elapsed_s"] > timings.SLOW_SECONDS
|
||||
@@ -1,4 +0,0 @@
|
||||
import timings
|
||||
|
||||
def within_budget(entries):
|
||||
return sum(e["elapsed_s"] for e in entries) <= timings.BUDGET_SECONDS
|
||||
@@ -1,5 +0,0 @@
|
||||
SLOW_SECONDS = 2.0
|
||||
BUDGET_SECONDS = 60.0
|
||||
|
||||
def record(seconds):
|
||||
return {"elapsed_s": seconds}
|
||||
@@ -250,10 +250,10 @@ func TestCorpusGradersPassTheReferenceSolution(t *testing.T) {
|
||||
t.Parallel()
|
||||
work := stageSeed(t, task.dir)
|
||||
if !stageSolved(t, task.dir, work) {
|
||||
if dir == trainCorpusDir || dir == verificationStressDir {
|
||||
t.Fatal("no solution/: a corpus task must prove its grader can pass")
|
||||
if dir == trainCorpusDir {
|
||||
t.Fatal("no solution/: a generated task must prove its grader can pass")
|
||||
}
|
||||
t.Skip("no solution/: the e2e suite does not commit reference solutions")
|
||||
t.Skip("no solution/ to check this grader against")
|
||||
}
|
||||
if err := gradeSeed(t, work); err != nil {
|
||||
t.Fatalf("the reference solution does not grade clean, so no attempt can: %v", err)
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/i18n"
|
||||
"reasonix/internal/contract/config"
|
||||
)
|
||||
|
||||
func TestKernelLanguageWithShellSystemLanguage(t *testing.T) {
|
||||
before := i18n.CurrentLanguage()
|
||||
t.Cleanup(func() { i18n.DetectLanguage(before) })
|
||||
for _, key := range []string{"REASONIX_LANG", "LC_ALL", "LC_MESSAGES", "LANG"} {
|
||||
t.Setenv(key, "")
|
||||
}
|
||||
t.Setenv("REASONIX_SYSTEM_LANG", "zh-CN")
|
||||
for _, tt := range []struct{ setting, env, want string }{
|
||||
{"auto", "", "zh"},
|
||||
{"en", "", "en"},
|
||||
{"auto", "en", "en"},
|
||||
{"zh-TW", "en", "zh-TW"},
|
||||
} {
|
||||
t.Setenv("REASONIX_LANG", tt.env)
|
||||
if got := resolveKernelLanguage(&config.Config{Language: tt.setting}); got != tt.want {
|
||||
t.Errorf("setting %q, env %q: language = %q, want %q", tt.setting, tt.env, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"reasonix/internal/assembly/boot"
|
||||
"reasonix/internal/frontend/serve"
|
||||
)
|
||||
|
||||
// launchWorkspace is the folder the first pane opens. The working directory
|
||||
// counts only when someone chose it: a Start-menu or Finder launch inherits the
|
||||
// install directory or the filesystem root, and taking that as a project puts
|
||||
// the app's own folder in the sidebar again after every removal.
|
||||
func launchWorkspace(appExe string) string {
|
||||
home, _ := os.UserHomeDir()
|
||||
return pickLaunchWorkspace(boot.ResolveWorkspaceRoot(""), appExe, serve.LaunchWorkspaces(), home)
|
||||
}
|
||||
|
||||
func pickLaunchWorkspace(cwd, appExe string, remembered []string, home string) string {
|
||||
if userChoseDir(cwd, appExe) {
|
||||
return cwd
|
||||
}
|
||||
for _, dir := range remembered {
|
||||
if st, err := os.Stat(dir); err == nil && st.IsDir() {
|
||||
return dir
|
||||
}
|
||||
}
|
||||
if home != "" {
|
||||
return home
|
||||
}
|
||||
return cwd
|
||||
}
|
||||
|
||||
func userChoseDir(cwd, appExe string) bool {
|
||||
if cwd == "" || filepath.Dir(cwd) == cwd {
|
||||
return false
|
||||
}
|
||||
rel, err := filepath.Rel(filepath.Dir(appExe), cwd)
|
||||
if err != nil {
|
||||
return true
|
||||
}
|
||||
inside := rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)))
|
||||
return !inside
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUserChoseDirRejectsWhatALauncherInherits(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
app := filepath.Join(base, "ReasonixStudio")
|
||||
exe := filepath.Join(app, "Reasonix Studio.exe")
|
||||
cases := []struct {
|
||||
name, cwd, exe string
|
||||
want bool
|
||||
}{
|
||||
{"install directory", app, exe, false},
|
||||
{"below the install directory", filepath.Join(app, "resources"), exe, false},
|
||||
{"filesystem root", filepath.VolumeName(base) + string(filepath.Separator), exe, false},
|
||||
{"a project elsewhere", filepath.Join(base, "project"), exe, true},
|
||||
{"a sibling sharing the name prefix", app + "-notes", exe, true},
|
||||
{"unpackaged run keeps the directory", app, "", true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := userChoseDir(c.cwd, c.exe); got != c.want {
|
||||
t.Errorf("%s: userChoseDir(%q) = %v, want %v", c.name, c.cwd, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPickLaunchWorkspaceFallbackOrder(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
app := filepath.Join(base, "ReasonixStudio")
|
||||
exe := filepath.Join(app, "Reasonix Studio.exe")
|
||||
mk := func(name string) string {
|
||||
d := filepath.Join(base, name)
|
||||
if err := os.MkdirAll(d, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return d
|
||||
}
|
||||
a, b, home := mk("a"), mk("b"), mk("home")
|
||||
gone := filepath.Join(base, "gone")
|
||||
proj := mk("proj")
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
cwd string
|
||||
remembered []string
|
||||
want string
|
||||
}{
|
||||
{"chosen directory wins over the list", proj, []string{a}, proj},
|
||||
{"list head", app, []string{a, b}, a},
|
||||
{"stale head skipped", app, []string{gone, b}, b},
|
||||
{"all stale falls to home", app, []string{gone}, home},
|
||||
{"empty list falls to home", app, nil, home},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := pickLaunchWorkspace(c.cwd, exe, c.remembered, home); got != c.want {
|
||||
t.Errorf("%s: got %q, want %q", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLaunchWorkspaceKeepsLaunchProjectAfterSidebarReorder(t *testing.T) {
|
||||
base := t.TempDir()
|
||||
home := filepath.Join(base, "state")
|
||||
app := filepath.Join(base, "app")
|
||||
a, b := filepath.Join(base, "a"), filepath.Join(base, "b")
|
||||
for _, dir := range []string{home, app, a, b} {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
t.Setenv("REASONIX_HOME", home)
|
||||
t.Chdir(app)
|
||||
data, _ := json.Marshal(map[string]any{"paths": []string{b, a}, "launch": a})
|
||||
if err := os.WriteFile(filepath.Join(home, "serve-workspaces.json"), data, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exe := filepath.Join(app, "Reasonix Studio")
|
||||
if got := launchWorkspace(exe); got != a {
|
||||
t.Fatalf("launchWorkspace = %q, want the pre-reorder launch project %q", got, a)
|
||||
}
|
||||
if err := os.Remove(a); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := launchWorkspace(exe); got != b {
|
||||
t.Fatalf("missing launch project = %q, want next remembered folder %q", got, b)
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
@@ -35,7 +34,6 @@ import (
|
||||
"reasonix/internal/frontend/traystate"
|
||||
"reasonix/internal/platform/account"
|
||||
"reasonix/internal/platform/appupdate"
|
||||
"reasonix/internal/platform/feedback"
|
||||
"reasonix/internal/platform/instanceid"
|
||||
"reasonix/internal/platform/notify"
|
||||
"reasonix/internal/platform/remotecloud"
|
||||
@@ -149,7 +147,6 @@ func main() {
|
||||
// because that pipe is the only channel pointing that way; every log goes to
|
||||
// logs, which is why no line there can be mistaken for one of these.
|
||||
func run(lease io.Reader, handshakeTo, logs io.Writer, page string, shell shellIdentity) int {
|
||||
phases := newStartupPhases(logs, time.Now)
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
if lease != nil {
|
||||
@@ -162,7 +159,6 @@ func run(lease io.Reader, handshakeTo, logs io.Writer, page string, shell shellI
|
||||
}
|
||||
|
||||
served, err := serve.FindPage(page)
|
||||
phases.mark(startupPage)
|
||||
if err != nil {
|
||||
fmt.Fprintln(logs, "reasonix-studio-host:", err)
|
||||
return 1
|
||||
@@ -171,7 +167,7 @@ func run(lease io.Reader, handshakeTo, logs io.Writer, page string, shell shellI
|
||||
fmt.Fprintln(logs, "reasonix-studio-host: no built page found; serving the kernel only")
|
||||
}
|
||||
|
||||
hub, err := assemble(ctx, logs, handshakeTo, shell, served, phases)
|
||||
hub, err := assemble(ctx, logs, handshakeTo, shell, served)
|
||||
if err != nil {
|
||||
fmt.Fprintln(logs, "reasonix-studio-host:", err)
|
||||
return 1
|
||||
@@ -181,7 +177,6 @@ func run(lease io.Reader, handshakeTo, logs io.Writer, page string, shell shellI
|
||||
defer hub.Shutdown()
|
||||
|
||||
bound, err := bind(hub.Handler())
|
||||
phases.mark(startupBind)
|
||||
if err != nil {
|
||||
fmt.Fprintln(logs, "reasonix-studio-host:", err)
|
||||
return 1
|
||||
@@ -189,10 +184,7 @@ func run(lease io.Reader, handshakeTo, logs io.Writer, page string, shell shellI
|
||||
// The credential-writing setup surface opens only on a loopback address,
|
||||
// and this is the first host that has one to show it.
|
||||
hub.EnableProviderSetupForListener(bound.listener.Addr().String())
|
||||
phases.mark(startupSetup)
|
||||
err = announce(handshakeTo, bound)
|
||||
phases.mark(startupAnnounce)
|
||||
if err != nil {
|
||||
if err := announce(handshakeTo, bound); err != nil {
|
||||
fmt.Fprintln(logs, "reasonix-studio-host:", err)
|
||||
return 1
|
||||
}
|
||||
@@ -289,15 +281,13 @@ func resolveKernelLanguage(cfg *config.Config) string {
|
||||
|
||||
// assemble builds the hub this host serves: one pane on the workspace it was
|
||||
// launched in, carrying the capabilities a local window may exercise.
|
||||
func assemble(ctx context.Context, logs, handshakeTo io.Writer, shell shellIdentity, page fs.FS, phases *startupPhases) (*serve.Hub, error) {
|
||||
func assemble(ctx context.Context, logs, handshakeTo io.Writer, shell shellIdentity, page fs.FS) (*serve.Hub, error) {
|
||||
// The one-time upgrades belong to whichever entry point starts first; a
|
||||
// person who only ever opens the window would otherwise never get them.
|
||||
if _, err := config.ApplyUserConfigUpgradesOnStartup(config.UserConfigPath()); err != nil {
|
||||
fmt.Fprintln(logs, "reasonix-studio-host: config upgrade:", err)
|
||||
}
|
||||
phases.mark(startupConfigUpgrade)
|
||||
cfg, err := config.Load()
|
||||
phases.mark(startupConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -321,24 +311,15 @@ func assemble(ctx context.Context, logs, handshakeTo io.Writer, shell shellIdent
|
||||
paneSink = reporter.Wrap(paneSink)
|
||||
}
|
||||
}
|
||||
phases.mark(startupSinks)
|
||||
root := launchWorkspace(shell.exe)
|
||||
phases.mark(startupWorkspace)
|
||||
root := boot.ResolveWorkspaceRoot("")
|
||||
built, err := boot.BuildRuntime(ctx, boot.Options{
|
||||
Version: version,
|
||||
WorkspaceRoot: root,
|
||||
SessionDir: serve.SessionDirFor(root),
|
||||
Sink: paneSink,
|
||||
Stderr: logs,
|
||||
OnPhase: phases.boot,
|
||||
StatsSource: surface.Desktop,
|
||||
FeedbackSurface: feedback.SurfaceStudio,
|
||||
// A stale default_model must not keep the window from opening to fix it.
|
||||
OpenOnFallbackModel: true,
|
||||
|
||||
CleanupPendingReconciler: serve.BackgroundCleanupReconciler,
|
||||
Version: version,
|
||||
WorkspaceRoot: root,
|
||||
SessionDir: serve.SessionDirFor(root),
|
||||
Sink: paneSink,
|
||||
Stderr: logs,
|
||||
StatsSource: surface.Desktop,
|
||||
})
|
||||
phases.mark(startupRuntime)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -353,7 +334,6 @@ func assemble(ctx context.Context, logs, handshakeTo io.Writer, shell shellIdent
|
||||
// Shut until the person at the window opens it; the context ending closes
|
||||
// it with the rest of the kernel, unpairing every device.
|
||||
share := serve.NewDeviceShare(page)
|
||||
share.RestorePort(cfg.SharePort())
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
share.Close()
|
||||
@@ -378,16 +358,12 @@ func assemble(ctx context.Context, logs, handshakeTo io.Writer, shell shellIdent
|
||||
srv := serve.New(built.Controller, bc, hubCfg)
|
||||
srv.SetPaneSink(paneSink)
|
||||
srv.AdoptRuntime(built)
|
||||
phases.mark(startupHub)
|
||||
_, err = hub.Adopt(srv, bc)
|
||||
phases.mark(startupAdopt)
|
||||
if err != nil {
|
||||
if _, err := hub.Adopt(srv, bc); err != nil {
|
||||
hub.Shutdown()
|
||||
return nil, err
|
||||
}
|
||||
hub.StartRecoveryGC(ctx)
|
||||
startCloudRemote(ctx, cfg, logs, hub)
|
||||
phases.mark(startupBackground)
|
||||
return hub, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -487,7 +487,7 @@ func TestAssembleAppliesTheStartupUpgrades(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Chdir(testenv.TempDir(t))
|
||||
hub, err := assemble(t.Context(), io.Discard, io.Discard, shellIdentity{}, nil, newStartupPhases(io.Discard, time.Now))
|
||||
hub, err := assemble(t.Context(), io.Discard, io.Discard, shellIdentity{}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/assembly/boot"
|
||||
)
|
||||
|
||||
type startupPhase string
|
||||
|
||||
const (
|
||||
startupPage startupPhase = "page"
|
||||
startupConfigUpgrade startupPhase = "config-upgrade"
|
||||
startupConfig startupPhase = "config"
|
||||
startupSinks startupPhase = "sinks"
|
||||
startupWorkspace startupPhase = "workspace"
|
||||
startupRuntime startupPhase = "runtime"
|
||||
startupHub startupPhase = "hub"
|
||||
startupAdopt startupPhase = "adopt"
|
||||
startupBackground startupPhase = "background"
|
||||
startupBind startupPhase = "bind"
|
||||
startupSetup startupPhase = "provider-setup"
|
||||
startupAnnounce startupPhase = "announce"
|
||||
)
|
||||
|
||||
type startupPhases struct {
|
||||
logs io.Writer
|
||||
now func() time.Time
|
||||
last time.Time
|
||||
}
|
||||
|
||||
func newStartupPhases(logs io.Writer, now func() time.Time) *startupPhases {
|
||||
return &startupPhases{logs: logs, now: now, last: now()}
|
||||
}
|
||||
|
||||
func (p *startupPhases) mark(phase startupPhase) {
|
||||
now := p.now()
|
||||
p.write(phase, now.Sub(p.last))
|
||||
p.last = now
|
||||
}
|
||||
|
||||
func (p *startupPhases) write(phase startupPhase, elapsed time.Duration) {
|
||||
fmt.Fprintf(p.logs, "reasonix-studio-host: startup phase=%s elapsed_ms=%d\n", phase, elapsed.Milliseconds())
|
||||
}
|
||||
|
||||
func (p *startupPhases) boot(phase boot.Phase) {
|
||||
p.write(startupPhase("boot."+phase.Name), phase.D)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/assembly/boot"
|
||||
)
|
||||
|
||||
func TestStartupPhasesUseElapsedMilliseconds(t *testing.T) {
|
||||
var logs bytes.Buffer
|
||||
now := time.Unix(100, 0)
|
||||
p := newStartupPhases(&logs, func() time.Time { return now })
|
||||
now = now.Add(1250 * time.Millisecond)
|
||||
p.mark(startupPage)
|
||||
now = now.Add(7 * time.Millisecond)
|
||||
p.boot(boot.Phase{Name: "sessions", D: 3 * time.Millisecond})
|
||||
p.mark(startupRuntime)
|
||||
want := "reasonix-studio-host: startup phase=page elapsed_ms=1250\n" +
|
||||
"reasonix-studio-host: startup phase=boot.sessions elapsed_ms=3\n" +
|
||||
"reasonix-studio-host: startup phase=runtime elapsed_ms=7\n"
|
||||
if got := logs.String(); got != want {
|
||||
t.Fatalf("phase log = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/base/i18n"
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/config"
|
||||
"reasonix/internal/frontend/serve"
|
||||
)
|
||||
|
||||
func TestAssembledTrayLanguage(t *testing.T) {
|
||||
before := i18n.CurrentLanguage()
|
||||
t.Cleanup(func() { i18n.DetectLanguage(before) })
|
||||
for _, tt := range []struct{ name, system, desktop, env, locale, want string }{
|
||||
{"system", "zh-Hans-CN", "auto", "", "", "zh"},
|
||||
{"traditional", "zh-Hant-TW", "auto", "", "", "zh-TW"},
|
||||
{"desktop", "zh-Hans-CN", "en", "", "", "en"},
|
||||
{"reasonix", "zh-Hans-CN", "auto", "en-US", "", "en"},
|
||||
{"desktop-over-env", "en-US", "zh", "en", "", "zh"},
|
||||
{"c-locale", "zh-Hans-CN", "auto", "", "C", "en"},
|
||||
{"posix-locale", "zh-Hans-CN", "auto", "", "POSIX.UTF-8", "en"},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Setenv("REASONIX_HOME", testenv.TempDir(t))
|
||||
t.Chdir(testenv.TempDir(t))
|
||||
for _, key := range []string{"REASONIX_LANG", "LC_ALL", "LC_MESSAGES", "LANG"} {
|
||||
t.Setenv(key, "")
|
||||
}
|
||||
t.Setenv("REASONIX_SYSTEM_LANG", tt.system)
|
||||
t.Setenv("REASONIX_LANG", tt.env)
|
||||
t.Setenv("LANG", tt.locale)
|
||||
cfg := config.Default()
|
||||
disabled := false
|
||||
cfg.Desktop.Telemetry, cfg.Desktop.Metrics = &disabled, &disabled
|
||||
if err := cfg.SetDesktopLanguage(tt.desktop); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := cfg.SaveTo(config.UserConfigPath()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
hub, err := assemble(ctx, io.Discard, io.Discard, shellIdentity{}, nil, newStartupPhases(io.Discard, time.Now))
|
||||
if err != nil {
|
||||
t.Fatalf("assemble: %v", err)
|
||||
}
|
||||
defer hub.Shutdown()
|
||||
b, err := bind(hub.Handler())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- b.serve(ctx) }()
|
||||
defer func() {
|
||||
cancel()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
case <-time.After(30 * time.Second):
|
||||
t.Error("host did not drain")
|
||||
}
|
||||
}()
|
||||
check := func(want string) {
|
||||
t.Helper()
|
||||
status, body := ask(t, b, http.MethodGet, "/tray/state", nil)
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("tray status = %d", status)
|
||||
}
|
||||
var state serve.TrayState
|
||||
if err := json.Unmarshal([]byte(body), &state); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
say := i18n.Catalog(want)
|
||||
labels := serve.TrayLabels{Open: say.TrayOpen, CloseToTray: say.TrayCloseToTray, Quit: say.TrayQuit}
|
||||
if state.Labels != labels || state.Line != say.TrayIdle {
|
||||
t.Fatalf("tray = %+v, want labels %+v and line %q", state, labels, say.TrayIdle)
|
||||
}
|
||||
}
|
||||
check(tt.want)
|
||||
for _, lang := range []string{"en", "zh"} {
|
||||
status, _ := ask(t, b, http.MethodPost, "/appearance", func(r *http.Request) {
|
||||
r.Body = io.NopCloser(strings.NewReader(`{"language":"` + lang + `"}`))
|
||||
})
|
||||
if status != http.StatusOK {
|
||||
t.Fatalf("appearance status = %d", status)
|
||||
}
|
||||
check(lang)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -71,40 +71,3 @@ func TestTheHostPointsTheKernelsCatalogueAtTheConfiguredLanguage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sweeping a session directory costs a handful of file operations per
|
||||
// transcript, and the window cannot open until the handshake. A build that
|
||||
// sweeps inline makes the first launch after the cache went cold wait on
|
||||
// history it never displays.
|
||||
func TestTheHostsFirstBuildSweepsSessionsInTheBackground(t *testing.T) {
|
||||
file, err := parser.ParseFile(token.NewFileSet(), "main.go", nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("parse main.go: %v", err)
|
||||
}
|
||||
found := false
|
||||
ast.Inspect(file, func(n ast.Node) bool {
|
||||
lit, ok := n.(*ast.CompositeLit)
|
||||
if !ok {
|
||||
return true
|
||||
}
|
||||
sel, ok := lit.Type.(*ast.SelectorExpr)
|
||||
if !ok || sel.Sel.Name != "Options" {
|
||||
return true
|
||||
}
|
||||
for _, elt := range lit.Elts {
|
||||
kv, ok := elt.(*ast.KeyValueExpr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if key, ok := kv.Key.(*ast.Ident); ok && key.Name == "CleanupPendingReconciler" {
|
||||
if v, ok := kv.Value.(*ast.SelectorExpr); ok && v.Sel.Name == "BackgroundCleanupReconciler" {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
if !found {
|
||||
t.Fatal("the host's boot.Options does not set CleanupPendingReconciler to serve.BackgroundCleanupReconciler")
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -10,7 +11,10 @@ import (
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var cliChannelSecrets = []string{"NPM_TOKEN", "HOMEBREW_TAP_TOKEN"}
|
||||
var cliChannelSecrets = []string{"STUDIO_NPM_TOKEN", "STUDIO_HOMEBREW_TAP_TOKEN"}
|
||||
|
||||
// The 1.x line reads these repository secrets; studio must never touch them.
|
||||
var legacyRegistrySecrets = []string{"NPM_TOKEN", "HOMEBREW_TAP_TOKEN"}
|
||||
|
||||
func parseWorkflowFile(t *testing.T, path string) *yaml.Node {
|
||||
t.Helper()
|
||||
@@ -90,8 +94,8 @@ func TestCLIChannelsJobIsGatedAndAttested(t *testing.T) {
|
||||
if job == nil {
|
||||
t.Fatal("cli-channels job is missing")
|
||||
}
|
||||
if mappingValue(job, "environment") != nil {
|
||||
t.Fatal("cli-channels must not declare an environment; the switch and the tag rule are its controls")
|
||||
if got := mappingScalar(job, "environment"); got != "studio-cli-channels" {
|
||||
t.Fatalf("cli-channels environment = %q, want studio-cli-channels", got)
|
||||
}
|
||||
if got := mappingScalar(job, "if"); got != "vars.STUDIO_PUBLISHES_CLI == 'true'" {
|
||||
t.Fatalf("cli-channels if = %q, want the STUDIO_PUBLISHES_CLI switch", got)
|
||||
@@ -109,11 +113,18 @@ func TestCLIChannelsJobIsGatedAndAttested(t *testing.T) {
|
||||
t.Fatalf("cli-channels permission %s = %q, want %q", scope, got, level)
|
||||
}
|
||||
}
|
||||
if !nodeContains(job, "environments/studio-cli-channels") || !nodeContains(job, "required_reviewers") {
|
||||
t.Fatal("cli-channels must verify the environment's required reviewers before publishing")
|
||||
}
|
||||
assertGateBeforePublishing(t, job)
|
||||
for _, name := range cliChannelSecrets {
|
||||
if !secretRefPattern(name).MatchString(scalarString(job)) {
|
||||
t.Fatalf("cli-channels does not read secrets.%s", name)
|
||||
}
|
||||
}
|
||||
if name, found := refersTo(job, legacyRegistrySecrets); found {
|
||||
t.Fatalf("cli-channels reads %s; it must use the environment-only STUDIO_* names", name)
|
||||
}
|
||||
}
|
||||
|
||||
func scalarString(node *yaml.Node) string {
|
||||
@@ -122,20 +133,6 @@ func scalarString(node *yaml.Node) string {
|
||||
return strings.Join(scalars, "\n")
|
||||
}
|
||||
|
||||
func TestReleaseStudioGrantsNoWriteAll(t *testing.T) {
|
||||
root := parseWorkflowFile(t, "../../.github/workflows/release-studio.yml")
|
||||
if top := mappingValue(root, "permissions"); top != nil && top.Kind == yaml.ScalarNode && top.Value == "write-all" {
|
||||
t.Error("release-studio.yml grants write-all")
|
||||
}
|
||||
jobs := jobsOf(t, root)
|
||||
for i := 0; i+1 < len(jobs.Content); i += 2 {
|
||||
name, job := jobs.Content[i].Value, jobs.Content[i+1]
|
||||
if p := mappingValue(job, "permissions"); p != nil && p.Kind == yaml.ScalarNode && p.Value == "write-all" {
|
||||
t.Errorf("release-studio.yml job %s grants write-all", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoWorkflowGrantsIDTokenOrBroadPermissionsOutsideCLIChannels(t *testing.T) {
|
||||
files, err := filepath.Glob("../../.github/workflows/*.yml")
|
||||
if err != nil || len(files) == 0 {
|
||||
@@ -176,7 +173,7 @@ func TestOnlyCLIChannelsReferencesRegistrySecrets(t *testing.T) {
|
||||
if err != nil || len(files) == 0 {
|
||||
t.Fatalf("no workflows found: %v", err)
|
||||
}
|
||||
guarded := cliChannelSecrets
|
||||
guarded := append(append([]string{}, cliChannelSecrets...), legacyRegistrySecrets...)
|
||||
for _, file := range files {
|
||||
root := parseWorkflowFile(t, file)
|
||||
jobs := mappingValue(root, "jobs")
|
||||
@@ -199,18 +196,18 @@ func TestOnlyCLIChannelsReferencesRegistrySecrets(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
if name, found := refersTo(root, cliChannelSecrets); found {
|
||||
t.Errorf("%s reaches %s, which only release-studio.yml cli-channels may read", file, name)
|
||||
t.Errorf("%s reaches %s, which belongs to studio's cli-channels", file, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretReferenceDetectionCoversBypassForms(t *testing.T) {
|
||||
for _, text := range []string{
|
||||
"${{ secrets.NPM_TOKEN }}",
|
||||
"${{ secrets['NPM_TOKEN'] }}",
|
||||
`${{ secrets["NPM_TOKEN"] }}`,
|
||||
"${{ secrets.STUDIO_NPM_TOKEN }}",
|
||||
"${{ secrets['STUDIO_NPM_TOKEN'] }}",
|
||||
`${{ secrets["STUDIO_NPM_TOKEN"] }}`,
|
||||
"${{ toJSON(secrets) }}",
|
||||
"${{ secrets[format('NPM_{0}', 'TOKEN')] }}",
|
||||
"${{ secrets[format('STUDIO_{0}', 'NPM_TOKEN')] }}",
|
||||
} {
|
||||
var doc yaml.Node
|
||||
if err := yaml.Unmarshal([]byte("run: \""+regexp.MustCompile(`"`).ReplaceAllString(text, `\"`)+"\"\n"), &doc); err != nil {
|
||||
@@ -228,3 +225,82 @@ func TestSecretReferenceDetectionCoversBypassForms(t *testing.T) {
|
||||
t.Error("an unrelated secret was reported")
|
||||
}
|
||||
}
|
||||
|
||||
func assertGateBeforePublishing(t *testing.T, job *yaml.Node) {
|
||||
t.Helper()
|
||||
steps := mappingValue(job, "steps")
|
||||
if steps == nil || steps.Kind != yaml.SequenceNode {
|
||||
t.Fatal("cli-channels has no steps")
|
||||
}
|
||||
gate := -1
|
||||
for i, step := range steps.Content {
|
||||
if nodeContains(mappingValue(step, "run"), "environments/studio-cli-channels") {
|
||||
gate = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if gate < 0 {
|
||||
t.Fatal("cli-channels has no environment check step")
|
||||
}
|
||||
for i, step := range steps.Content[:gate] {
|
||||
if run := mappingScalar(step, "run"); run != "" || mappingValue(step, "env") != nil {
|
||||
t.Errorf("step %d runs or gets credentials before the environment check", i)
|
||||
}
|
||||
}
|
||||
for i, step := range steps.Content {
|
||||
if i > gate {
|
||||
continue
|
||||
}
|
||||
for _, name := range append(append([]string{}, cliChannelSecrets...), legacyRegistrySecrets...) {
|
||||
if nodeContains(step, name) {
|
||||
t.Errorf("step %d references %s before or at the environment check", i, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironmentCheckStepFailsClosed(t *testing.T) {
|
||||
bash, err := exec.LookPath("bash")
|
||||
if err != nil {
|
||||
t.Skip("bash not available")
|
||||
}
|
||||
root := parseWorkflowFile(t, "../../.github/workflows/release-studio.yml")
|
||||
job := mappingValue(jobsOf(t, root), "cli-channels")
|
||||
var script string
|
||||
for _, step := range mappingValue(job, "steps").Content {
|
||||
if run := mappingScalar(step, "run"); strings.Contains(run, "environments/studio-cli-channels") {
|
||||
script = run
|
||||
break
|
||||
}
|
||||
}
|
||||
if script == "" {
|
||||
t.Fatal("environment check step not found")
|
||||
}
|
||||
reviewers := `{"type":"required_reviewers","reviewers":[{"a":1}]}`
|
||||
cases := []struct {
|
||||
name, response string
|
||||
pass bool
|
||||
}{
|
||||
{"one reviewer", "HTTP/2.0 200 OK\n\n{\"protection_rules\":[" + reviewers + "]}", true},
|
||||
{"no rules", "HTTP/2.0 200 OK\n\n{\"protection_rules\":[]}", false},
|
||||
{"two JSON documents", "HTTP/2.0 200 OK\n\n{\"protection_rules\":[]}{\"protection_rules\":[]}", false},
|
||||
{"empty body", "HTTP/2.0 200 OK\n\n", false},
|
||||
{"non-numeric body", "HTTP/2.0 200 OK\n\n\"x\"", false},
|
||||
{"missing environment", "HTTP/2.0 404 Not Found\n\n{\"message\":\"Not Found\"}", false},
|
||||
{"server error", "HTTP/2.0 500 Error\n\n{}", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
dir := t.TempDir()
|
||||
fake := "#!/bin/sh\ncat <<'EOF'\n" + tc.response + "\nEOF\n"
|
||||
if err := os.WriteFile(filepath.Join(dir, "gh"), []byte(fake), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cmd := exec.Command(bash, "-c", script+"\necho PASSED")
|
||||
cmd.Env = append(os.Environ(), "PATH="+dir+string(os.PathListSeparator)+os.Getenv("PATH"), "REPO=x/y", "GH_TOKEN=x", "GITHUB_ACTOR=t")
|
||||
out, err := cmd.CombinedOutput()
|
||||
passed := err == nil && strings.Contains(string(out), "PASSED")
|
||||
if passed != tc.pass {
|
||||
t.Errorf("%s: passed=%v, want %v\n%s", tc.name, passed, tc.pass, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestCLIPointerJobIsSwitchedSerializedAndNarrow(t *testing.T) {
|
||||
root := parseWorkflowFile(t, "../../.github/workflows/release-studio.yml")
|
||||
job := mappingValue(jobsOf(t, root), "cli-pointer")
|
||||
if job == nil {
|
||||
t.Fatal("cli-pointer job is missing")
|
||||
}
|
||||
if got := mappingScalar(job, "if"); got != "vars.STUDIO_PUBLISHES_CLI == 'true'" {
|
||||
t.Fatalf("cli-pointer if = %q, want the STUDIO_PUBLISHES_CLI switch", got)
|
||||
}
|
||||
needs := mappingValue(job, "needs")
|
||||
if needs == nil || needs.Kind != yaml.SequenceNode {
|
||||
t.Fatal("cli-pointer must list its needs")
|
||||
}
|
||||
seen := map[string]bool{}
|
||||
for _, n := range needs.Content {
|
||||
seen[n.Value] = true
|
||||
}
|
||||
if !seen["resolve"] || !seen["publish"] || !seen["cli-gate"] {
|
||||
t.Fatalf("cli-pointer needs %v, want resolve, publish and cli-gate", seen)
|
||||
}
|
||||
concurrency := mappingValue(job, "concurrency")
|
||||
if concurrency == nil || !strings.HasPrefix(mappingScalar(concurrency, "group"), "studio-cli-pointer-") {
|
||||
t.Fatal("cli-pointer must take its own per-channel lock, never the 1.x workflow's release-cli-<channel> group")
|
||||
}
|
||||
if mappingScalar(concurrency, "cancel-in-progress") != "false" {
|
||||
t.Fatal("cli-pointer must not cancel a run that is mid-write")
|
||||
}
|
||||
permissions := mappingValue(job, "permissions")
|
||||
if permissions == nil || permissions.Kind != yaml.MappingNode || len(permissions.Content) != 2 ||
|
||||
mappingScalar(permissions, "contents") != "write" {
|
||||
t.Fatal("cli-pointer permissions must be exactly contents: write")
|
||||
}
|
||||
if !nodeContains(job, "scripts/publish-cli-pointer.sh") {
|
||||
t.Fatal("cli-pointer must run scripts/publish-cli-pointer.sh")
|
||||
}
|
||||
if !nodeContains(job, "vars.CLI_PUBLISH_FROZEN") {
|
||||
t.Fatal("cli-pointer must pass vars.CLI_PUBLISH_FROZEN to its script")
|
||||
}
|
||||
if name, found := refersTo(job, append([]string{"RELEASE_TAG_TOKEN"}, cliChannelSecrets...)); found {
|
||||
t.Fatalf("cli-pointer reads %s; registry credentials belong to cli-channels only", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIGateBlocksEveryCLIChannel(t *testing.T) {
|
||||
root := parseWorkflowFile(t, "../../.github/workflows/release-studio.yml")
|
||||
jobs := jobsOf(t, root)
|
||||
gate := mappingValue(jobs, "cli-gate")
|
||||
if gate == nil {
|
||||
t.Fatal("cli-gate job is missing")
|
||||
}
|
||||
if got := mappingScalar(gate, "if"); got != "vars.STUDIO_PUBLISHES_CLI == 'true'" {
|
||||
t.Fatalf("cli-gate if = %q, want the STUDIO_PUBLISHES_CLI switch", got)
|
||||
}
|
||||
if !nodeContains(gate, "vars.CLI_PUBLISH_FROZEN") {
|
||||
t.Fatal("cli-gate must read vars.CLI_PUBLISH_FROZEN")
|
||||
}
|
||||
if nodeContains(gate, "actions/checkout") || nodeContains(gate, "secrets.") {
|
||||
t.Fatal("cli-gate must run no repository code and read no secret")
|
||||
}
|
||||
for _, name := range []string{"cli-tag", "cli-channels", "cli-pointer"} {
|
||||
needs := mappingValue(mappingValue(jobs, name), "needs")
|
||||
found := false
|
||||
for _, n := range needs.Content {
|
||||
found = found || n.Value == "cli-gate"
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("%s does not wait for cli-gate", name)
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"cli-channels", "cli-pointer"} {
|
||||
found := false
|
||||
for _, n := range mappingValue(mappingValue(jobs, name), "needs").Content {
|
||||
found = found || n.Value == "cli-tag"
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("%s does not wait for cli-tag", name)
|
||||
}
|
||||
}
|
||||
found := false
|
||||
for _, n := range mappingValue(mappingValue(jobs, "cli-channels"), "needs").Content {
|
||||
found = found || n.Value == "cli-pointer"
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("cli-channels does not wait for cli-pointer: the cask would name archives that may not exist yet")
|
||||
}
|
||||
}
|
||||
|
||||
var releaseTagSecrets = []string{"RELEASE_TAG_TOKEN"}
|
||||
|
||||
func workflowFiles(t *testing.T) []string {
|
||||
t.Helper()
|
||||
var files []string
|
||||
for _, pattern := range []string{"*.yml", "*.yaml"} {
|
||||
found, err := filepath.Glob("../../.github/workflows/" + pattern)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
files = append(files, found...)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
t.Fatal("no workflows found")
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
// The token may appear in one inline step of Studio's cli-tag job and in no
|
||||
// other env scope: not the workflow's, not a job's, not another step's.
|
||||
func TestReleaseTagTokenIsReadByOneInlineStep(t *testing.T) {
|
||||
holders := 0
|
||||
for _, file := range workflowFiles(t) {
|
||||
root := parseWorkflowFile(t, file)
|
||||
studio := filepath.Base(file) == "release-studio.yml"
|
||||
for i := 0; i+1 < len(root.Content); i += 2 {
|
||||
if root.Content[i].Value == "jobs" {
|
||||
continue
|
||||
}
|
||||
if what, found := refersTo(root.Content[i+1], releaseTagSecrets); found {
|
||||
t.Errorf("%s reaches %s at workflow level (%s)", file, what, root.Content[i].Value)
|
||||
}
|
||||
}
|
||||
jobs := mappingValue(root, "jobs")
|
||||
if jobs == nil {
|
||||
continue
|
||||
}
|
||||
for i := 0; i+1 < len(jobs.Content); i += 2 {
|
||||
id, job := jobs.Content[i].Value, jobs.Content[i+1]
|
||||
allowed := studio && id == "cli-tag"
|
||||
for j := 0; j+1 < len(job.Content); j += 2 {
|
||||
key, value := job.Content[j].Value, job.Content[j+1]
|
||||
if key != "steps" {
|
||||
if what, found := refersTo(value, releaseTagSecrets); found {
|
||||
t.Errorf("%s job %s reaches %s in %s; only a step of cli-tag may", file, id, what, key)
|
||||
}
|
||||
continue
|
||||
}
|
||||
for _, step := range value.Content {
|
||||
if what, found := refersTo(step, releaseTagSecrets); found {
|
||||
holders++
|
||||
if !allowed {
|
||||
t.Errorf("%s job %s reaches %s; only Studio's cli-tag step may", file, id, what)
|
||||
}
|
||||
if mappingScalar(step, "uses") != "" {
|
||||
t.Errorf("%s job %s: the token step must be inline shell, not an action", file, id)
|
||||
}
|
||||
} else if allowed && mappingScalar(step, "uses") != "" {
|
||||
t.Errorf("cli-tag must check out nothing and run no action")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if holders != 1 {
|
||||
t.Fatalf("%d steps read RELEASE_TAG_TOKEN, want exactly one", holders)
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -19,7 +19,7 @@
|
||||
"computer-live": "electron test/computer_live.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "44.5.1",
|
||||
"electron": "44.4.5",
|
||||
"electron-builder": "^26.15.3",
|
||||
"playwright": "1.63.0"
|
||||
},
|
||||
|
||||
@@ -7,17 +7,11 @@ const { chromium } = require("playwright");
|
||||
|
||||
const ASSETS = path.join(__dirname, "..", "assets");
|
||||
const ICO_SIZES = [16, 20, 24, 32, 40, 48, 64, 256];
|
||||
// The tray is drawn at 16 logical pixels. Windows asks for exactly 16*scale
|
||||
// pixels and resizes whatever it is handed, so every Windows scale has its own
|
||||
// file (src/trayimage.js picks it).
|
||||
const TRAY_SCALES = [[1, ""], [1.25, "@1.25x"], [1.5, "@1.5x"], [1.75, "@1.75x"], [2, "@2x"], [2.5, "@2.5x"], [3, "@3x"]];
|
||||
// The tray is drawn at 16 logical pixels; nativeImage picks the @Nx file that
|
||||
// matches the display, so a scaled screen never enlarges the 16px one.
|
||||
const TRAY_SCALES = [[1, ""], [1.25, "@1.25x"], [1.5, "@1.5x"], [2, "@2x"], [3, "@3x"]];
|
||||
// The app icon's margin is the platform's to keep; a tray slot has none to spare.
|
||||
const TRAY_VIEWBOX = 'viewBox="103 103 818 818"';
|
||||
// Windows draws the .ico edge to edge beside neighbours that fill their cell, so
|
||||
// its tile is cropped to a 2% margin and the glyph enlarged within it; macOS and
|
||||
// Linux keep the full-canvas artwork the platform templates expect.
|
||||
const WIN_VIEWBOX = 'viewBox="91 91 842 842"';
|
||||
const WIN_GLYPH_SCALE = "scale(2.4)";
|
||||
// PNG payloads for every type modern macOS reads; ic11–ic14 are the @2x slots.
|
||||
const ICNS_TYPES = [
|
||||
["ic04", 16], ["ic05", 32], ["ic07", 128], ["ic08", 256], ["ic09", 512], ["ic10", 1024],
|
||||
@@ -79,9 +73,8 @@ async function main() {
|
||||
for (const [scale, suffix] of TRAY_SCALES) {
|
||||
fs.writeFileSync(path.join(ASSETS, `tray${suffix}.png`), await render(page, traySvg, 16 * scale));
|
||||
}
|
||||
const winSvg = svg.replace('viewBox="0 0 1024 1024"', WIN_VIEWBOX).replace("scale(1.9)", WIN_GLYPH_SCALE);
|
||||
const icoPngs = [];
|
||||
for (const size of ICO_SIZES) icoPngs.push([size, await render(page, winSvg, size)]);
|
||||
for (const size of ICO_SIZES) icoPngs.push([size, await at(size)]);
|
||||
fs.writeFileSync(path.join(ASSETS, "icon.ico"), ico(icoPngs));
|
||||
const icnsPngs = [];
|
||||
for (const [type, size] of ICNS_TYPES) icnsPngs.push([type, await at(size)]);
|
||||
|
||||
@@ -9,8 +9,8 @@ importers:
|
||||
.:
|
||||
devDependencies:
|
||||
electron:
|
||||
specifier: 44.5.1
|
||||
version: 44.5.1([email protected])
|
||||
specifier: 44.4.5
|
||||
version: 44.4.5([email protected])
|
||||
electron-builder:
|
||||
specifier: ^26.15.3
|
||||
version: 26.15.3([email protected])([email protected])
|
||||
@@ -374,8 +374,8 @@ packages:
|
||||
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
electron@44.5.1:
|
||||
resolution: {integrity: sha512-lx7AYoFIiyvEvgvpyWO9kOC60xtNPpiPZAth6Fmg9NutzpCAMLwpUlaBY2EXfZyIgUdzEmLMj9KvK0Vrokl1yQ==}
|
||||
electron@44.4.5:
|
||||
resolution: {integrity: sha512-SjgoaeYsSWZfJzubgQU7juvuXMTvn6/e1gAHdGFA/yuMbpF+I+skYqIJ6DdXBXHeWbkbpNpmwZCTpiivtlWZSw==}
|
||||
engines: {node: '>= 22.12.0'}
|
||||
hasBin: true
|
||||
|
||||
@@ -1551,7 +1551,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron@44.5.1([email protected]):
|
||||
electron@44.4.5([email protected]):
|
||||
dependencies:
|
||||
'@electron-internal/extract-zip': 1.0.5
|
||||
'@electron/get': 5.1.0([email protected])
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use strict";
|
||||
const crypto = require("node:crypto");
|
||||
const { WebContentsView, session } = require("electron");
|
||||
const { installPaneReload } = require("./reload");
|
||||
const { guestNavigationAllowed, typedAddress } = require("./browserguard");
|
||||
|
||||
// The size a page lays out at while nobody is looking at it: the kernel drives
|
||||
@@ -97,7 +96,6 @@ class BrowserViews {
|
||||
if (allowed(to)) onPopup(to);
|
||||
return { action: "deny" };
|
||||
});
|
||||
installPaneReload(contents);
|
||||
contents.debugger.attach("1.3");
|
||||
contents.debugger.on("message", (_event, method, params, sessionId) => {
|
||||
if (sessionId) return;
|
||||
|
||||
@@ -1,131 +1,21 @@
|
||||
"use strict";
|
||||
|
||||
// Role labels come from the platform's own catalogue, which follows the
|
||||
// system locale rather than the language the interface was set to, so they are
|
||||
// stated here for the two languages the interface draws.
|
||||
const WORDS = {
|
||||
zh: {
|
||||
edit: "编辑",
|
||||
undo: "撤销",
|
||||
redo: "重做",
|
||||
cut: "剪切",
|
||||
copy: "复制",
|
||||
paste: "粘贴",
|
||||
selectAll: "全选",
|
||||
pasteAndMatchStyle: "粘贴并匹配样式",
|
||||
delete: "删除",
|
||||
substitutions: "替换",
|
||||
showSubstitutions: "显示替换",
|
||||
smartQuotes: "智能引号",
|
||||
smartDashes: "智能破折号",
|
||||
textReplacement: "文本替换",
|
||||
speech: "语音",
|
||||
startSpeaking: "开始朗读",
|
||||
stopSpeaking: "停止朗读",
|
||||
},
|
||||
en: {
|
||||
edit: "Edit",
|
||||
undo: "Undo",
|
||||
redo: "Redo",
|
||||
cut: "Cut",
|
||||
copy: "Copy",
|
||||
paste: "Paste",
|
||||
selectAll: "Select All",
|
||||
pasteAndMatchStyle: "Paste and Match Style",
|
||||
delete: "Delete",
|
||||
substitutions: "Substitutions",
|
||||
showSubstitutions: "Show Substitutions",
|
||||
smartQuotes: "Smart Quotes",
|
||||
smartDashes: "Smart Dashes",
|
||||
textReplacement: "Text Replacement",
|
||||
speech: "Speech",
|
||||
startSpeaking: "Start Speaking",
|
||||
stopSpeaking: "Stop Speaking",
|
||||
},
|
||||
};
|
||||
|
||||
function wordsFor(lang) {
|
||||
return WORDS[lang] ?? WORDS.en;
|
||||
}
|
||||
|
||||
// The edit commands as roles carrying explicit labels: roles do the clipboard
|
||||
// work, the labels follow the interface language.
|
||||
function editItems(lang, can) {
|
||||
const w = wordsFor(lang);
|
||||
const on = (flag) => (can ? { enabled: !!can[flag] } : {});
|
||||
return [
|
||||
{ role: "undo", label: w.undo, ...on("canUndo") },
|
||||
{ role: "redo", label: w.redo, ...on("canRedo") },
|
||||
{ type: "separator" },
|
||||
{ role: "cut", label: w.cut, ...on("canCut") },
|
||||
{ role: "copy", label: w.copy, ...on("canCopy") },
|
||||
{ role: "paste", label: w.paste, ...on("canPaste") },
|
||||
{ type: "separator" },
|
||||
{ role: "selectAll", label: w.selectAll, ...on("canSelectAll") },
|
||||
];
|
||||
}
|
||||
|
||||
// The context menu's shape, kept apart from the platform so it can be checked
|
||||
// without one. editFlags is the page's own account of what is possible at the
|
||||
// click: this only decides what to offer.
|
||||
function contextTemplate(params, lang) {
|
||||
// click: roles do the clipboard work, this only decides what to offer.
|
||||
function contextTemplate(params) {
|
||||
if (!params.isEditable && !params.selectionText) return [];
|
||||
return editItems(lang, params.editFlags ?? {});
|
||||
}
|
||||
|
||||
// Everything the platform's own Edit menu carries, with the same roles, so
|
||||
// only the words change.
|
||||
function editMenuTemplate(lang) {
|
||||
const w = wordsFor(lang);
|
||||
const items = editItems(lang);
|
||||
const at = items.findIndex((i) => i.role === "selectAll");
|
||||
items.splice(at - 1, 1);
|
||||
items.splice(at - 1, 0,
|
||||
{ role: "pasteAndMatchStyle", label: w.pasteAndMatchStyle },
|
||||
{ role: "delete", label: w.delete });
|
||||
items.push(
|
||||
const can = params.editFlags ?? {};
|
||||
return [
|
||||
{ role: "undo", enabled: !!can.canUndo },
|
||||
{ role: "redo", enabled: !!can.canRedo },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: w.substitutions,
|
||||
submenu: [
|
||||
{ role: "showSubstitutions", label: w.showSubstitutions },
|
||||
{ type: "separator" },
|
||||
{ role: "toggleSmartQuotes", label: w.smartQuotes },
|
||||
{ role: "toggleSmartDashes", label: w.smartDashes },
|
||||
{ role: "toggleTextReplacement", label: w.textReplacement },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: w.speech,
|
||||
submenu: [
|
||||
{ role: "startSpeaking", label: w.startSpeaking },
|
||||
{ role: "stopSpeaking", label: w.stopSpeaking },
|
||||
],
|
||||
},
|
||||
);
|
||||
return { label: w.edit, submenu: items };
|
||||
{ role: "cut", enabled: !!can.canCut },
|
||||
{ role: "copy", enabled: !!can.canCopy },
|
||||
{ role: "paste", enabled: !!can.canPaste },
|
||||
{ type: "separator" },
|
||||
{ role: "selectAll", enabled: !!can.canSelectAll },
|
||||
];
|
||||
}
|
||||
|
||||
function applicationMenuTemplate(lang) {
|
||||
return [{ role: "appMenu" }, editMenuTemplate(lang), { role: "windowMenu" }];
|
||||
}
|
||||
|
||||
// Only the Edit menu follows the interface language; the application and window
|
||||
// menus are roles and follow the OS. Menu is injected so this runs without Electron.
|
||||
function menuInstaller(Menu, platform = process.platform) {
|
||||
let installedLang = null;
|
||||
return function installApplicationMenu(languageOf = () => "en") {
|
||||
if (platform !== "darwin") {
|
||||
Menu.setApplicationMenu(null);
|
||||
return null;
|
||||
}
|
||||
const lang = languageOf();
|
||||
if (lang === installedLang) return Menu.getApplicationMenu();
|
||||
installedLang = lang;
|
||||
const menu = Menu.buildFromTemplate(applicationMenuTemplate(lang));
|
||||
Menu.setApplicationMenu(menu);
|
||||
return menu;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { contextTemplate, editMenuTemplate, applicationMenuTemplate, menuInstaller };
|
||||
module.exports = { contextTemplate };
|
||||
@@ -5,42 +5,22 @@ const PROTOCOL_VERSION = 1;
|
||||
// A child that never sends a handshake must not be able to grow this process
|
||||
// while it waits, nor hold the launch open forever.
|
||||
const HANDSHAKE_LIMIT = 4096;
|
||||
const HANDSHAKE_TIMEOUT_MS = 60000;
|
||||
const SLOW_AFTER_MS = 3000;
|
||||
|
||||
// HostStartError says why a launch has no kernel. code is one of "timeout",
|
||||
// "exited", "spawn", "overflow" or "malformed"; callers branch on it, never on
|
||||
// the sentence.
|
||||
class HostStartError extends Error {
|
||||
constructor(code, message, detail = {}) {
|
||||
super(message);
|
||||
this.name = "HostStartError";
|
||||
this.code = code;
|
||||
Object.assign(this, detail);
|
||||
}
|
||||
}
|
||||
const HANDSHAKE_TIMEOUT_MS = 20000;
|
||||
|
||||
// start spawns the kernel and reads the handshake it writes to stdout. Every
|
||||
// line after it is an act a handover asks of this process, because that pipe is
|
||||
// the only channel pointing this way. stdin is the lease: the kernel drains
|
||||
// when this end closes, which is what stops it outliving a parent that exited
|
||||
// without being asked to.
|
||||
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 });
|
||||
function start(binary, args, { onStderr, onExit, onAct } = {}) {
|
||||
const child = spawn(binary, args, { stdio: ["pipe", "pipe", "pipe"] });
|
||||
child.stderr.setEncoding("utf8");
|
||||
if (onStderr) child.stderr.on("data", onStderr);
|
||||
if (onExit) child.on("exit", onExit);
|
||||
const ready = firstLine(child, { onSlow, timeoutMs }).then(({ line, rest }) => {
|
||||
const ready = firstLine(child).then(({ line, rest }) => {
|
||||
// Only once the handshake parses: a child whose first line was not one has
|
||||
// said nothing this process should act on.
|
||||
let handshake;
|
||||
try {
|
||||
handshake = parse(line);
|
||||
} catch (err) {
|
||||
throw new HostStartError("malformed", err.message);
|
||||
}
|
||||
const handshake = parse(line);
|
||||
if (onAct) readActs(child, rest, onAct);
|
||||
return handshake;
|
||||
});
|
||||
@@ -80,31 +60,19 @@ function readActs(child, rest, onAct) {
|
||||
drain();
|
||||
}
|
||||
|
||||
function firstLine(child, { onSlow, timeoutMs }) {
|
||||
function firstLine(child) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let buffered = "";
|
||||
const began = Date.now();
|
||||
const elapsed = () => Date.now() - began;
|
||||
const settle = (fn, value) => {
|
||||
clearTimeout(timer);
|
||||
clearTimeout(slow);
|
||||
child.stdout.off("data", onData);
|
||||
child.off("exit", onEarlyExit);
|
||||
child.off("error", onSpawnFailure);
|
||||
fn(value);
|
||||
};
|
||||
const slow = onSlow ? setTimeout(onSlow, Math.min(SLOW_AFTER_MS, timeoutMs)) : undefined;
|
||||
const timer = setTimeout(
|
||||
() =>
|
||||
settle(
|
||||
reject,
|
||||
new HostStartError(
|
||||
"timeout",
|
||||
`the kernel was still running but sent no handshake within ${Math.round(timeoutMs / 1000)} s`,
|
||||
{ elapsedMs: elapsed() },
|
||||
),
|
||||
),
|
||||
timeoutMs,
|
||||
() => settle(reject, new Error("the kernel sent no handshake")),
|
||||
HANDSHAKE_TIMEOUT_MS,
|
||||
);
|
||||
const onData = (chunk) => {
|
||||
buffered += chunk;
|
||||
@@ -113,22 +81,15 @@ function firstLine(child, { onSlow, timeoutMs }) {
|
||||
return settle(resolve, { line: buffered.slice(0, end), rest: buffered.slice(end + 1) });
|
||||
}
|
||||
if (buffered.length > HANDSHAKE_LIMIT) {
|
||||
settle(reject, new HostStartError("overflow", "the handshake ran past its limit"));
|
||||
settle(reject, new Error("the handshake ran past its limit"));
|
||||
}
|
||||
};
|
||||
const onEarlyExit = (code, signal) =>
|
||||
settle(
|
||||
reject,
|
||||
new HostStartError("exited", `the kernel exited with ${code ?? `signal ${signal}`} before saying anything`, {
|
||||
exitCode: code,
|
||||
signal,
|
||||
elapsedMs: elapsed(),
|
||||
}),
|
||||
);
|
||||
const onEarlyExit = (code) =>
|
||||
settle(reject, new Error(`the kernel exited with ${code} before saying anything`));
|
||||
// A program that could not be started emits this and never exits, so without
|
||||
// it the launch waited out the handshake timeout and then reported the
|
||||
// kernel as silent — which sent the reader looking at the kernel.
|
||||
const onSpawnFailure = (err) => settle(reject, new HostStartError("spawn", `the kernel could not be started: ${err.message}`, { elapsedMs: elapsed() }));
|
||||
const onSpawnFailure = (err) => settle(reject, new Error(`the kernel could not be started: ${err.message}`));
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stdout.on("data", onData);
|
||||
child.on("exit", onEarlyExit);
|
||||
@@ -167,12 +128,4 @@ function parse(line) {
|
||||
return { origin: url.origin, token: body.token };
|
||||
}
|
||||
|
||||
const RETRY_WITHIN_MS = 10000;
|
||||
|
||||
// shouldRetry allows one more attempt only for a kernel that stopped, or could
|
||||
// not be run, soon after it was started.
|
||||
function shouldRetry(err, attempt, elapsedMs) {
|
||||
return attempt < 2 && (err?.code === "exited" || err?.code === "spawn") && elapsedMs <= RETRY_WITHIN_MS;
|
||||
}
|
||||
|
||||
module.exports = { shouldRetry, start, parse, readActs, HostStartError, PROTOCOL_VERSION, HANDSHAKE_TIMEOUT_MS };
|
||||
module.exports = { start, parse, readActs, PROTOCOL_VERSION };
|
||||
@@ -7,17 +7,15 @@ if (relaunchForOzonePlatform(app, process)) return;
|
||||
const fs = require("node:fs/promises");
|
||||
const { existsSync } = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { start, shouldRetry, HANDSHAKE_TIMEOUT_MS } = require("./host");
|
||||
const { showStarting } = require("./starting");
|
||||
const { start } = require("./host");
|
||||
const { StudioHost } = require("./hostclient");
|
||||
const { installTray } = require("./tray");
|
||||
const { instanceID, profileFor } = require("./instance");
|
||||
const { installApplicationMenu, installContextMenu } = require("./menu");
|
||||
const { uiLanguage } = require("./uilang");
|
||||
const { installFullScreenKey } = require("./fullscreen");
|
||||
const { installReload } = require("./reload");
|
||||
const { externalTarget } = require("./links");
|
||||
const { reveal, revealWorkspace } = require("./reveal");
|
||||
const { reveal } = require("./reveal");
|
||||
const { appIcon } = require("./appicon");
|
||||
const layout = require("./layout");
|
||||
const { offerCleanup } = require("./legacy");
|
||||
@@ -25,7 +23,7 @@ const { stripPackageGrants, unpaintedWindowCause } = require("./packagegrants");
|
||||
const { BrowserProtocol } = require("./browserprotocol");
|
||||
const { BrowserViews } = require("./browserviews");
|
||||
const { startBrowserRelay } = require("./browserrelay");
|
||||
const { loadPrefs, prefsFile, registerPrefs } = require("./prefs");
|
||||
const { prefsFile, registerPrefs } = require("./prefs");
|
||||
const { openLogs, redactArgv, failStartup } = require("./shelllog");
|
||||
|
||||
// A page in a minimized or fully covered window counts as hidden, and a hidden
|
||||
@@ -47,7 +45,6 @@ const LIGHTS = { x: 11, y: 20 };
|
||||
const DEFAULT_SIZE = { width: 1440, height: 900 };
|
||||
const MIN_SIZE = { width: 760, height: 480 };
|
||||
const HOST_DRAIN_MS = 1000;
|
||||
const RETRY_DELAY_MS = 1000;
|
||||
|
||||
const where = {
|
||||
packaged: app.isPackaged,
|
||||
@@ -73,70 +70,6 @@ let reload = null;
|
||||
let logs = null;
|
||||
let handshaken = false;
|
||||
|
||||
let starting = null;
|
||||
function closeStarting() {
|
||||
const win = starting;
|
||||
starting = null;
|
||||
if (win && !win.isDestroyed()) win.close();
|
||||
}
|
||||
|
||||
function handshakeTimeout() {
|
||||
const ms = Number(process.env.REASONIX_STUDIO_HANDSHAKE_TIMEOUT_MS);
|
||||
return Number.isFinite(ms) && ms > 0 ? ms : HANDSHAKE_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
// A kernel that exits or cannot be spawned within seconds is tried once more:
|
||||
// a scanner holding a freshly written binary lets go about that fast. A slow
|
||||
// kernel is waited for instead, never restarted.
|
||||
async function launchKernel(args) {
|
||||
for (let attempt = 1; attempt <= 2; attempt++) {
|
||||
if (quitting) throw new Error("the launch was ended by quit");
|
||||
const began = Date.now();
|
||||
logs.shell.line(`host: starting ${hostBinary} (attempt ${attempt})`);
|
||||
kernel = start(hostBinary, args, {
|
||||
systemLanguage: app.getPreferredSystemLanguages()[0] ?? app.getLocale(),
|
||||
timeoutMs: handshakeTimeout(),
|
||||
onSlow: () => {
|
||||
logs.shell.line("host: no handshake yet; showing the starting window");
|
||||
if (starting || quitting) return;
|
||||
const win = showStarting(app.getLocale());
|
||||
starting = win;
|
||||
win.once("closed", () => {
|
||||
if (starting !== win) return;
|
||||
starting = null;
|
||||
logs.shell.line("host: starting window closed; ending the launch");
|
||||
app.quit();
|
||||
});
|
||||
},
|
||||
onStderr: (text) => {
|
||||
logs.host.raw(text);
|
||||
process.stderr.write(text);
|
||||
},
|
||||
onExit: (code, signal) => {
|
||||
logs.shell.line(`host: exited code=${code} signal=${signal}${handshaken ? "" : " before its handshake"}`);
|
||||
// Before the handshake the launch itself fails, and boot's catch owns
|
||||
// telling the person why; quitting here would race that dialog.
|
||||
if (handshaken && code !== 0 && !quitting) app.quit();
|
||||
},
|
||||
onAct: handOver,
|
||||
});
|
||||
const current = kernel;
|
||||
current.child.on("error", (err) => logs.shell.line(`host: spawn failed: ${err.message}`));
|
||||
current.child.stderr.on("close", () => logs.host.flush());
|
||||
try {
|
||||
return await current.ready;
|
||||
} catch (err) {
|
||||
if (quitting || !shouldRetry(err, attempt, Date.now() - began)) {
|
||||
err.attempts = attempt;
|
||||
closeStarting();
|
||||
throw err;
|
||||
}
|
||||
logs.shell.line(`host: ${err.message}; retrying once`);
|
||||
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function boot() {
|
||||
// Which build this is belongs to the shell: inside the bundle the kernel's
|
||||
// own os.Executable() names the host binary, not the application around it.
|
||||
@@ -153,7 +86,23 @@ async function boot() {
|
||||
// inside the bundle rather than being it.
|
||||
args.push("-studio-app", process.execPath, "-studio-app-pid", String(process.pid));
|
||||
}
|
||||
const ready = await launchKernel(args);
|
||||
logs.shell.line(`host: starting ${hostBinary}`);
|
||||
kernel = start(hostBinary, args, {
|
||||
onStderr: (text) => {
|
||||
logs.host.raw(text);
|
||||
process.stderr.write(text);
|
||||
},
|
||||
onExit: (code, signal) => {
|
||||
logs.shell.line(`host: exited code=${code} signal=${signal}${handshaken ? "" : " before its handshake"}`);
|
||||
// Before the handshake the launch itself fails, and boot's catch owns
|
||||
// telling the person why; quitting here would race that dialog.
|
||||
if (handshaken && code !== 0 && !quitting) app.quit();
|
||||
},
|
||||
onAct: handOver,
|
||||
});
|
||||
kernel.child.on("error", (err) => logs.shell.line(`host: spawn failed: ${err.message}`));
|
||||
kernel.child.stderr.on("close", () => logs.host.flush());
|
||||
const ready = await kernel.ready;
|
||||
logs.addSecret(ready.token);
|
||||
handshaken = true;
|
||||
logs.shell.line(`host: handshake from ${ready.origin}`);
|
||||
@@ -161,14 +110,13 @@ async function boot() {
|
||||
client = new StudioHost(ready.origin, ready.token);
|
||||
await armCredential(ready);
|
||||
win = createWindow();
|
||||
closeStarting();
|
||||
guard(win.webContents);
|
||||
win.webContents.once("render-process-gone", (_event, details) => {
|
||||
if (win.isVisible() || details.reason !== "crashed") return;
|
||||
const cause = unpaintedWindowCause(grants, app.getLocale());
|
||||
if (cause) dialog.showErrorBox(cause.title, cause.detail);
|
||||
});
|
||||
installContextMenu(win.webContents, win, uiLang);
|
||||
installContextMenu(win.webContents, win);
|
||||
installFullScreenKey(win.webContents, win);
|
||||
reload = installReload(win.webContents, win);
|
||||
win.once("ready-to-show", () => win.show());
|
||||
@@ -253,7 +201,6 @@ function onWindowClose(event) {
|
||||
// showWindow brings it back from wherever it went. Show alone is a no-op on a
|
||||
// window that is merely buried, so the focus is what actually raises it.
|
||||
function showWindow() {
|
||||
if (starting && !starting.isDestroyed()) starting.focus();
|
||||
if (quitting || !win || win.isDestroyed()) return;
|
||||
if (win.isMinimized()) win.restore();
|
||||
reload?.revive();
|
||||
@@ -316,10 +263,7 @@ function fromWindow(event) {
|
||||
return win && !win.isDestroyed() && event.sender === win.webContents ? win : null;
|
||||
}
|
||||
|
||||
// The page reads navigator.languages[0], which is the first preferred system
|
||||
// language; app.getLocale() is the locale Chromium resolved, which can differ.
|
||||
const uiLang = () => uiLanguage(loadPrefs(prefsFile(app.getPath("userData"))), app.getPreferredSystemLanguages()[0] ?? app.getLocale());
|
||||
registerPrefs(ipcMain, () => prefsFile(app.getPath("userData")), fromWindow, () => installApplicationMenu(uiLang));
|
||||
registerPrefs(ipcMain, () => prefsFile(app.getPath("userData")), fromWindow);
|
||||
|
||||
ipcMain.handle("window:minimise", (event) => {
|
||||
fromWindow(event)?.minimize();
|
||||
@@ -356,11 +300,8 @@ ipcMain.handle("browser:login-answer", (event, id, username, password) => {
|
||||
ipcMain.handle("shell:open-external", (event, raw) => {
|
||||
if (!fromWindow(event)) return;
|
||||
const target = externalTarget(raw);
|
||||
if (target) return shell.openExternal(target);
|
||||
if (target) void shell.openExternal(target);
|
||||
});
|
||||
ipcMain.handle("shell:reveal-workspace", (event, root) =>
|
||||
fromWindow(event) && client ? revealWorkspace(client, shell, String(root)) : { code: "", error: "no window" },
|
||||
);
|
||||
ipcMain.handle("shell:reveal", (event, base, rel) =>
|
||||
fromWindow(event) && client ? reveal(client, shell, String(base), String(rel)) : { code: "", error: "no window" },
|
||||
);
|
||||
@@ -436,7 +377,7 @@ if (!primary) {
|
||||
|
||||
app.whenReady().then(() => {
|
||||
if (!primary) return;
|
||||
installApplicationMenu(uiLang);
|
||||
installApplicationMenu();
|
||||
boot().catch((err) => {
|
||||
console.error("reasonix-studio:", err.message);
|
||||
if (quitting) {
|
||||
@@ -482,11 +423,7 @@ function handOver(act) {
|
||||
if (act === "quit") app.quit();
|
||||
}
|
||||
|
||||
// Before the handshake the only window is the starting one, and the launch
|
||||
// decides for itself whether to go on.
|
||||
app.on("window-all-closed", () => {
|
||||
if (handshaken) app.quit();
|
||||
});
|
||||
app.on("window-all-closed", () => app.quit());
|
||||
|
||||
// Closing this end of the pipe is what tells the kernel to drain. Without it a
|
||||
// session file is left being written by a process nobody is holding open.
|
||||
|
||||
@@ -1,19 +1,31 @@
|
||||
"use strict";
|
||||
const { Menu } = require("electron");
|
||||
const { contextTemplate, menuInstaller } = require("./editmenu");
|
||||
const { contextTemplate } = require("./editmenu");
|
||||
|
||||
// Without an application menu a WebContents has no copy, paste or undo at all:
|
||||
// macOS routes those shortcuts through it, and a window with none reads as a
|
||||
// broken text editor. Elsewhere the bar would render inside the window, and
|
||||
// those platforms bind the shortcuts themselves.
|
||||
const installApplicationMenu = menuInstaller(Menu);
|
||||
function installApplicationMenu() {
|
||||
if (process.platform !== "darwin") {
|
||||
Menu.setApplicationMenu(null);
|
||||
return null;
|
||||
}
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{ role: "appMenu" },
|
||||
{ role: "editMenu" },
|
||||
{ role: "windowMenu" },
|
||||
]);
|
||||
Menu.setApplicationMenu(menu);
|
||||
return menu;
|
||||
}
|
||||
|
||||
// A production build ships without one, so a right-click in a text field offered
|
||||
// nothing at all. Roles rather than handlers: the clipboard work belongs to the
|
||||
// platform, and editFlags is the page's own account of what is possible here.
|
||||
function installContextMenu(contents, window, languageOf = () => "en") {
|
||||
function installContextMenu(contents, window) {
|
||||
contents.on("context-menu", (_event, params) => {
|
||||
const template = contextTemplate(params, languageOf());
|
||||
const template = contextTemplate(params);
|
||||
if (!template.length) return;
|
||||
Menu.buildFromTemplate(template).popup({ window });
|
||||
});
|
||||
|
||||
@@ -44,7 +44,7 @@ function savePrefs(file, prefs) {
|
||||
// in the same process and must not reach this file.
|
||||
// fileOf is asked on each use: the profile directory is settled during boot,
|
||||
// after these handlers exist.
|
||||
function registerPrefs(ipcMain, fileOf, fromWindow, onSave = () => {}) {
|
||||
function registerPrefs(ipcMain, fileOf, fromWindow) {
|
||||
ipcMain.on("prefs:load", (event) => {
|
||||
event.returnValue = fromWindow(event) ? loadPrefs(fileOf()) : {};
|
||||
});
|
||||
@@ -52,7 +52,6 @@ function registerPrefs(ipcMain, fileOf, fromWindow, onSave = () => {}) {
|
||||
if (fromWindow(event)) {
|
||||
try {
|
||||
savePrefs(fileOf(), prefs);
|
||||
onSave();
|
||||
} catch {
|
||||
// A preference that did not save is one relaunch of lost state, not a
|
||||
// reason to fail the window.
|
||||
|
||||
@@ -28,8 +28,6 @@ contextBridge.exposeInMainWorld("reasonixHost", {
|
||||
openExternal: (url) => ipcRenderer.invoke("shell:open-external", String(url)),
|
||||
// A pane and a path inside its workspace; main asks the kernel where that is.
|
||||
revealPath: (base, rel) => ipcRenderer.invoke("shell:reveal", String(base), String(rel)),
|
||||
// A project the sidebar lists, which may have no pane at all.
|
||||
revealWorkspace: (root) => ipcRenderer.invoke("shell:reveal-workspace", String(root)),
|
||||
// Where a dropped file lives. Resolved here rather than in the page: the
|
||||
// renderer is handed a File and never a path, and a turn that has to work on
|
||||
// the file itself cannot do it on a copy of the bytes.
|
||||
|
||||
@@ -1,38 +1,22 @@
|
||||
"use strict";
|
||||
|
||||
// A page gone blank, whether its tree unmounted or its renderer died, has no
|
||||
// way back but quitting. The kernel owns every running turn and a loaded page
|
||||
// finds it again. F5 and Ctrl/Cmd+R belong to the page, so the recovery chord
|
||||
// is the shifted one.
|
||||
// No menu this shell installs carries a reload role, and a page gone blank,
|
||||
// whether its tree unmounted or its renderer died, has no way back but
|
||||
// quitting. The kernel owns every running turn and a loaded page finds it again.
|
||||
const CRASH_RELOADS = 3;
|
||||
const CRASH_WINDOW_MS = 60_000;
|
||||
|
||||
function reloadAction(input, platform = process.platform) {
|
||||
if (input.type !== "keyDown" || input.isAutoRepeat || input.alt) return null;
|
||||
const mod = platform === "darwin" ? input.meta && !input.control : input.control && !input.meta;
|
||||
const latin = typeof input.key === "string" && /^[a-z]$/i.test(input.key);
|
||||
const isR = latin ? input.key.toLowerCase() === "r" : input.code === "KeyR";
|
||||
if (mod && isR) return input.shift ? "hard" : "reload";
|
||||
if (!input.control && !input.meta && !input.shift && input.key === "F5") return "reload";
|
||||
return null;
|
||||
}
|
||||
|
||||
function installPaneReload(contents, platform = process.platform) {
|
||||
contents.on("before-input-event", (event, input) => {
|
||||
const action = reloadAction(input, platform);
|
||||
if (!action) return;
|
||||
event.preventDefault();
|
||||
if (action === "hard") contents.reloadIgnoringCache();
|
||||
else contents.reload();
|
||||
});
|
||||
}
|
||||
|
||||
// A dead renderer's input never reaches before-input-event, so the crash itself
|
||||
// has to trigger the reload, or for a hidden window the next revive before it is
|
||||
// shown. A page that dies on every load stops being reloaded.
|
||||
function installReload(contents, window, { platform = process.platform, now = Date.now } = {}) {
|
||||
const mac = platform === "darwin";
|
||||
contents.on("before-input-event", (event, input) => {
|
||||
if (reloadAction(input, platform) !== "hard") return;
|
||||
if (input.type !== "keyDown" || input.isAutoRepeat || input.alt || input.shift) return;
|
||||
const mod = mac ? input.meta && !input.control : input.control && !input.meta;
|
||||
const bare = !input.control && !input.meta;
|
||||
const chord = mod && (input.key === "r" || input.key === "R" || input.code === "KeyR");
|
||||
if (!chord && !(bare && input.key === "F5")) return;
|
||||
event.preventDefault();
|
||||
contents.reload();
|
||||
});
|
||||
@@ -56,4 +40,4 @@ function installReload(contents, window, { platform = process.platform, now = Da
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { installReload, installPaneReload, reloadAction, CRASH_RELOADS, CRASH_WINDOW_MS };
|
||||
module.exports = { installReload, CRASH_RELOADS, CRASH_WINDOW_MS };
|
||||
@@ -23,13 +23,9 @@ function local(target, platform) {
|
||||
async function locate(client, base, rel, platform) {
|
||||
if (!PANE_BASE.test(base)) return { refusal: refused("not a pane this window publishes") };
|
||||
const query = rel ? `?path=${encodeURIComponent(rel)}` : "";
|
||||
return ask(client, `${base}/workspace/locate${query}`, platform);
|
||||
}
|
||||
|
||||
async function ask(client, url, platform) {
|
||||
let res;
|
||||
try {
|
||||
res = await client.request("GET", url);
|
||||
res = await client.request("GET", `${base}/workspace/locate${query}`);
|
||||
} catch (err) {
|
||||
return { refusal: refused(err.message) };
|
||||
}
|
||||
@@ -58,13 +54,4 @@ async function reveal(client, shell, base, rel, platform = process.platform) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// A project the sidebar lists needs no pane: the hub answers for the folders
|
||||
// it lists and refuses any other, so the page still names no location itself.
|
||||
async function revealWorkspace(client, shell, root, platform = process.platform) {
|
||||
const found = await ask(client, `/host/workspaces/locate?root=${encodeURIComponent(String(root))}`, platform);
|
||||
if (found.refusal) return found.refusal;
|
||||
shell.showItemInFolder(found.target);
|
||||
return null;
|
||||
}
|
||||
|
||||
module.exports = { reveal, revealWorkspace };
|
||||
module.exports = { reveal };
|
||||
@@ -159,29 +159,17 @@ function openLogs(userData, options = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
const HINTS = {
|
||||
timeout: [
|
||||
"The kernel started but did not finish starting in time. Security software scanning a new or updated install is a common cause; starting Studio again usually works.",
|
||||
"内核已启动,但没有在限定时间内完成启动。安全软件扫描新安装或刚更新的程序是常见原因;再次启动通常可以。",
|
||||
],
|
||||
exited: [
|
||||
"The kernel process stopped before it was ready. Anything it printed is above; host.log has the rest.",
|
||||
"内核进程在就绪前就停止了。它输出过的内容在上面,host.log 里有更多。",
|
||||
],
|
||||
spawn: [
|
||||
"The kernel program could not be run. Security software blocking or locking it is a common cause.",
|
||||
"无法运行内核程序。安全软件拦截或占用它是常见原因。",
|
||||
],
|
||||
};
|
||||
|
||||
function startupFailure(locale, reason, logDir, code) {
|
||||
const zh = String(locale).toLowerCase().startsWith("zh");
|
||||
const hint = HINTS[code]?.[zh ? 1 : 0];
|
||||
const body = hint ? `${reason}\n\n${hint}` : reason;
|
||||
if (zh) {
|
||||
return { title: "Reasonix Studio 无法启动", detail: `${body}\n\n日志位于:\n${logDir}` };
|
||||
function startupFailure(locale, reason, logDir) {
|
||||
if (String(locale).toLowerCase().startsWith("zh")) {
|
||||
return {
|
||||
title: "Reasonix Studio 无法启动",
|
||||
detail: `${reason}\n\n日志位于:\n${logDir}`,
|
||||
};
|
||||
}
|
||||
return { title: "Reasonix Studio could not start", detail: `${body}\n\nLogs are in:\n${logDir}` };
|
||||
return {
|
||||
title: "Reasonix Studio could not start",
|
||||
detail: `${reason}\n\nLogs are in:\n${logDir}`,
|
||||
};
|
||||
}
|
||||
|
||||
// failStartup is the one way a launch that cannot come up ends: recorded, told
|
||||
@@ -189,9 +177,8 @@ function startupFailure(locale, reason, logDir, code) {
|
||||
// words when it never finished its handshake, and is already redacted.
|
||||
function failStartup({ app, dialog, logs, locale }, err, hostOutput = "") {
|
||||
logs.shell.line(`startup failed: ${err?.stack || err}`);
|
||||
const tried = err?.attempts > 1 ? ` (after ${err.attempts} attempts)` : "";
|
||||
const reason = [`${String(err?.message || err)}${tried}`, hostOutput].filter(Boolean).join("\n\n");
|
||||
const text = startupFailure(locale, logs.redact(reason), logs.dir, err?.code);
|
||||
const reason = [String(err?.message || err), hostOutput].filter(Boolean).join("\n\n");
|
||||
const text = startupFailure(locale, logs.redact(reason), logs.dir);
|
||||
try {
|
||||
dialog.showErrorBox(text.title, text.detail);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
"use strict";
|
||||
const { BrowserWindow } = require("electron");
|
||||
|
||||
const TEXT = {
|
||||
en: ["Reasonix Studio is starting", "The first start after installing or rebooting can take a minute."],
|
||||
zh: ["Reasonix Studio 正在启动", "安装或重启电脑后的首次启动可能需要一分钟。"],
|
||||
};
|
||||
|
||||
function startingPage(locale) {
|
||||
const [title, note] = String(locale).toLowerCase().startsWith("zh") ? TEXT.zh : TEXT.en;
|
||||
return `<!doctype html><meta charset="utf-8"><title>${title}</title><style>
|
||||
:root{color-scheme:light dark;--bg:#fafafa;--fg:#1c1c1e;--mute:#6b6b70}
|
||||
@media(prefers-color-scheme:dark){:root{--bg:#1b1b1d;--fg:#ececee;--mute:#9a9aa0}}
|
||||
body{margin:0;height:100vh;display:flex;flex-direction:column;justify-content:center;gap:8px;padding:0 28px;box-sizing:border-box;background:var(--bg);color:var(--fg);font:14px system-ui,sans-serif}
|
||||
h1{margin:0;font-size:16px;font-weight:600}p{margin:0;color:var(--mute);line-height:1.5}
|
||||
.bar{height:3px;margin-top:10px;border-radius:2px;background:var(--mute);opacity:.35;overflow:hidden;position:relative}
|
||||
.bar::after{content:"";position:absolute;inset:0 70% 0 0;background:var(--fg);animation:run 1.4s ease-in-out infinite}
|
||||
@keyframes run{from{transform:translateX(-100%)}to{transform:translateX(340%)}}
|
||||
</style><h1>${title}</h1><p>${note}</p><div class="bar"></div>`;
|
||||
}
|
||||
|
||||
// showStarting is the only sign of life while the kernel has not answered: until
|
||||
// it does there is no window of the application's own to draw.
|
||||
function showStarting(locale) {
|
||||
const win = new BrowserWindow({
|
||||
width: 380,
|
||||
height: 150,
|
||||
resizable: false,
|
||||
minimizable: false,
|
||||
maximizable: false,
|
||||
frame: true,
|
||||
title: "Reasonix Studio",
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: { sandbox: true, contextIsolation: true, nodeIntegration: false },
|
||||
});
|
||||
win.removeMenu();
|
||||
void win.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(startingPage(locale))}`);
|
||||
return win;
|
||||
}
|
||||
|
||||
module.exports = { showStarting, startingPage };
|
||||
@@ -1,17 +1,18 @@
|
||||
"use strict";
|
||||
const { Menu, Tray, nativeImage, screen } = require("electron");
|
||||
const { trayImage } = require("./trayimage.js");
|
||||
const path = require("node:path");
|
||||
const { Menu, Tray, nativeImage } = require("electron");
|
||||
|
||||
// How often the fold is re-read. It is a projection the kernel recomputes on
|
||||
// demand, so a missed read costs nothing the next one does not restore — which
|
||||
// is why it is pulled on a timer rather than carried on the stream.
|
||||
const FOLD_INTERVAL_MS = 5000;
|
||||
const ICON = path.join(__dirname, "..", "assets", "tray.png");
|
||||
|
||||
// installTray brings the icon up and reports whether there is one. A shell with
|
||||
// no icon must never background its window: hiding it would leave a running
|
||||
// session with nothing left to bring it back.
|
||||
function installTray(host, { onOpen, onQuit }) {
|
||||
const image = trayImage(nativeImage, screen.getPrimaryDisplay().scaleFactor);
|
||||
const image = nativeImage.createFromPath(ICON);
|
||||
if (image.isEmpty()) return null;
|
||||
const icon = new Tray(image);
|
||||
const state = { fold: null, prefs: null };
|
||||
@@ -46,9 +47,6 @@ function installTray(host, { onOpen, onQuit }) {
|
||||
);
|
||||
};
|
||||
|
||||
const refit = () => icon.setImage(trayImage(nativeImage, screen.getPrimaryDisplay().scaleFactor));
|
||||
if (process.platform === "win32") screen.on("display-metrics-changed", refit);
|
||||
|
||||
icon.on("click", onOpen);
|
||||
const timer = setInterval(() => void refresh(), FOLD_INTERVAL_MS);
|
||||
|
||||
@@ -70,7 +68,6 @@ function installTray(host, { onOpen, onQuit }) {
|
||||
refresh,
|
||||
close() {
|
||||
clearInterval(timer);
|
||||
screen.removeListener("display-metrics-changed", refit);
|
||||
icon.destroy();
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
"use strict";
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
|
||||
const ASSETS = path.join(__dirname, "..", "assets");
|
||||
const SCALES = [
|
||||
[1, ""],
|
||||
[1.25, "@1.25x"],
|
||||
[1.5, "@1.5x"],
|
||||
[1.75, "@1.75x"],
|
||||
[2, "@2x"],
|
||||
[2.5, "@2.5x"],
|
||||
[3, "@3x"],
|
||||
];
|
||||
|
||||
function trayAsset(scaleFactor) {
|
||||
const want = Number.isFinite(scaleFactor) && scaleFactor > 0 ? scaleFactor : 1;
|
||||
const hit = SCALES.find(([scale]) => scale >= want - 1e-6) ?? SCALES[SCALES.length - 1];
|
||||
return { file: path.join(ASSETS, `tray${hit[1]}.png`), pixels: Math.round(16 * hit[0]) };
|
||||
}
|
||||
|
||||
// Windows rebuilds the tray icon from the 1x bitmap, resizing it to the small
|
||||
// icon metric, so the file is loaded as 1x pixels of exactly that size. Other
|
||||
// platforms choose among the @Nx files themselves.
|
||||
function trayImage(nativeImage, scaleFactor) {
|
||||
if (process.platform !== "win32") return nativeImage.createFromPath(path.join(ASSETS, "tray.png"));
|
||||
return nativeImage.createFromBuffer(fs.readFileSync(trayAsset(scaleFactor).file));
|
||||
}
|
||||
|
||||
module.exports = { trayAsset, trayImage, SCALES };
|
||||
@@ -1,17 +0,0 @@
|
||||
"use strict";
|
||||
|
||||
// The interface language the page will draw in, decided from the same two
|
||||
// facts it uses: its saved choice (rx-lang) and, for anything but an explicit
|
||||
// zh or en, the machine's first locale.
|
||||
function isChinese(tag) {
|
||||
const s = String(tag ?? "").toLowerCase();
|
||||
return s.startsWith("zh") || s.startsWith("yue") || s.startsWith("cmn") || s.includes("hans") || s.includes("hant");
|
||||
}
|
||||
|
||||
function uiLanguage(prefs, machineLocale) {
|
||||
const want = String(prefs?.["rx-lang"] ?? "").trim().toLowerCase();
|
||||
if (want === "zh" || want === "en") return want;
|
||||
return isChinese(machineLocale) ? "zh" : "en";
|
||||
}
|
||||
|
||||
module.exports = { uiLanguage };
|
||||
@@ -303,11 +303,11 @@ async function menuChecks(win, js) {
|
||||
const menu = Menu.getApplicationMenu();
|
||||
const roles = (menu?.items ?? []).map((i) => String(i.role).toLowerCase());
|
||||
check("the application menu is installed", !!menu);
|
||||
check("it carries the three menus macOS routes through", ["appmenu", "windowmenu"].every((r) => roles.includes(r)) && menu.items.length === 3, roles);
|
||||
check("it carries the three menus macOS routes through", ["appmenu", "editmenu", "windowmenu"].every((r) => roles.includes(r)), roles);
|
||||
|
||||
const edit = (menu?.items ?? [])[1];
|
||||
const edit = (menu?.items ?? []).find((i) => String(i.role).toLowerCase() === "editmenu");
|
||||
const editRoles = (edit?.submenu?.items ?? []).map((i) => String(i.role).toLowerCase());
|
||||
check("the edit menu carries every editing command", ["undo", "redo", "cut", "copy", "paste", "pasteandmatchstyle", "delete", "selectall"].every((r) => editRoles.includes(r)), editRoles);
|
||||
check("the edit menu carries every editing command", ["undo", "redo", "cut", "copy", "paste", "selectall"].every((r) => editRoles.includes(r)), editRoles);
|
||||
|
||||
app.focus({ steal: true });
|
||||
win.focus();
|
||||
|
||||
@@ -6,9 +6,8 @@ import os from "node:os";
|
||||
import fs from "node:fs";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { parse, readActs, start } = require("../src/host.js");
|
||||
const { contextTemplate, editMenuTemplate, applicationMenuTemplate, menuInstaller } = require("../src/editmenu.js");
|
||||
const { uiLanguage } = require("../src/uilang.js");
|
||||
const { parse, readActs } = require("../src/host.js");
|
||||
const { contextTemplate } = require("../src/editmenu.js");
|
||||
const { externalTarget } = require("../src/links.js");
|
||||
const { offerCleanup, ownBundle } = require("../src/legacy.js");
|
||||
const { stripPackageGrants, readReport, unpaintedWindowCause } = require("../src/packagegrants.js");
|
||||
@@ -17,23 +16,6 @@ const { pick, loadPrefs, savePrefs, registerPrefs } = require("../src/prefs.js")
|
||||
const TOKEN = "a".repeat(64);
|
||||
const line = (over) => JSON.stringify({ version: 1, origin: "http://127.0.0.1:8080", token: TOKEN, ...over });
|
||||
|
||||
test("the spawned host receives the system language and inherits explicit locale overrides", async () => {
|
||||
const script = `console.log(${JSON.stringify(line())});
|
||||
console.log(JSON.stringify({act: JSON.stringify({system: process.env.REASONIX_SYSTEM_LANG,
|
||||
explicit: process.env.REASONIX_LANG})}));`;
|
||||
const before = process.env.REASONIX_SYSTEM_LANG;
|
||||
const explicit = process.env.REASONIX_LANG;
|
||||
const state = new Promise((resolve) => {
|
||||
const host = start(process.execPath, ["-e", script], {
|
||||
systemLanguage: "zh-Hant-TW",
|
||||
onAct: (act) => resolve(JSON.parse(act)),
|
||||
});
|
||||
host.ready.catch(resolve);
|
||||
});
|
||||
assert.deepEqual(await state, { system: "zh-Hant-TW", ...(explicit === undefined ? {} : { explicit }) });
|
||||
assert.equal(process.env.REASONIX_SYSTEM_LANG, before);
|
||||
});
|
||||
|
||||
test("the handshake is accepted only when every field accounts for itself", () => {
|
||||
assert.deepEqual(parse(line()), { origin: "http://127.0.0.1:8080", token: TOKEN });
|
||||
|
||||
@@ -81,68 +63,6 @@ test("the context menu mirrors what the page says is possible", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("the edit menu's labels follow the interface language, not the system's", () => {
|
||||
const params = { isEditable: true, selectionText: "", editFlags: {} };
|
||||
const labels = (lang) => contextTemplate(params, lang).filter((i) => i.role).map((i) => i.label);
|
||||
assert.deepEqual(labels("zh"), ["撤销", "重做", "剪切", "复制", "粘贴", "全选"]);
|
||||
assert.deepEqual(labels("en"), ["Undo", "Redo", "Cut", "Copy", "Paste", "Select All"]);
|
||||
assert.equal(editMenuTemplate("zh").label, "编辑");
|
||||
});
|
||||
|
||||
test("the edit menu keeps everything the platform's own one carries", () => {
|
||||
const roles = (items) => items.flatMap((i) => [i.role, ...(i.submenu ? roles(i.submenu) : [])]).filter(Boolean);
|
||||
const want = ["undo", "redo", "cut", "copy", "paste", "pasteAndMatchStyle", "delete", "selectAll",
|
||||
"showSubstitutions", "toggleSmartQuotes", "toggleSmartDashes", "toggleTextReplacement", "startSpeaking", "stopSpeaking"];
|
||||
for (const lang of ["zh", "en"]) {
|
||||
assert.deepEqual(roles(editMenuTemplate(lang).submenu), want);
|
||||
const labels = (items) => items.flatMap((i) => [i.label, ...(i.submenu ? labels(i.submenu) : [])]).filter((l) => l !== undefined);
|
||||
assert.ok(labels(editMenuTemplate(lang).submenu).every((l) => l.length > 0));
|
||||
}
|
||||
assert.deepEqual(editMenuTemplate("en").submenu.map((i) => i.role ?? i.type ?? i.label),
|
||||
["undo", "redo", "separator", "cut", "copy", "paste", "pasteAndMatchStyle", "delete", "selectAll", "separator", "Substitutions", "Speech"]);
|
||||
assert.deepEqual(applicationMenuTemplate("zh").map((i) => i.role ?? "edit"), ["appMenu", "edit", "windowMenu"]);
|
||||
});
|
||||
|
||||
test("the application menu is rebuilt when the language changes and not otherwise", () => {
|
||||
const built = [];
|
||||
const Menu = {
|
||||
buildFromTemplate: (t) => (built.push(t), { template: t }),
|
||||
setApplicationMenu: () => {},
|
||||
getApplicationMenu: () => null,
|
||||
};
|
||||
const install = menuInstaller(Menu, "darwin");
|
||||
let lang = "en";
|
||||
install(() => lang);
|
||||
install(() => lang);
|
||||
assert.equal(built.length, 1);
|
||||
lang = "zh";
|
||||
install(() => lang);
|
||||
assert.equal(built.length, 2);
|
||||
assert.equal(built[1][1].label, "编辑");
|
||||
let cleared = 0;
|
||||
menuInstaller({ ...Menu, setApplicationMenu: () => cleared++ }, "linux")(() => "zh");
|
||||
assert.equal(cleared, 1);
|
||||
assert.equal(built.length, 2);
|
||||
});
|
||||
|
||||
test("the interface language is the page's own choice, else the machine's", () => {
|
||||
assert.equal(uiLanguage({ "rx-lang": "zh" }, "en-US"), "zh");
|
||||
assert.equal(uiLanguage({ "rx-lang": "en" }, "zh-CN"), "en");
|
||||
assert.equal(uiLanguage({ "rx-lang": "" }, "zh-Hans-CN"), "zh");
|
||||
assert.equal(uiLanguage({}, "fr-FR"), "en");
|
||||
assert.equal(uiLanguage(undefined, undefined), "en");
|
||||
});
|
||||
|
||||
test("a saved preference tells the shell so it can follow it", () => {
|
||||
const handlers = {};
|
||||
const ipc = { on: (name, fn) => { handlers[name] = fn; } };
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rx-prefs-"));
|
||||
let saved = 0;
|
||||
registerPrefs(ipc, () => path.join(dir, "p.json"), () => true, () => { saved++; });
|
||||
handlers["prefs:save"]({ returnValue: null }, { "rx-lang": "zh" });
|
||||
assert.equal(saved, 1);
|
||||
});
|
||||
|
||||
test("only http and https ever reach the platform opener", () => {
|
||||
assert.equal(externalTarget("https://example.test/a"), "https://example.test/a");
|
||||
assert.equal(externalTarget("http://example.test/"), "http://example.test/");
|
||||
@@ -204,7 +124,7 @@ test("an unreachable kernel is an answer, not a crash", async () => {
|
||||
assert.equal(await dead.trayState(), null);
|
||||
});
|
||||
|
||||
const { reveal, revealWorkspace } = require("../src/reveal.js");
|
||||
const { reveal } = require("../src/reveal.js");
|
||||
|
||||
// A kernel that answers /workspace/locate as the test says, and a shell that
|
||||
// only records what it was asked to open.
|
||||
@@ -223,7 +143,7 @@ async function revealRig(answer, platform = process.platform) {
|
||||
openPath: async (p) => (opened.push(["open", p]), ""),
|
||||
showItemInFolder: (p) => opened.push(["select", p]),
|
||||
};
|
||||
return { asked, opened, run: (base, rel) => reveal(client, shell, base, rel, platform), workspace: (root) => revealWorkspace(client, shell, root, platform), close: () => server.close() };
|
||||
return { asked, opened, run: (base, rel) => reveal(client, shell, base, rel, platform), close: () => server.close() };
|
||||
}
|
||||
|
||||
const ROOT = path.resolve(os.tmpdir(), "rx-workspace");
|
||||
@@ -274,22 +194,6 @@ test("the page cannot steer reveal to a location the kernel did not name", async
|
||||
}
|
||||
});
|
||||
|
||||
test("a listed project is shown through the hub and only on its answer", async () => {
|
||||
const rig = await revealRig((url) =>
|
||||
url.pathname === "/host/workspaces/locate" && url.searchParams.get("root") === "/work/a b"
|
||||
? [200, { path: path.join(ROOT, "a b"), dir: true }]
|
||||
: [404, { code: "workspace.not_listed", error: "not listed" }],
|
||||
);
|
||||
try {
|
||||
assert.equal(await rig.workspace("/work/a b"), null);
|
||||
assert.equal((await rig.workspace("/etc")).code, "workspace.not_listed");
|
||||
assert.deepEqual(rig.opened, [["select", path.join(ROOT, "a b")]]);
|
||||
assert.equal(rig.asked[0], "/host/workspaces/locate?root=%2Fwork%2Fa%20b");
|
||||
} finally {
|
||||
rig.close();
|
||||
}
|
||||
});
|
||||
|
||||
// A root that is an .app bundle, or a link to one, is a directory to stat and
|
||||
// an application to `open`: handing it to openPath would launch it.
|
||||
test("the workspace root is selected, never opened", async () => {
|
||||
@@ -410,79 +314,6 @@ test("a kernel that cannot be started says so, rather than going quiet", async (
|
||||
});
|
||||
});
|
||||
|
||||
test("a launch failure carries a typed cause, and a slow kernel is waited for, not restarted", async () => {
|
||||
const { start } = require("../src/host.js");
|
||||
const node = process.execPath;
|
||||
let slow = 0;
|
||||
const silent = start(node, ["-e", "setTimeout(()=>{},5000)"], { timeoutMs: 400, onSlow: () => slow++ });
|
||||
await assert.rejects(silent.ready, (err) => {
|
||||
assert.equal(err.code, "timeout");
|
||||
assert.match(err.message, /still running/);
|
||||
return true;
|
||||
});
|
||||
silent.child.kill();
|
||||
assert.equal(slow, 1, "the slow state was not reported once");
|
||||
|
||||
const quick = start(node, ["-e", "process.exit(7)"]);
|
||||
await assert.rejects(quick.ready, (err) => err.code === "exited" && err.exitCode === 7 && /exited with 7/.test(err.message));
|
||||
|
||||
const missing = start(path.join(os.tmpdir(), "reasonix-no-such-kernel-c9"), []);
|
||||
await assert.rejects(missing.ready, (err) => err.code === "spawn");
|
||||
|
||||
const handshake = JSON.stringify({ version: 1, origin: "http://127.0.0.1:1", token: "x".repeat(32) });
|
||||
const fine = start(node, ["-e", `console.log(${JSON.stringify(handshake)});setTimeout(()=>{},300)`], { timeoutMs: 400 });
|
||||
assert.equal((await fine.ready).origin, "http://127.0.0.1:1");
|
||||
});
|
||||
|
||||
test("a retry needs an early stop, is capped at one, and never follows a timeout", () => {
|
||||
const { shouldRetry } = require("../src/host.js");
|
||||
const exited = { code: "exited" };
|
||||
assert.equal(shouldRetry(exited, 1, 500), true);
|
||||
assert.equal(shouldRetry({ code: "spawn" }, 1, 500), true);
|
||||
assert.equal(shouldRetry(exited, 1, 10001), false, "an exit after the early window was retried");
|
||||
assert.equal(shouldRetry(exited, 2, 500), false, "a second retry was allowed");
|
||||
assert.equal(shouldRetry({ code: "timeout" }, 1, 500), false);
|
||||
assert.equal(shouldRetry(new Error("x"), 1, 500), false);
|
||||
});
|
||||
|
||||
test("a handshake that does not parse is a typed failure", async () => {
|
||||
const { start } = require("../src/host.js");
|
||||
const bad = start(process.execPath, ["-e", "console.log('hello')"]);
|
||||
await assert.rejects(bad.ready, (err) => err.code === "malformed");
|
||||
});
|
||||
|
||||
test("a kernel that never answers is started once and not retried", { skip: process.platform === "win32" }, async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "reasonix-silent-"));
|
||||
const host = path.join(dir, "host.sh");
|
||||
fs.writeFileSync(host, "#!/bin/sh\nexec sleep 5\n", { mode: 0o755 });
|
||||
process.env.REASONIX_STUDIO_HANDSHAKE_TIMEOUT_MS = "400";
|
||||
const shell = loadShell({ lock: true, host });
|
||||
try {
|
||||
await shell.quitted;
|
||||
const log = shell.shellLog();
|
||||
assert.equal((log.match(/host: starting /g) || []).length, 1);
|
||||
assert.doesNotMatch(log, /retrying once/);
|
||||
assert.match(shell.calls[0][2], /still running/);
|
||||
} finally {
|
||||
delete process.env.REASONIX_STUDIO_HANDSHAKE_TIMEOUT_MS;
|
||||
shell.cleanup();
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("each failure cause gets its own explanation in both languages", () => {
|
||||
const { startupFailure } = require("../src/shelllog.js");
|
||||
const seen = new Set();
|
||||
for (const code of ["timeout", "exited", "spawn"]) {
|
||||
for (const locale of ["en", "zh-CN"]) {
|
||||
const { detail } = startupFailure(locale, "r", "/logs", code);
|
||||
assert.ok(!seen.has(detail), `${code}/${locale} repeated another cause's text`);
|
||||
seen.add(detail);
|
||||
}
|
||||
}
|
||||
assert.equal(startupFailure("en", "r", "/logs", "overflow").detail, "r\n\nLogs are in:\n/logs");
|
||||
});
|
||||
|
||||
// A fake child: only the stdout half readActs touches, and a way to push bytes
|
||||
// through it in whatever chunks the test wants -- which is the point, since the
|
||||
// bug this guards is a line split across two of them.
|
||||
@@ -1018,67 +849,30 @@ function reloadRig(platform = "darwin") {
|
||||
return { state, send, gone, revive };
|
||||
}
|
||||
|
||||
const reloadPress = (over) => ({ type: "keyDown", key: "r", code: "KeyR", control: false, alt: false, shift: false, meta: false, isAutoRepeat: false, ...over });
|
||||
const reloadMods = [
|
||||
["darwin", { meta: true }, { control: true }],
|
||||
["win32", { control: true }, { meta: true }],
|
||||
["linux", { control: true }, { meta: true }],
|
||||
];
|
||||
|
||||
test("reloadAction reads the chord, not the layout", () => {
|
||||
const { reloadAction } = require("../src/reload.js");
|
||||
for (const [platform, mod, other] of reloadMods) {
|
||||
const at = (input) => reloadAction(input, platform);
|
||||
assert.equal(at(reloadPress(mod)), "reload");
|
||||
assert.equal(at(reloadPress({ ...mod, key: "к" })), "reload");
|
||||
assert.equal(at(reloadPress({ key: "F5", code: "F5" })), "reload");
|
||||
assert.equal(at(reloadPress({ ...mod, key: "p", code: "KeyR" })), null, `${platform}: a Dvorak P reloaded`);
|
||||
assert.equal(at(reloadPress({ ...mod, key: "r", code: "KeyP" })), "reload", `${platform}: a Dvorak R did not reload`);
|
||||
assert.equal(at(reloadPress({ ...mod, shift: true })), "hard");
|
||||
assert.equal(at(reloadPress({ ...mod, shift: true, key: "R" })), "hard");
|
||||
for (const input of [
|
||||
reloadPress(), reloadPress(other), reloadPress({ ...mod, alt: true }),
|
||||
reloadPress({ ...mod, type: "keyUp" }), reloadPress({ ...mod, isAutoRepeat: true }),
|
||||
reloadPress({ ...mod, key: "t", code: "KeyT" }), reloadPress({ key: "F5", code: "F5", control: true }),
|
||||
reloadPress({ key: "F5", code: "F5", shift: true }), reloadPress({ shift: true }),
|
||||
]) assert.equal(at(input), null, `${platform}: ${JSON.stringify(input)}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("only the hard reload chord reloads the app window; F5 and Ctrl/Cmd+R reach the page", () => {
|
||||
for (const [platform, mod] of reloadMods) {
|
||||
test("the reload key brings back a window whose page stopped drawing", () => {
|
||||
const press = (over) => ({ type: "keyDown", key: "r", code: "KeyR", control: false, alt: false, shift: false, meta: false, isAutoRepeat: false, ...over });
|
||||
const cases = [
|
||||
["darwin", { meta: true }, { control: true }],
|
||||
["win32", { control: true }, { meta: true }],
|
||||
["linux", { control: true }, { meta: true }],
|
||||
];
|
||||
for (const [platform, mod, other] of cases) {
|
||||
const { state, send } = reloadRig(platform);
|
||||
send(reloadPress(mod));
|
||||
send(reloadPress({ key: "F5", code: "F5" }));
|
||||
assert.equal(state.reloads, 0, `${platform}: a plain reload key reloaded the UI`);
|
||||
assert.equal(state.prevented, 0, `${platform}: a plain reload key was swallowed`);
|
||||
send(reloadPress({ ...mod, shift: true, key: "R" }));
|
||||
assert.equal(state.reloads, 1, `${platform}: the recovery chord did not reload`);
|
||||
assert.equal(state.prevented, 1);
|
||||
}
|
||||
});
|
||||
|
||||
test("a browser pane reloads on F5 and Ctrl/Cmd+R, ignoring cache on the shifted chord, and swallows nothing else", () => {
|
||||
const { installPaneReload } = require("../src/reload.js");
|
||||
for (const [platform, mod] of reloadMods) {
|
||||
let handler;
|
||||
const calls = { reload: 0, hard: 0, prevented: 0 };
|
||||
const contents = {
|
||||
on: (name, fn) => { if (name === "before-input-event") handler = fn; },
|
||||
reload: () => { calls.reload += 1; },
|
||||
reloadIgnoringCache: () => { calls.hard += 1; },
|
||||
};
|
||||
installPaneReload(contents, platform);
|
||||
const send = (input) => handler({ preventDefault: () => { calls.prevented += 1; } }, input);
|
||||
send(reloadPress(mod));
|
||||
send(reloadPress({ key: "F5", code: "F5" }));
|
||||
assert.deepEqual(calls, { reload: 2, hard: 0, prevented: 2 }, platform);
|
||||
send(reloadPress({ ...mod, shift: true, key: "R" }));
|
||||
assert.deepEqual(calls, { reload: 2, hard: 1, prevented: 3 }, platform);
|
||||
send(reloadPress({ ...mod, key: "f", code: "KeyF" }));
|
||||
send(reloadPress({ key: "F12", code: "F12" }));
|
||||
send(reloadPress({ ...mod, alt: true }));
|
||||
assert.deepEqual(calls, { reload: 2, hard: 1, prevented: 3 }, `${platform}: another key was handled`);
|
||||
send(press(mod));
|
||||
send(press({ ...mod, key: "R" }));
|
||||
send(press({ ...mod, key: "к" }));
|
||||
send(press({ key: "F5", code: "F5" }));
|
||||
assert.equal(state.reloads, 4, `${platform}: the reload keys did not reload`);
|
||||
send(press());
|
||||
send(press(other));
|
||||
send(press({ ...mod, shift: true }));
|
||||
send(press({ ...mod, alt: true }));
|
||||
send(press({ ...mod, type: "keyUp" }));
|
||||
send(press({ ...mod, isAutoRepeat: true }));
|
||||
send(press({ ...mod, key: "t", code: "KeyT" }));
|
||||
send(press({ key: "F5", code: "F5", control: true }));
|
||||
assert.equal(state.reloads, 4, `${platform}: something other than a fresh reload press reloaded`);
|
||||
assert.equal(state.prevented, 4);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1310,7 +1104,6 @@ function loadShell({ lock, host }) {
|
||||
if (key === "getPath") return () => userData;
|
||||
if (key === "getVersion") return () => "9.9.9";
|
||||
if (key === "getLocale") return () => "en-US";
|
||||
if (key === "getPreferredSystemLanguages") return () => ["en-US"];
|
||||
if (key === "isPackaged") return false;
|
||||
if (key === "quit") return () => { calls.push(["quit"]); quit(); };
|
||||
return inert;
|
||||
@@ -1370,29 +1163,8 @@ test("a host that exits before its handshake is logged and shown, not swallowed"
|
||||
assert.match(shell.hostLog(), /bad option/, "the host's stderr never reached host.log");
|
||||
const log = shell.shellLog();
|
||||
assert.match(log, /host: exited code=9 signal=null before its handshake/);
|
||||
assert.match(log, /startup failed: HostStartError: /);
|
||||
assert.equal((log.match(/host: starting /g) || []).length, 2, "an early exit was not retried exactly once");
|
||||
assert.match(log, /retrying once/);
|
||||
assert.match(detail, /after 2 attempts/);
|
||||
assert.match(detail, /stopped before it was ready/, "no cause was given for an early exit");
|
||||
assert.match(log, /startup failed: Error: /);
|
||||
} finally {
|
||||
shell.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
test("the tray has a file for every Windows scale, at exactly 16 * scale pixels", () => {
|
||||
const { trayAsset, SCALES } = require("../src/trayimage.js");
|
||||
const dim = (f) => {
|
||||
const b = fs.readFileSync(f);
|
||||
return [b.readUInt32BE(16), b.readUInt32BE(20)];
|
||||
};
|
||||
for (const [scale] of SCALES) {
|
||||
const { file, pixels } = trayAsset(scale);
|
||||
assert.deepEqual(dim(file), [pixels, pixels], file);
|
||||
assert.equal(pixels, Math.round(16 * scale));
|
||||
}
|
||||
assert.equal(trayAsset(1.75).pixels, 28);
|
||||
assert.equal(trayAsset(1.8).pixels, 32);
|
||||
assert.equal(trayAsset(5).pixels, 48);
|
||||
assert.equal(trayAsset(NaN).pixels, 16);
|
||||
});
|
||||
@@ -29,9 +29,9 @@
|
||||
"@codemirror/search": "^6.7.2",
|
||||
"@codemirror/state": "^6.7.6",
|
||||
"@codemirror/view": "^6.43.13",
|
||||
"@lezer/highlight": "^1.2.5",
|
||||
"@lezer/highlight": "^1.2.4",
|
||||
"codemirror": "^6.0.2",
|
||||
"katex": "^0.18.10",
|
||||
"katex": "^0.18.9",
|
||||
"react": "^19.3.0",
|
||||
"react-dom": "^19.3.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
@@ -53,7 +53,7 @@
|
||||
"jsdom": "^30.1.1",
|
||||
"playwright": "^1.63.0",
|
||||
"typescript": "^7.0.2",
|
||||
"vite": "^8.3.2",
|
||||
"vitest": "^5.0.3"
|
||||
"vite": "^8.3.0",
|
||||
"vitest": "^5.0.1"
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,6 @@ node perf/pick.mjs # 补全菜单:/ 和 @ 的选中态,深浅两色下
|
||||
node perf/models.mjs # 连接面板:网关报出一百多个模型时,这一屏还能不能用
|
||||
node perf/side.mjs # 右栏:端点自己写的一句话有多长,都不该改变栏宽
|
||||
node perf/queue.mjs # 待送达:正文读不回来时,「改」不许开出一个空编辑器
|
||||
node perf/onboarding.mjs # First-run setup: zoom-aware card bounds, resize, scrolling, focus and completion
|
||||
node perf/running.mjs # 执行过程:在跑的步骤自己在动、收起后由标题接着动,步骤行没有空栏
|
||||
node perf/lang.mjs # 双语验证:英文启动、界面译文到位、切回中文
|
||||
node perf/locale.mjs # 跟随系统语言:中文各写法都归中文,其余归英文
|
||||
|
||||
@@ -4,7 +4,6 @@ import { track as trackWidth } from "../src/ui/viewport";
|
||||
import "../src/styles/tokens.css";
|
||||
import "../src/styles/app.css";
|
||||
import "../src/styles/studio.css";
|
||||
import "../src/styles/feedback.css";
|
||||
import { App } from "../src/ui/App";
|
||||
import { MockHub } from "../src/port/mock_hub";
|
||||
import { MockPort } from "../src/port/mock";
|
||||
@@ -45,12 +44,11 @@ class BenchPort extends MockPort {
|
||||
}
|
||||
|
||||
async providerSetup() {
|
||||
return ONBOARDING ? { required: true, provider: "", model: "" } : null;
|
||||
return null;
|
||||
}
|
||||
|
||||
async appearance() {
|
||||
let look = await super.appearance();
|
||||
if (query.has("zoom")) look = { ...look, zoom: Number(query.get("zoom")) };
|
||||
const look = await super.appearance();
|
||||
return PREF === null ? look : { ...look, language: PREF };
|
||||
}
|
||||
|
||||
@@ -150,7 +148,6 @@ const WORKSPACES = Number(query.get("ws") ?? 0);
|
||||
// ?pref= 是「内核记着的语言」。真机上它来自 config;这里由地址给,好让
|
||||
// 一次验证能把两侧摆成同一个值——否则 adopt 会认为本地缓存过期。
|
||||
const PREF = query.get("pref");
|
||||
const ONBOARDING = query.has("onboarding");
|
||||
const SESSIONS = Number(query.get("sess") ?? 0);
|
||||
// ?turns= is how long the conversation being opened already is.
|
||||
const TURNS = Number(query.get("turns") ?? 0);
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
// The first-run "start" button must be reachable at every window height.
|
||||
// The body clips, so a card taller than the window is only reachable if the
|
||||
// stage itself scrolls. Focus is no evidence: the browser scrolls the clipped
|
||||
// body to a focused element, so the wheel is what gets asserted.
|
||||
import { chromium } from "playwright";
|
||||
import { mkdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
const PAGE = process.env.PERF_URL ?? "http://localhost:4399/perf.html?onboarding=1";
|
||||
const SHOTS = process.env.ONBOARDING_SHOTS;
|
||||
const SIZES = [
|
||||
[1920, 700], [1600, 600], [2560, 560], [1366, 600], [1280, 540], [1440, 900], [1600, 760, 1.25],
|
||||
[760, 480], [761, 480], [760, 480, 0.8], [761, 480, 1.8], [800, 480, 1.8], [840, 560, 1.8],
|
||||
[840, 560, 1.5], [1280, 720, 1.8], [1600, 900, 1.8, [840, 560]], [840, 560, 1.8, [1600, 900]],
|
||||
];
|
||||
if (SHOTS) mkdirSync(SHOTS, { recursive: true });
|
||||
const fails = [];
|
||||
const check = (name, ok, detail = "") => {
|
||||
console.log(`${ok ? " ok" : "FAIL"} ${name}${detail ? " — " + detail : ""}`);
|
||||
if (!ok) fails.push(name);
|
||||
};
|
||||
|
||||
const browser = await chromium.launch();
|
||||
for (const scheme of ["light", "dark"]) {
|
||||
for (const [width, height, zoom = 1, resize] of SIZES) {
|
||||
const ctx = await browser.newContext({ locale: "zh-CN", viewport: { width, height }, colorScheme: scheme });
|
||||
const page = await ctx.newPage();
|
||||
page.on("pageerror", (e) => fails.push("page error: " + e.message));
|
||||
const url = new URL(PAGE);
|
||||
url.searchParams.set("zoom", String(zoom));
|
||||
await page.goto(url.href, { waitUntil: "domcontentloaded" });
|
||||
await page.waitForSelector(".onb-card");
|
||||
await page.waitForFunction((z) => Number(getComputedStyle(document.documentElement).zoom) === z, zoom);
|
||||
const tag = `${scheme} ${width}x${height}${zoom !== 1 ? ` zoom${zoom}` : ""}${resize ? ` to ${resize.join("x")}` : ""}`;
|
||||
|
||||
const fitsCard = async (label) => {
|
||||
const layout = await page.evaluate(() => {
|
||||
const stage = document.querySelector(".onb-stage");
|
||||
const shell = document.querySelector(".onb-shell");
|
||||
const r = shell.getBoundingClientRect();
|
||||
const fields = ["#onb-url", "#onb-key", ".onb-go", ".onb-note", ".onb-protocols", ".onb-pick"];
|
||||
const clipped = fields.flatMap((selector) => {
|
||||
const el = document.querySelector(selector);
|
||||
if (!el) return [];
|
||||
const box = el.getBoundingClientRect();
|
||||
return box.left < Math.max(0, r.left) - 0.5 || box.right > Math.min(innerWidth, r.right) + 0.5
|
||||
? [{ selector, left: box.left, right: box.right, shellRight: r.right }] : [];
|
||||
});
|
||||
return { clipped, columns: getComputedStyle(shell).gridTemplateColumns.split(" ").length,
|
||||
expectedColumns: stage.clientWidth <= 760 ? 1 : 2, width: stage.scrollWidth, client: stage.clientWidth };
|
||||
});
|
||||
check(`${tag} ${label}: form stays within the card and viewport`, layout.clipped.length === 0, JSON.stringify(layout.clipped));
|
||||
check(`${tag} ${label}: columns follow zoom-adjusted room`, layout.columns === layout.expectedColumns, JSON.stringify(layout));
|
||||
check(`${tag} ${label}: stage does not scroll sideways`, layout.width <= layout.client + 1);
|
||||
};
|
||||
|
||||
const reachable = async (label, enabled) => {
|
||||
const viewport = page.viewportSize();
|
||||
const go = page.locator(".onb-go");
|
||||
await page.evaluate(() => document.activeElement?.blur());
|
||||
await page.mouse.move(viewport.width / 2, viewport.height / 2);
|
||||
await page.mouse.wheel(0, 4000);
|
||||
await page.waitForTimeout(250);
|
||||
const box = await go.boundingBox();
|
||||
check(`${tag} ${label}: after wheeling to the bottom the button is in the viewport`, !!box && box.y >= 0 && box.y + box.height <= viewport.height + 0.5, JSON.stringify(box));
|
||||
const bar = await page.locator(".onb-brandbar").boundingBox();
|
||||
check(`${tag} ${label}: the title bar stays in the viewport`, !!bar && bar.y >= -0.5 && bar.y + bar.height <= viewport.height + 0.5, JSON.stringify(bar));
|
||||
if (!enabled) return;
|
||||
const hit = await page.evaluate(() => {
|
||||
const r = document.querySelector(".onb-go").getBoundingClientRect();
|
||||
const el = document.elementFromPoint(r.x + r.width / 2, r.y + r.height / 2);
|
||||
return !!el?.closest(".onb-go");
|
||||
});
|
||||
check(`${tag} ${label}: the point under its centre hits it`, hit);
|
||||
if (SHOTS) await page.screenshot({ path: join(SHOTS, `${tag.replaceAll(" ", "-")}-${label.replaceAll(" ", "-")}.png`) });
|
||||
const first = page.locator(".onb-chip").first();
|
||||
await page.evaluate(() => document.activeElement?.blur());
|
||||
await first.focus();
|
||||
await page.waitForTimeout(250);
|
||||
const [fb, bb] = [await first.boundingBox(), await page.locator(".onb-brandbar").boundingBox()];
|
||||
check(`${tag} ${label}: a focused top-of-card element sits below the title bar`, !!fb && !!bb && fb.y >= bb.y + bb.height - 0.5, JSON.stringify({ fb, bb }));
|
||||
};
|
||||
|
||||
await fitsCard("before connect");
|
||||
await reachable("before connect", false);
|
||||
await page.fill('input[placeholder="sk-…"]', "sk-test");
|
||||
await page.locator(".onb-go").click();
|
||||
await page.waitForSelector(".onb-found");
|
||||
await fitsCard("after connect");
|
||||
await reachable("after connect", true);
|
||||
if (resize) {
|
||||
await page.setViewportSize({ width: resize[0], height: resize[1] });
|
||||
await page.waitForFunction(() => {
|
||||
const stage = document.querySelector(".onb-stage");
|
||||
return Math.abs(stage.getBoundingClientRect().width - innerWidth) < 1;
|
||||
});
|
||||
await fitsCard("after resize");
|
||||
await reachable("after resize", true);
|
||||
}
|
||||
await page.locator(".onb-go").click();
|
||||
await page.waitForSelector(".app .chrome");
|
||||
check(`${tag}: Start finishes setup`, await page.locator(".onb-stage").count() === 0);
|
||||
await ctx.close();
|
||||
}
|
||||
}
|
||||
await browser.close();
|
||||
console.log(fails.length ? `\n${fails.length} failed` : "\nall passed");
|
||||
process.exit(fails.length ? 1 : 0);
|
||||
@@ -60,14 +60,14 @@ importers:
|
||||
specifier: ^6.43.13
|
||||
version: 6.43.13
|
||||
'@lezer/highlight':
|
||||
specifier: ^1.2.5
|
||||
version: 1.2.5
|
||||
specifier: ^1.2.4
|
||||
version: 1.2.4
|
||||
codemirror:
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2
|
||||
katex:
|
||||
specifier: ^0.18.10
|
||||
version: 0.18.10
|
||||
specifier: ^0.18.9
|
||||
version: 0.18.9
|
||||
react:
|
||||
specifier: ^19.3.0
|
||||
version: 19.3.0
|
||||
@@ -116,7 +116,7 @@ importers:
|
||||
version: 19.3.0(@types/[email protected])
|
||||
'@vitejs/plugin-react':
|
||||
specifier: ^6.1.1
|
||||
version: 6.1.1([email protected].2([email protected]))
|
||||
version: 6.1.1([email protected].0([email protected]))
|
||||
jsdom:
|
||||
specifier: ^30.1.1
|
||||
version: 30.1.1
|
||||
@@ -127,11 +127,11 @@ importers:
|
||||
specifier: ^7.0.2
|
||||
version: 7.0.2
|
||||
vite:
|
||||
specifier: ^8.3.2
|
||||
version: 8.3.2([email protected])
|
||||
specifier: ^8.3.0
|
||||
version: 8.3.0([email protected])
|
||||
vitest:
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3([email protected])([email protected].2([email protected]))
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1([email protected])([email protected].0([email protected]))
|
||||
|
||||
packages:
|
||||
|
||||
@@ -453,8 +453,8 @@ packages:
|
||||
'@lezer/[email protected]':
|
||||
resolution: {integrity: sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==}
|
||||
|
||||
'@lezer/[email protected].5':
|
||||
resolution: {integrity: sha512-O1GMVKgtf5YspFaRzpmqkVgMtIk2HG9uMAbH6wdAIdNh3TVzCsch/+cRi+6l2//UGDKe6NB9BW3cvuR5ezMQ0w==}
|
||||
'@lezer/[email protected].4':
|
||||
resolution: {integrity: sha512-4UeRVFO0lsBaM7GA32zOqbmqNPme9jedo8Ey+5c5656MU3MLitMLIA8aCZTYjUMEpEsuSeXEm78Uea6Iq3bvSg==}
|
||||
|
||||
'@lezer/[email protected]':
|
||||
resolution: {integrity: sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==}
|
||||
@@ -489,101 +489,101 @@ packages:
|
||||
'@marijn/[email protected]':
|
||||
resolution: {integrity: sha512-Wy0V7+SGUjnF9/TkiM1hKVDPj7jKXduPNboMVtHTA8dySMURWqfg/JZ9E2Sq8JgSJmkl7k7Qe9FLeMSrSraWmQ==}
|
||||
|
||||
'@oxc-project/[email protected]52.0':
|
||||
resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==}
|
||||
'@oxc-project/[email protected]47.0':
|
||||
resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==}
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
resolution: {integrity: sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==}
|
||||
'@rolldown/[email protected].6':
|
||||
resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
resolution: {integrity: sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==}
|
||||
'@rolldown/[email protected].6':
|
||||
resolution: {integrity: sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
resolution: {integrity: sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==}
|
||||
'@rolldown/[email protected].6':
|
||||
resolution: {integrity: sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
resolution: {integrity: sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==}
|
||||
'@rolldown/[email protected].6':
|
||||
resolution: {integrity: sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
resolution: {integrity: sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==}
|
||||
'@rolldown/[email protected].6':
|
||||
resolution: {integrity: sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
resolution: {integrity: sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==}
|
||||
'@rolldown/[email protected].6':
|
||||
resolution: {integrity: sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
resolution: {integrity: sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==}
|
||||
'@rolldown/[email protected].6':
|
||||
resolution: {integrity: sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
resolution: {integrity: sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==}
|
||||
'@rolldown/[email protected].6':
|
||||
resolution: {integrity: sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
resolution: {integrity: sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==}
|
||||
'@rolldown/[email protected].6':
|
||||
resolution: {integrity: sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
resolution: {integrity: sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==}
|
||||
'@rolldown/[email protected].6':
|
||||
resolution: {integrity: sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
resolution: {integrity: sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==}
|
||||
'@rolldown/[email protected].6':
|
||||
resolution: {integrity: sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
resolution: {integrity: sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==}
|
||||
'@rolldown/[email protected].6':
|
||||
resolution: {integrity: sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
resolution: {integrity: sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==}
|
||||
'@rolldown/[email protected].6':
|
||||
resolution: {integrity: sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
resolution: {integrity: sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==}
|
||||
'@rolldown/[email protected].6':
|
||||
resolution: {integrity: sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
resolution: {integrity: sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==}
|
||||
'@rolldown/[email protected].6':
|
||||
resolution: {integrity: sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -799,8 +799,8 @@ packages:
|
||||
oxc-transform-react:
|
||||
optional: true
|
||||
|
||||
'@vitest/[email protected].3':
|
||||
resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==}
|
||||
'@vitest/[email protected].1':
|
||||
resolution: {integrity: sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==}
|
||||
peerDependencies:
|
||||
msw: ^2.4.9
|
||||
vite: ^6.0.0 || ^7.0.0 || ^8.0.0
|
||||
@@ -810,8 +810,8 @@ packages:
|
||||
vite:
|
||||
optional: true
|
||||
|
||||
'@vitest/[email protected].3':
|
||||
resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==}
|
||||
'@vitest/[email protected].1':
|
||||
resolution: {integrity: sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
@@ -1051,8 +1051,8 @@ packages:
|
||||
resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==}
|
||||
hasBin: true
|
||||
|
||||
[email protected].10:
|
||||
resolution: {integrity: sha512-/B6p9eY9DX7aHBfpkHdpirDTZ5QH9xTwL9y837PWuzuv41O5jFdqNQwVQEQsimY0/iVNkwBY4+4hMfhQ7d4Dbw==}
|
||||
[email protected].9:
|
||||
resolution: {integrity: sha512-8ad9RyoKsb/g8/yLFE+KAlP+DhbCTRUNi/V9XGsxn0R+trJJltNwzcDNo0q/DEkOy5fQUQTAQyCXCYSE+OakTQ==}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
@@ -1396,8 +1396,8 @@ packages:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected].12:
|
||||
resolution: {integrity: sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==}
|
||||
[email protected].6:
|
||||
resolution: {integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
@@ -1408,6 +1408,9 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -1415,6 +1418,9 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
|
||||
|
||||
@@ -1505,8 +1511,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
|
||||
|
||||
[email protected].2:
|
||||
resolution: {integrity: sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==}
|
||||
[email protected].0:
|
||||
resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -1548,20 +1554,20 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
[email protected].3:
|
||||
resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==}
|
||||
[email protected].1:
|
||||
resolution: {integrity: sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==}
|
||||
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.3
|
||||
'@vitest/browser-preview': 5.0.3
|
||||
'@vitest/browser-playwright': 5.0.1
|
||||
'@vitest/browser-preview': 5.0.1
|
||||
'@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0
|
||||
'@vitest/coverage-istanbul': 5.0.3
|
||||
'@vitest/coverage-v8': 5.0.3
|
||||
'@vitest/ui': 5.0.3
|
||||
'@vitest/coverage-istanbul': 5.0.1
|
||||
'@vitest/coverage-v8': 5.0.1
|
||||
'@vitest/ui': 5.0.1
|
||||
happy-dom: '*'
|
||||
jsdom: '*'
|
||||
vite: ^6.4.0 || ^7.0.0 || ^8.0.0
|
||||
@@ -1615,9 +1621,9 @@ packages:
|
||||
resolution: {integrity: sha512-TEZA+Zqxin7Jjsm2cjRohCmen5awh+hT6Zi3VZdqZlNRk7zvOI/9WpBFg/DWlA56bWnzwm6DuB8NS0EsxQH9uQ==}
|
||||
engines: {node: ^22.14.0 || >=24.0.0}
|
||||
|
||||
why-is-node-running@3.2.1:
|
||||
resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==}
|
||||
engines: {node: '>=20.11'}
|
||||
why-is-node-running@2.3.0:
|
||||
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
@@ -1765,7 +1771,7 @@ snapshots:
|
||||
'@codemirror/language': 6.12.4
|
||||
'@codemirror/state': 6.7.6
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/highlight': 1.2.4
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@codemirror/[email protected]':
|
||||
@@ -1783,7 +1789,7 @@ snapshots:
|
||||
'@codemirror/language': 6.12.4
|
||||
'@codemirror/state': 6.7.6
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/highlight': 1.2.4
|
||||
'@lezer/lr': 1.4.10
|
||||
'@lezer/yaml': 1.0.4
|
||||
|
||||
@@ -1792,7 +1798,7 @@ snapshots:
|
||||
'@codemirror/state': 6.7.6
|
||||
'@codemirror/view': 6.43.13
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/highlight': 1.2.4
|
||||
'@lezer/lr': 1.4.10
|
||||
style-mod: 4.1.4
|
||||
|
||||
@@ -1937,41 +1943,41 @@ snapshots:
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/highlight': 1.2.4
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/highlight': 1.2.4
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/[email protected].5':
|
||||
'@lezer/[email protected].4':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/highlight': 1.2.4
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/highlight': 1.2.4
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/highlight': 1.2.4
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/highlight': 1.2.4
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/[email protected]':
|
||||
@@ -1981,79 +1987,79 @@ snapshots:
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/highlight': 1.2.4
|
||||
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/highlight': 1.2.4
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/highlight': 1.2.4
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/highlight': 1.2.4
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@lezer/[email protected]':
|
||||
dependencies:
|
||||
'@lezer/common': 1.5.2
|
||||
'@lezer/highlight': 1.2.5
|
||||
'@lezer/highlight': 1.2.4
|
||||
'@lezer/lr': 1.4.10
|
||||
|
||||
'@marijn/[email protected]': {}
|
||||
|
||||
'@oxc-project/[email protected]52.0': {}
|
||||
'@oxc-project/[email protected]47.0': {}
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
'@rolldown/[email protected].6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
'@rolldown/[email protected].6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
'@rolldown/[email protected].6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
'@rolldown/[email protected].6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
'@rolldown/[email protected].6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
'@rolldown/[email protected].6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
'@rolldown/[email protected].6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
'@rolldown/[email protected].6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
'@rolldown/[email protected].6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
'@rolldown/[email protected].6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
'@rolldown/[email protected].6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
'@rolldown/[email protected].6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
'@rolldown/[email protected].6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
'@rolldown/[email protected].6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected].12':
|
||||
'@rolldown/[email protected].6':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]': {}
|
||||
@@ -2188,21 +2194,21 @@ snapshots:
|
||||
|
||||
'@ungap/[email protected]': {}
|
||||
|
||||
'@vitejs/[email protected]([email protected].2([email protected]))':
|
||||
'@vitejs/[email protected]([email protected].0([email protected]))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.1
|
||||
vite: 8.3.2([email protected])
|
||||
vite: 8.3.0([email protected])
|
||||
|
||||
'@vitest/[email protected].3([email protected].2([email protected]))':
|
||||
'@vitest/[email protected].1([email protected].0([email protected]))':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
'@vitest/spy': 5.0.3
|
||||
'@vitest/spy': 5.0.1
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 1.4.2
|
||||
optionalDependencies:
|
||||
vite: 8.3.2([email protected])
|
||||
vite: 8.3.0([email protected])
|
||||
|
||||
'@vitest/[email protected].3': {}
|
||||
'@vitest/[email protected].1': {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -2513,7 +2519,7 @@ snapshots:
|
||||
dependencies:
|
||||
commander: 8.3.0
|
||||
|
||||
[email protected].10:
|
||||
[email protected].9:
|
||||
dependencies:
|
||||
commander: 15.0.0
|
||||
|
||||
@@ -3111,26 +3117,26 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected].12:
|
||||
[email protected].6:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.152.0
|
||||
'@oxc-project/types': 0.147.0
|
||||
'@rolldown/pluginutils': 1.0.1
|
||||
optionalDependencies:
|
||||
'@rolldown/binding-android-arm-eabi': 1.2.12
|
||||
'@rolldown/binding-android-arm64': 1.2.12
|
||||
'@rolldown/binding-darwin-arm64': 1.2.12
|
||||
'@rolldown/binding-darwin-x64': 1.2.12
|
||||
'@rolldown/binding-freebsd-x64': 1.2.12
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.2.12
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.2.12
|
||||
'@rolldown/binding-linux-arm64-musl': 1.2.12
|
||||
'@rolldown/binding-linux-ppc64-gnu': 1.2.12
|
||||
'@rolldown/binding-linux-s390x-gnu': 1.2.12
|
||||
'@rolldown/binding-linux-x64-gnu': 1.2.12
|
||||
'@rolldown/binding-linux-x64-musl': 1.2.12
|
||||
'@rolldown/binding-openharmony-arm64': 1.2.12
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.2.12
|
||||
'@rolldown/binding-win32-x64-msvc': 1.2.12
|
||||
'@rolldown/binding-android-arm-eabi': 1.2.6
|
||||
'@rolldown/binding-android-arm64': 1.2.6
|
||||
'@rolldown/binding-darwin-arm64': 1.2.6
|
||||
'@rolldown/binding-darwin-x64': 1.2.6
|
||||
'@rolldown/binding-freebsd-x64': 1.2.6
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.2.6
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.2.6
|
||||
'@rolldown/binding-linux-arm64-musl': 1.2.6
|
||||
'@rolldown/binding-linux-ppc64-gnu': 1.2.6
|
||||
'@rolldown/binding-linux-s390x-gnu': 1.2.6
|
||||
'@rolldown/binding-linux-x64-gnu': 1.2.6
|
||||
'@rolldown/binding-linux-x64-musl': 1.2.6
|
||||
'@rolldown/binding-openharmony-arm64': 1.2.6
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.2.6
|
||||
'@rolldown/binding-win32-x64-msvc': 1.2.6
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
@@ -3138,10 +3144,14 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -3269,21 +3279,21 @@ snapshots:
|
||||
'@types/unist': 3.0.3
|
||||
vfile-message: 4.0.3
|
||||
|
||||
[email protected].2([email protected]):
|
||||
[email protected].0([email protected]):
|
||||
dependencies:
|
||||
lightningcss: 1.33.0
|
||||
picomatch: 4.0.7
|
||||
postcss: 8.5.28
|
||||
rolldown: 1.2.12
|
||||
rolldown: 1.2.6
|
||||
tinyglobby: 0.2.17
|
||||
optionalDependencies:
|
||||
esbuild: 0.28.2
|
||||
fsevents: 2.3.3
|
||||
|
||||
[email protected].3([email protected])([email protected].2([email protected])):
|
||||
[email protected].1([email protected])([email protected].0([email protected])):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/mocker': 5.0.3([email protected].2([email protected]))
|
||||
'@vitest/mocker': 5.0.1([email protected].0([email protected]))
|
||||
chai: 6.2.2
|
||||
es-module-lexer: 2.3.2
|
||||
expect-type: 1.4.0
|
||||
@@ -3294,8 +3304,8 @@ snapshots:
|
||||
tinybench: 6.1.4
|
||||
tinyexec: 1.3.0
|
||||
tinyglobby: 0.2.17
|
||||
vite: 8.3.2([email protected])
|
||||
why-is-node-running: 3.2.1
|
||||
vite: 8.3.0([email protected])
|
||||
why-is-node-running: 2.3.0
|
||||
optionalDependencies:
|
||||
jsdom: 30.1.1
|
||||
transitivePeerDependencies:
|
||||
@@ -3329,7 +3339,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- '@noble/hashes'
|
||||
|
||||
why-is-node-running@3.2.1: {}
|
||||
why-is-node-running@2.3.0:
|
||||
dependencies:
|
||||
siginfo: 2.0.0
|
||||
stackback: 0.0.2
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
|
||||
@@ -81,7 +81,6 @@ export const ACTIONS: UIAction[] = [
|
||||
// refused while a turn runs, so it never stops work.
|
||||
{ id: "session.close", kind: "kernel-mutation", target: "entity", proof: "interaction" },
|
||||
{ id: "session.archive", kind: "kernel-mutation", target: "entity", proof: "authority-effect" },
|
||||
{ id: "session.copy-info", kind: "shell-native", target: "entity", proof: "interaction" },
|
||||
{ id: "session.export", kind: "shell-native", target: "entity", proof: "browser" },
|
||||
{ id: "session.delete", kind: "destructive", target: "entity", proof: "authority-effect" },
|
||||
|
||||
@@ -96,10 +95,6 @@ export const ACTIONS: UIAction[] = [
|
||||
|
||||
{ id: "tool.image-open", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "tool.image-close", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "tool.show-all", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "agent.open", kind: "view", target: "entity", proof: "interaction" },
|
||||
{ id: "agent.focus-trap", kind: "navigation", target: "none", proof: "interaction" },
|
||||
{ id: "agent.close", kind: "view", target: "none", proof: "interaction" },
|
||||
|
||||
// ── The turn ─────────────────────────────────────────────────────────────
|
||||
// Send and stop remain two actions even when they sit together: during a
|
||||
@@ -114,15 +109,6 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "prompt.adopt", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "prompt.discard", kind: "view", target: "none", proof: "interaction" },
|
||||
|
||||
// ── Committing what is staged ────────────────────────────────────────────
|
||||
// Drafting reads the index and asks the model; only confirm records a commit,
|
||||
// and only locally.
|
||||
{ id: "commit.draft", kind: "repeatable", target: "none", proof: "interaction" },
|
||||
{ id: "commit.edit", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "commit.acknowledge", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "commit.confirm", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "commit.close", kind: "view", target: "none", proof: "interaction" },
|
||||
|
||||
// ── What is waiting to be sent ───────────────────────────────────────────
|
||||
{ id: "queue.edit", kind: "kernel-mutation", target: "entity", proof: "interaction" },
|
||||
// Giving a session a name of your own. The sidebar's tree and the pane tabs
|
||||
@@ -165,37 +151,6 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "account.reload", kind: "repeatable", target: "none", proof: "interaction" },
|
||||
{ id: "account.sign-in", kind: "kernel-mutation", target: "none", proof: "interaction" },
|
||||
{ id: "account.sign-out", kind: "kernel-mutation", target: "none", proof: "interaction" },
|
||||
{ id: "community.join", kind: "shell-native", target: "none", proof: "interaction" },
|
||||
{ id: "community.discord", kind: "shell-native", target: "none", proof: "interaction" },
|
||||
{ id: "community.issues", kind: "shell-native", target: "none", proof: "interaction" },
|
||||
{ id: "community.contributors", kind: "shell-native", target: "none", proof: "interaction" },
|
||||
{ id: "community.profile", kind: "shell-native", target: "entity", proof: "interaction" },
|
||||
{ id: "community.douyin-qr", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "community.author", kind: "shell-native", target: "none", proof: "interaction" },
|
||||
{ id: "community.more", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.open", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.tab", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.category", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.body", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.name", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.contact", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.shot.pick", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.shot.add", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.shot.remove", kind: "view", target: "entity", proof: "interaction" },
|
||||
{ id: "feedback.send", kind: "kernel-mutation", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.another", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.thread.more", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.reply.open", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.reply.body", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.reply.send", kind: "kernel-mutation", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.reply.cancel", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.mine", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.refresh", kind: "repeatable", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.retry-env", kind: "repeatable", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.retry-mine", kind: "repeatable", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.link", kind: "shell-native", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.focus", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "feedback.close", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "backup.category", kind: "view", target: "entity", proof: "interaction" },
|
||||
{ id: "backup.label", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "backup.passphrase", kind: "view", target: "optional", proof: "interaction" },
|
||||
@@ -222,7 +177,6 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "memory.forget", kind: "destructive", target: "entity", proof: "interaction" },
|
||||
{ id: "config.repair", kind: "kernel-mutation", target: "none", proof: "interaction" },
|
||||
{ id: "extensions.reload", kind: "kernel-mutation", target: "none", proof: "interaction" },
|
||||
{ id: "extensions.refresh", kind: "repeatable", target: "none", proof: "interaction" },
|
||||
{ id: "extensions.inspect", kind: "kernel-mutation", target: "none", proof: "interaction" },
|
||||
{ id: "extensions.install", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "extensions.update", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
@@ -242,8 +196,6 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "market.sort", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "market.pinned", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "market.show-all", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "market.retry", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "market.detail-retry", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "market.more", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "market.open", kind: "navigation", target: "none", proof: "interaction" },
|
||||
{ id: "market.back", kind: "navigation", target: "none", proof: "interaction" },
|
||||
@@ -259,16 +211,12 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "market.signin", kind: "navigation", target: "none", proof: "interaction" },
|
||||
{ id: "market.draft", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "market.publish", kind: "kernel-mutation", target: "none", proof: "interaction" },
|
||||
{ id: "market.prepare-version", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "market.publish-again", kind: "view", target: "none", proof: "interaction" },
|
||||
// The account's own packages: a preview, an install pinned to that preview's
|
||||
// digest, and sending a private one to review.
|
||||
{ id: "market.own-inspect", kind: "kernel-mutation", target: "none", proof: "interaction" },
|
||||
{ id: "market.own-retry", kind: "kernel-mutation", target: "none", proof: "interaction" },
|
||||
{ id: "market.mine-retry", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "market.submit", kind: "kernel-mutation", target: "none", proof: "interaction" },
|
||||
{ id: "market.vote", kind: "kernel-mutation", target: "none", proof: "interaction" },
|
||||
{ id: "market.vote-retry", kind: "view", target: "none", proof: "interaction" },
|
||||
// Taking back the innermost open thing: a popover, an inline form. One
|
||||
// intent, reached by pressing away and by Escape.
|
||||
{ id: "layer.dismiss", kind: "navigation", target: "none", proof: "interaction" },
|
||||
@@ -306,7 +254,6 @@ export const ACTIONS: UIAction[] = [
|
||||
// state — which is why it is one id and why it is not a mutation.
|
||||
{ id: "external.open", kind: "shell-native", target: "none", proof: "interaction" },
|
||||
{ id: "remote-host.remove", kind: "destructive", target: "none", proof: "authority-effect" },
|
||||
{ id: "workspace.reveal", kind: "shell-native", target: "entity", proof: "interaction" },
|
||||
{ id: "workspace.remove", kind: "destructive", target: "none", proof: "authority-effect" },
|
||||
{ id: "extensions.invoke", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
|
||||
@@ -321,8 +268,6 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "remote.probe", kind: "repeatable", target: "entity", proof: "authority-effect" },
|
||||
{ id: "remote.read", kind: "repeatable", target: "entity", proof: "authority-effect" },
|
||||
{ id: "workspace.add", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "workspace.add-cancel", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "workspace.move", kind: "kernel-mutation", target: "entity", proof: "authority-effect" },
|
||||
|
||||
// ── Taking a change back ─────────────────────────────────────────────────
|
||||
// Planning a revert and performing one are two contracts: the first asks the
|
||||
@@ -358,7 +303,6 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "context.window-tokens", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "mcp.add", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "mcp.inspect", kind: "repeatable", target: "none", proof: "interaction" },
|
||||
{ id: "mcp.scope", kind: "interaction", target: "none", proof: "interaction" },
|
||||
{ id: "remotes.add", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "remotes.save", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "remotes.remove", kind: "destructive", target: "entity", proof: "authority-effect" },
|
||||
@@ -370,7 +314,6 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "share.cloud-offer", kind: "repeatable", target: "none", proof: "interaction" },
|
||||
{ id: "share.toggle", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "share.address", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "share.port", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "share.offer", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "share.ask-revoke", kind: "view", target: "entity", proof: "interaction" },
|
||||
{ id: "share.keep", kind: "view", target: "entity", proof: "interaction" },
|
||||
@@ -399,11 +342,6 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "theme.import", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "theme.folder", kind: "repeatable", target: "none", proof: "interaction" },
|
||||
|
||||
// Usage changes what report is read, not canonical session state.
|
||||
{ id: "usage.range.start", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "usage.range.end", kind: "view", target: "none", proof: "interaction" },
|
||||
{ id: "usage.range.month", kind: "view", target: "none", proof: "interaction" },
|
||||
|
||||
// ── How the window looks ─────────────────────────────────────────────────
|
||||
// Named for what a person is doing, not for the control they reached for.
|
||||
// The interface scale has a row of presets and a slider beside it, and both
|
||||
@@ -482,9 +420,6 @@ export const ACTIONS: UIAction[] = [
|
||||
{ id: "browser-tools.enabled", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
// Read by the running turn at its next round, so no rebuild stands between the
|
||||
// click and the canonical change.
|
||||
// The kernel announces the change on the stream, so no rebuild stands between
|
||||
// the click and the canonical change.
|
||||
{ id: "currency.mode", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "progress-watch.pause", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "progress-watch.rounds", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
{ id: "progress-watch.token-multiple", kind: "kernel-mutation", target: "none", proof: "authority-effect" },
|
||||
|
||||
|
Before Width: | Height: | Size: 137 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="296" height="296" class="segno"><g transform="scale(8)"><path fill="#fff" d="M0 0h37v37h-37z"/><path class="qrline" stroke="#000" d="M4 4.5h7m2 0h1m1 0h1m2 0h1m2 0h1m1 0h2m1 0h7m-29 1h1m5 0h1m5 0h1m3 0h2m2 0h1m1 0h1m5 0h1m-29 1h1m1 0h3m1 0h1m2 0h4m1 0h2m1 0h1m2 0h1m1 0h1m1 0h3m1 0h1m-29 1h1m1 0h3m1 0h1m6 0h1m2 0h1m1 0h3m1 0h1m1 0h3m1 0h1m-29 1h1m1 0h3m1 0h1m11 0h1m1 0h1m1 0h1m1 0h3m1 0h1m-29 1h1m5 0h1m1 0h5m2 0h3m1 0h1m2 0h1m5 0h1m-29 1h7m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h7m-20 1h1m1 0h3m1 0h3m-18 1h1m2 0h1m1 0h2m1 0h1m1 0h2m2 0h1m1 0h1m1 0h2m1 0h1m1 0h1m-21 1h1m1 0h1m1 0h2m1 0h1m2 0h1m1 0h4m1 0h1m1 0h1m2 0h1m2 0h1m-29 1h4m1 0h4m1 0h2m1 0h3m2 0h2m3 0h5m-28 1h4m4 0h1m3 0h2m2 0h1m9 0h2m-28 1h2m2 0h1m1 0h1m1 0h5m1 0h2m1 0h1m1 0h2m1 0h2m1 0h1m1 0h2m-28 1h1m1 0h2m3 0h1m2 0h1m1 0h4m1 0h2m1 0h1m-22 1h2m1 0h2m1 0h1m1 0h1m1 0h1m1 0h3m3 0h1m1 0h2m1 0h1m1 0h4m-28 1h2m1 0h2m2 0h1m1 0h1m4 0h1m1 0h1m1 0h1m1 0h2m1 0h2m1 0h1m-28 1h4m2 0h2m1 0h3m1 0h1m3 0h3m1 0h1m5 0h1m-27 1h2m2 0h1m1 0h1m2 0h1m1 0h5m1 0h1m2 0h2m2 0h1m2 0h1m-29 1h1m1 0h2m1 0h6m3 0h5m3 0h3m2 0h2m-27 1h2m4 0h1m1 0h2m2 0h1m2 0h1m1 0h2m3 0h1m2 0h2m-29 1h1m2 0h1m2 0h1m2 0h1m5 0h3m1 0h6m1 0h1m-19 1h2m1 0h2m1 0h2m1 0h4m3 0h1m1 0h3m-29 1h7m2 0h1m2 0h1m4 0h2m1 0h1m1 0h1m1 0h1m2 0h1m-28 1h1m5 0h1m1 0h2m1 0h1m2 0h2m3 0h2m3 0h5m-29 1h1m1 0h3m1 0h1m2 0h2m1 0h1m3 0h3m1 0h5m2 0h1m-28 1h1m1 0h3m1 0h1m1 0h2m2 0h2m1 0h2m1 0h2m2 0h5m1 0h1m-29 1h1m1 0h3m1 0h1m2 0h2m1 0h1m1 0h1m2 0h4m3 0h3m1 0h1m-29 1h1m5 0h1m2 0h1m2 0h1m1 0h1m2 0h1m1 0h2m1 0h1m4 0h1m-28 1h7m1 0h1m3 0h2m2 0h2m2 0h3m1 0h1m2 0h1"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
@@ -1,141 +0,0 @@
|
||||
[
|
||||
"SivanCola",
|
||||
"KHG420",
|
||||
"ttmouse",
|
||||
"lifu963",
|
||||
"HUQIANTAO",
|
||||
"GTC2080",
|
||||
"mchenziyi",
|
||||
"Li-Charles-One",
|
||||
"light-front-theory",
|
||||
"eghrhegpe",
|
||||
"Bernardxu123",
|
||||
"HaoyueQin",
|
||||
"BuGlessRB",
|
||||
"chuiwenwei",
|
||||
"CVEngineer66",
|
||||
"JesonChou",
|
||||
"lanshi17",
|
||||
"SuMuxi66",
|
||||
"paradoxSCH",
|
||||
"CnsMaple",
|
||||
"cyq1017",
|
||||
"Guan0923",
|
||||
"ipanta",
|
||||
"XTLine",
|
||||
"YoungDan-hero",
|
||||
"erphenheimer",
|
||||
"SauronSkywalker",
|
||||
"ashishexee",
|
||||
"lizhengwu",
|
||||
"par73e",
|
||||
"yizeyi18",
|
||||
"Nath-Vikky",
|
||||
"PMKang",
|
||||
"qiaone",
|
||||
"ferstar",
|
||||
"Harbor404",
|
||||
"liaoyl830",
|
||||
"Lxiny-zy",
|
||||
"zhangsanouo",
|
||||
"AtmoOmen",
|
||||
"codex",
|
||||
"Diarica",
|
||||
"ffff2004",
|
||||
"ikelvingo",
|
||||
"kekexunxun",
|
||||
"lightfront",
|
||||
"maoxin1234",
|
||||
"nostalgia296",
|
||||
"onionviolet",
|
||||
"PaulDing98",
|
||||
"scrappylabsai",
|
||||
"skyzhao1223",
|
||||
"Tatlatat",
|
||||
"yekern",
|
||||
"assoyuan",
|
||||
"billycat",
|
||||
"boscocp",
|
||||
"closss",
|
||||
"drafish",
|
||||
"HBLADEH",
|
||||
"heshuimu",
|
||||
"iysun",
|
||||
"jackie-cqz",
|
||||
"LVGG",
|
||||
"nanami7777777",
|
||||
"qiukeren",
|
||||
"sudoviber",
|
||||
"warvyvr",
|
||||
"whale-fall-ouo",
|
||||
"wqshan-cn",
|
||||
"xianyu-sheng",
|
||||
"Xio-Shark",
|
||||
"xxoingr",
|
||||
"yhemeo613",
|
||||
"zdjmrq",
|
||||
"Abysstsuki",
|
||||
"ahmedalbusaidy",
|
||||
"Akashmdshetty",
|
||||
"alxbnct",
|
||||
"c020627",
|
||||
"chengoak",
|
||||
"CooperSheroy",
|
||||
"dcs02280653-collab",
|
||||
"dengbushi",
|
||||
"EMEEEEMMMM",
|
||||
"ERroooooR",
|
||||
"gcoder1991",
|
||||
"h4rk8s",
|
||||
"HHHEEEWWW",
|
||||
"hobostay",
|
||||
"HorusJiang",
|
||||
"i1j",
|
||||
"innocarpe",
|
||||
"j2bap",
|
||||
"jackijianxa",
|
||||
"jbts6",
|
||||
"ji-tz",
|
||||
"JimmyDaddy",
|
||||
"kabaka9527",
|
||||
"kate1128",
|
||||
"LegGasai",
|
||||
"leslie3747",
|
||||
"LouF666",
|
||||
"MagnumGoYB",
|
||||
"MaitreyeeDeshmukh",
|
||||
"myipanta",
|
||||
"nanami-0713",
|
||||
"Ninot1Quyi",
|
||||
"NkAntony777",
|
||||
"no-name-touse",
|
||||
"Nontee22",
|
||||
"onenewcode",
|
||||
"Oxygen56",
|
||||
"PorunC",
|
||||
"Q1hangL",
|
||||
"QingLong-C",
|
||||
"Ray-lyy",
|
||||
"rixzkiye",
|
||||
"Robertoarce",
|
||||
"ryukeilee",
|
||||
"seankhliao",
|
||||
"ShiroKSH",
|
||||
"siwenyu666-beep",
|
||||
"SprayHank",
|
||||
"Starry0214",
|
||||
"Surfire",
|
||||
"taibai233",
|
||||
"themarchrain",
|
||||
"tic-top",
|
||||
"weikengchen",
|
||||
"wudiandy",
|
||||
"xiaoshuai1024",
|
||||
"xiaotaoya2022-cell",
|
||||
"xinrenH",
|
||||
"ying-hua",
|
||||
"yuanchenglu",
|
||||
"zhuguang-ZFG",
|
||||
"Zhuocheng-Lang",
|
||||
"zlwu"
|
||||
]
|
||||
@@ -1,31 +0,0 @@
|
||||
// Why a fold installed nothing, keyed by the kernel's code. A code with no entry
|
||||
// shows only that nothing folded, never a guess at the cause.
|
||||
export const FOLD_WHY: Record<string, string> = {
|
||||
summary_failed: "生成摘要的请求失败了",
|
||||
summary_timeout: "生成摘要的请求超时了",
|
||||
summary_truncated: "摘要在输出上限处被截断,没有采用",
|
||||
summary_input_too_large: "待折叠的内容缩减后仍超过单次摘要请求的容量",
|
||||
hook_refused: "扩展拒绝了这次折叠",
|
||||
persist_failed: "折叠结果没能保存",
|
||||
context_changed: "摘要生成期间对话发生了变化,已放弃这次折叠",
|
||||
digest_lost_every_change: "摘要没有记下这段内容里的任何一处改动,已放弃这次折叠",
|
||||
candidate_not_smaller: "折叠后的上下文不比原来小,没有采用",
|
||||
candidate_above_ceiling: "折叠后仍超过检查点上限(受保护的内容太多),没有采用",
|
||||
candidate_above_trigger: "折叠后仍不低于压缩阈值,没有采用",
|
||||
candidate_above_physical_ceiling: "折叠后仍超过窗口的物理上限,没有采用",
|
||||
savings_below_minimum: "固定前缀已占满检查点上限,这次折叠省下的空间太少,没有采用",
|
||||
fixed_prefix_above_trigger: "无法折叠的固定部分本身已超过压缩阈值",
|
||||
fold_empty_after_hooks: "扩展钩子把待折叠的内容清空了",
|
||||
cancelled: "压缩被取消了",
|
||||
unclassified: "压缩失败,原因未归类;详情见日志",
|
||||
busy: "有一轮对话或会话切换正在进行,请稍后再试",
|
||||
input_unchanged: "上下文自上次整理后没有变化",
|
||||
no_new_closed_prefix: "自上个检查点以来没有已结束的内容",
|
||||
fold_below_economics: "新增内容太少,不值得再生成一次摘要",
|
||||
active_turn_boundary: "正在进行的这一轮需要先结束",
|
||||
no_foldable_region: "没有可折叠的内容",
|
||||
};
|
||||
|
||||
// The reason a /compact notice carries as its detail; an empty detail is the
|
||||
// kernel's decline with no class.
|
||||
export const NO_CODE_WHY = "没有值得折叠的内容";
|
||||
@@ -16,20 +16,16 @@ import { EN_STORAGE } from "./en_storage";
|
||||
import { EN_USAGE } from "./en_usage";
|
||||
import { EN_COMPOSER } from "./en_composer";
|
||||
import { EN_BACKUP } from "./en_backup";
|
||||
import { EN_FEEDBACK } from "./en_feedback";
|
||||
import { EN_COMMUNITY } from "./en_community";
|
||||
|
||||
export const EN: Record<string, string> = {
|
||||
"工作台": "Workbench",
|
||||
"从右侧选择文件,或打开浏览器": "Choose a file on the right, or open a browser",
|
||||
...EN_SETTINGS,
|
||||
...EN_COMMUNITY,
|
||||
...EN_METRICS,
|
||||
...EN_STORAGE,
|
||||
...EN_USAGE,
|
||||
...EN_COMPOSER,
|
||||
...EN_BACKUP,
|
||||
...EN_FEEDBACK,
|
||||
...EN_KERNEL,
|
||||
...EN_REMOTE,
|
||||
...EN_WINDOW,
|
||||
@@ -71,7 +67,6 @@ export const EN: Record<string, string> = {
|
||||
"复制": "Copy",
|
||||
"已复制": "Copied",
|
||||
"复制不了": "Copy blocked",
|
||||
"复制失败,请重试": "Copy failed. Try again.",
|
||||
"复制这段回答": "Copy this answer",
|
||||
"复制这段代码": "Copy this code",
|
||||
"复制路径": "Copy path",
|
||||
@@ -281,10 +276,6 @@ export const EN: Record<string, string> = {
|
||||
"尚无文件夹": "No folders yet",
|
||||
"正在读取文件夹…": "Reading folders…",
|
||||
"打开项目…": "Open a project…",
|
||||
"添加工作区": "Add a workspace",
|
||||
"无法打开文件夹选择器。请输入内核所在机器上的路径。": "The folder picker cannot open. Enter a path on the machine running the kernel.",
|
||||
"工作区路径": "Workspace path",
|
||||
"此内核不支持添加工作区。": "This kernel does not support adding workspaces.",
|
||||
"先打开一个项目": "Open a project first",
|
||||
"读取代码、运行测试与修改文件均只在你选定的文件夹内进行。": "Reading code, running tests and editing files all happen inside the folder you choose.",
|
||||
"使用当前位置": "Stay where I am",
|
||||
@@ -340,11 +331,6 @@ export const EN: Record<string, string> = {
|
||||
"正在运行,暂停后才能关闭": "Running; pause it before closing",
|
||||
"受阻": "blocked",
|
||||
"状态不明": "uncertain",
|
||||
"这条插话在本轮结束前没来得及送达,没有发给模型": "This guidance was not delivered before the turn ended, so the model never saw it",
|
||||
"本轮已结束,但对话记录没能保存": "The turn ended, but the transcript could not be saved",
|
||||
"本轮已结束,但队列没能确认这一条": "The turn ended, but the queue could not acknowledge this item",
|
||||
"运行这一条的会话已不在了": "The session that was running this item is gone",
|
||||
"队列记录损坏,这一条是从残留文件里找回的": "The queue record was corrupt; this item was salvaged from leftover files",
|
||||
"冻结 {n} 文件": "{n} frozen",
|
||||
"无法读取该条的正文,未打开编辑:{why}":
|
||||
"Could not read this entry back, so it was not opened for editing: {why}",
|
||||
@@ -546,9 +532,7 @@ export const EN: Record<string, string> = {
|
||||
"编辑": "Edit",
|
||||
"测试中…": "Testing…",
|
||||
"测试连接": "Test it",
|
||||
"保存失败": "Save failed",
|
||||
"已保存,尚未生效": "Saved, not applied yet",
|
||||
"刷新模型目录失败": "Could not refresh the model catalog",
|
||||
"没保存成功": "Not saved",
|
||||
"重新问一次有哪些模型": "Ask for the model list again",
|
||||
"刷新模型目录": "Refresh model catalog",
|
||||
"正在刷新…": "Refreshing…",
|
||||
@@ -787,7 +771,6 @@ export const EN: Record<string, string> = {
|
||||
"打开主题目录": "Open themes folder",
|
||||
"阅读": "Read",
|
||||
"设置需要一个打开的会话。请先在左栏添加一个文件夹。": "Settings need an open session. Add a folder in the sidebar first.",
|
||||
"反馈需要一个打开的会话。请先在左栏添加一个文件夹。": "Feedback needs an open session. Add a folder in the sidebar first.",
|
||||
"{host} 需要登录": "{host} asks you to sign in",
|
||||
"代理 {host} 需要登录": "The proxy {host} asks you to sign in",
|
||||
"用户名": "Username",
|
||||
@@ -812,7 +795,6 @@ export const EN: Record<string, string> = {
|
||||
"已导入「{name}」。": "Imported “{name}”.",
|
||||
"立即使用": "Use now",
|
||||
"未读取:{names}": "Not read: {names}",
|
||||
"请单独选择一个 .zip,或选择不含压缩包的 theme.json 和图片。": "Choose a single .zip on its own, or select a theme folder’s theme.json and images without a ZIP.",
|
||||
"尚未安装主题。选择一个 .zip,或同时选中主题文件夹里的 theme.json 和图片。": "No themes installed. Choose a .zip, or select a theme folder’s theme.json and images together.",
|
||||
"选择一个 .zip,或同时选中主题文件夹里的 theme.json 和图片。": "Choose a .zip, or select a theme folder’s theme.json and images together.",
|
||||
|
||||
@@ -837,7 +819,6 @@ export const EN: Record<string, string> = {
|
||||
"已关闭": "Disabled",
|
||||
"未连接": "Not connected",
|
||||
"待命 · 首次调用时启动": "Standby · starts on first call",
|
||||
"待授权 · 来自项目声明": "Awaiting approval · declared by the project",
|
||||
"本地构建": "Local build",
|
||||
"只在这台机器上": "This machine only",
|
||||
"这个项目": "This project",
|
||||
@@ -1057,7 +1038,6 @@ export const EN: Record<string, string> = {
|
||||
"兼顾响应速度与可靠性,适合大多数任务": "Balance speed and reliability for most tasks",
|
||||
"投入更多时间分析复杂上下文与执行方案": "Spend more time analyzing complex context and execution plans",
|
||||
"用于最复杂的问题,等待时间与消耗最高": "For the hardest problems, with the longest wait and highest usage",
|
||||
"思考仍开启并计费,该模型无法关闭思考": "Thinking stays on and is billed; this model cannot turn it off",
|
||||
"仅显示当前模型实际支持的档位。": "Only levels actually supported by this model are shown.",
|
||||
"按模型生效": "Per model",
|
||||
"查看上下文与压缩": "View context and compaction",
|
||||
@@ -1110,8 +1090,6 @@ export const EN: Record<string, string> = {
|
||||
"项目操作:{name}": "Project actions: {name}",
|
||||
"项目操作": "Project actions",
|
||||
"不删除文件": "Keeps files",
|
||||
"在文件管理器中显示": "Show in file manager",
|
||||
"文件夹已不在磁盘上": "Folder is gone",
|
||||
"删除会话:{title}": "Delete session: {title}",
|
||||
"会话在文件夹里打开,先添加一个": "A session opens in a folder; add one first",
|
||||
"添加文件夹": "Add folder",
|
||||
@@ -1124,11 +1102,6 @@ export const EN: Record<string, string> = {
|
||||
"读取": "Read",
|
||||
"本地工作区": "Local workspace",
|
||||
"暂停执行": "Pause run",
|
||||
"复制会话信息": "Copy session information",
|
||||
"会话 ID": "Session ID",
|
||||
"会话上下文路径": "Session context path",
|
||||
"任务路径": "Task path",
|
||||
"任务日志": "Task log",
|
||||
"导出会话": "Export session",
|
||||
"取消置顶": "Unpin",
|
||||
"置顶会话": "Pin session",
|
||||
@@ -1223,64 +1196,6 @@ export const EN: Record<string, string> = {
|
||||
"The model returned nothing; try again",
|
||||
"优化请求格式不正确":
|
||||
"The refine request was malformed",
|
||||
"提交…":
|
||||
"Commit…",
|
||||
"起草提交说明":
|
||||
"Draft commit message",
|
||||
"读取暂存区,起草提交说明…":
|
||||
"Reading the staged changes and drafting a message…",
|
||||
"重新起草":
|
||||
"Redraft",
|
||||
"提交说明":
|
||||
"Commit message",
|
||||
"暂存的文件":
|
||||
"Staged files",
|
||||
"{n} 个文件已暂存":
|
||||
"{n} files staged",
|
||||
"可能含有密钥:{files}":
|
||||
"May contain secrets: {files}",
|
||||
"新增的内容里有形似密钥的值":
|
||||
"Added lines contain values shaped like credentials",
|
||||
"我确认这些内容可以提交":
|
||||
"I confirm these can be committed",
|
||||
"提交到本地":
|
||||
"Commit locally",
|
||||
"只在本地提交,不会推送":
|
||||
"Recorded locally only; nothing is pushed",
|
||||
"已提交 {hash}:{subject}":
|
||||
"Committed {hash}: {subject}",
|
||||
"改过暂存区后请重新起草":
|
||||
"Draft again after changing what is staged",
|
||||
"暂存区内容过长,只按前面一部分起草":
|
||||
"The staged diff is long; the draft reads only its first part",
|
||||
"这个工作区不是 git 仓库,无法提交":
|
||||
"This workspace is not a git repository, so there is nothing to commit",
|
||||
"暂存区是空的,先用 git add 暂存要提交的文件":
|
||||
"Nothing is staged; stage the files to commit with git add first",
|
||||
"暂存区在你确认之后又变了,请重新起草":
|
||||
"The staged changes changed after you reviewed them; draft again",
|
||||
"暂存的内容里有疑似密钥的文件,需要你确认后才能提交":
|
||||
"The staged changes include likely secrets; confirm them before committing",
|
||||
"提交说明是空的":
|
||||
"The commit message is empty",
|
||||
"提交说明含有无法记录的字符":
|
||||
"The commit message contains characters git cannot record",
|
||||
"提交说明超过 {max_bytes} 字节":
|
||||
"The commit message is over {max_bytes} bytes",
|
||||
"git 没有配置提交者姓名和邮箱(user.name、user.email)":
|
||||
"git has no author name and email configured (user.name, user.email)",
|
||||
"git 没能记录这次提交":
|
||||
"git could not record the commit",
|
||||
"当前会话没有可用的模型,无法起草提交说明":
|
||||
"This session has no model to draft a commit message with",
|
||||
"起草提交说明超时,请重试":
|
||||
"Drafting the commit message timed out; try again",
|
||||
"模型没有给出提交说明,请重试":
|
||||
"The model returned no commit message; try again",
|
||||
"读取暂存区失败":
|
||||
"Reading the staged changes failed",
|
||||
"提交请求格式不正确":
|
||||
"The commit request was malformed",
|
||||
"新增工具":
|
||||
"New tools",
|
||||
"agent 可以调用,每次调用照常经过权限确认":
|
||||
@@ -1298,8 +1213,5 @@ export const EN: Record<string, string> = {
|
||||
"同一段内容被逐字重复": "The same block is being repeated verbatim",
|
||||
"任务已按设置暂停:模型卡在重复输出同一段文字": "Paused by your setting: the model is stuck repeating the same text",
|
||||
"等待你的输入": "Waiting for you",
|
||||
"引导没有生效:这一轮在处理它之前就结束了。如果仍然需要,请再发送一次:": "Guidance was not applied because the turn ended before it could be processed. Send it again if it is still needed:",
|
||||
"费用显示币种已设为 {mode}": "Cost display currency set to {mode}",
|
||||
"上下文已用到压缩阈值的 {percent}%,已告知模型约剩 {remaining} 个词元的空间。": "Context at {percent}% of the compaction threshold — the model was told it has about {remaining} tokens of room left.",
|
||||
"模型卡在重复输出同一段文字,这一轮已停止;可以重试、补充引导,或换一个供应商/模型": "The model is stuck repeating the same text; this turn was stopped. Try again, add guidance, or switch provider/model.",
|
||||
};
|
||||
@@ -21,9 +21,6 @@ export const EN_BACKUP: Record<string, string> = {
|
||||
"备注(可选)": "Label (optional)",
|
||||
"例如:公司笔记本": "e.g. work laptop",
|
||||
"加密口令(至少 {n} 个字符)": "Passphrase (at least {n} characters)",
|
||||
"至少选择一项备份内容": "Tick at least one thing to back up",
|
||||
"口令还差 {n} 个字符": "Passphrase too short: {n} more needed",
|
||||
"请再输一次口令": "Repeat the passphrase to confirm it",
|
||||
"再输一次": "Repeat it",
|
||||
"两次输入的口令不一致": "The two passphrases differ",
|
||||
"正在加密上传…": "Encrypting and uploading…",
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
export const EN_COMMUNITY: Record<string, string> = {
|
||||
"社区与贡献者": "Community and contributors",
|
||||
"遇到问题、想交流用法或想参与进来,可以加入社区;下面是为 Reasonix 提交过代码的人。": "Join the community for help, tips and ideas. Below are the people who have contributed code to Reasonix.",
|
||||
"QQ 群二维码": "QQ group QR code",
|
||||
"群号": "Group number",
|
||||
"复制群号": "Copy group number",
|
||||
"用 QQ 扫描二维码,或点击下方按钮加入。": "Scan the QR code with QQ, or use the button below to join.",
|
||||
"加入 QQ 群": "Join the QQ group",
|
||||
"贡献者": "Contributors",
|
||||
"按提交数排序,名单随版本更新": "Sorted by commits; the list is refreshed with each release",
|
||||
"查看全部贡献者": "View all contributors",
|
||||
"显示全部 {n} 位": "Show all {n}",
|
||||
"抖音": "Douyin",
|
||||
"抖音号": "Douyin ID",
|
||||
"复制抖音号": "Copy Douyin ID",
|
||||
"显示二维码": "Show QR code",
|
||||
"收起二维码": "Hide QR code",
|
||||
"抖音二维码": "Douyin QR code",
|
||||
"作者与维护者": "Author and maintainer",
|
||||
};
|
||||
@@ -3,8 +3,6 @@
|
||||
export const EN_COMPOSER: Record<string, string> = {
|
||||
"↓ 回到最新": "↓ Back to latest",
|
||||
"不思考": "No reasoning",
|
||||
"最轻量": "Minimal",
|
||||
"比快速档更轻;GLM-5.2 会跳过思考": "Lighter than Fast; GLM-5.2 skips thinking",
|
||||
"超深入": "Extra deep",
|
||||
"不做推理直接作答,响应最快,适合简单问答": "Answers without reasoning; fastest, for simple questions",
|
||||
"比深入投入更多推理,适合困难的多步问题": "More reasoning than Deep, for hard multi-step problems",
|
||||
@@ -23,7 +21,6 @@ export const EN_COMPOSER: Record<string, string> = {
|
||||
"正在加载命令与技能…": "Loading commands and skills…",
|
||||
"引用文件": "Reference files",
|
||||
"Enter 插话 · Shift+Enter 换行": "Enter to steer · Shift+Enter for a new line",
|
||||
"点按插话 · 回车换行": "Tap Steer · Enter for a new line",
|
||||
"{n} 字 · {lines} 行": "{n} characters · {lines} lines",
|
||||
"知道了": "Got it",
|
||||
"是否信任此文件夹": "Trust this folder",
|
||||
@@ -69,7 +66,6 @@ export const EN_COMPOSER: Record<string, string> = {
|
||||
"Nobody has said whether this model reads images · treated as no for now; tick it under Connection to send them straight through",
|
||||
"自动": "Auto",
|
||||
"Enter 发送 · Shift+Enter 换行": "Enter to send · Shift+Enter for a new line",
|
||||
"点按发送 · 回车换行": "Tap Send · Enter for a new line",
|
||||
"不询问": "Do not ask",
|
||||
"全部放行": "Allow all",
|
||||
"均衡": "Balanced",
|
||||
|
||||
@@ -1,133 +0,0 @@
|
||||
// English for the in-app feedback dialog and its refusals.
|
||||
export const EN_FEEDBACK: Record<string, string> = {
|
||||
"反馈": "Feedback",
|
||||
"已完成:": "Done: ",
|
||||
"当前:": "Current: ",
|
||||
"尚未开始:": "Not yet: ",
|
||||
"发送反馈": "Send feedback",
|
||||
"我的反馈": "My feedback",
|
||||
"问题": "Bug",
|
||||
"建议": "Idea",
|
||||
"疑问": "Question",
|
||||
"其他": "Other",
|
||||
"已收到": "Received",
|
||||
"已登记": "Recorded",
|
||||
"处理中": "In progress",
|
||||
"已修复": "Fixed",
|
||||
"不予修复": "Won't fix",
|
||||
"重复": "Duplicate",
|
||||
"已登记为 {issue}": "Recorded as {issue}",
|
||||
"已在 {version} 修复": "Fixed in {version}",
|
||||
"已修复,将随下个版本发布": "Fixed, ships in the next version",
|
||||
"与 {issue} 重复": "Duplicate of {issue}",
|
||||
"已解决": "Resolved",
|
||||
"有些反馈是用这台电脑以前的身份发出的,已经查不到它们的最新状态。": "Some reports were sent under this computer's earlier identity, so their current status can no longer be read.",
|
||||
"状态已无法追踪": "Status no longer trackable",
|
||||
"处理进展": "Progress",
|
||||
"刷新列表": "Refresh list",
|
||||
"每次打开这一页时刷新。状态和回复来自维护者,已登记的反馈会跟随对应的 GitHub 议题。":
|
||||
"Refreshed each time you open this page. Status and replies come from the maintainers; a report that was filed follows its GitHub issue.",
|
||||
"暂时连不上反馈服务。下面是保存在本机的记录,状态可能不是最新的。":
|
||||
"Cannot reach the feedback service right now. This is what was saved on this machine, and statuses may be out of date.",
|
||||
"正在读取你的反馈…": "Loading your feedback…",
|
||||
"还没有提交过反馈。发出第一条之后,它的进展会出现在这里。": "No feedback sent yet. Once you send one, its progress shows up here.",
|
||||
"复制回执号": "Copy receipt number",
|
||||
"回执号": "Receipt number",
|
||||
"已收到你的反馈": "Your feedback was received",
|
||||
"回执号是这份反馈的凭据,请留着它。我们会先看一遍,只有被登记为 GitHub 议题的内容才会公开。进展、议题链接,以及我们的回复或提问,都会出现在「我的反馈」里。":
|
||||
"Keep the receipt number: it identifies this report. We read it first, and only what we file as a GitHub issue becomes public. Progress, the issue link and any reply or question from us appear under My feedback.",
|
||||
"你的文字里有看起来像密钥的内容,已在发出前打码。": "Text in your report that looked like a secret was masked before it was sent.",
|
||||
"查看我的反馈": "View my feedback",
|
||||
"再写一条": "Write another",
|
||||
"只有被我们登记为议题的反馈才会公开":
|
||||
"It becomes public only if we file it as an issue",
|
||||
"如果登记为 GitHub 议题,你的文字、昵称和截图会发布在 esengine/DeepSeek-Reasonix 仓库,任何人都看得到;不登记就不会公开。你会在这里收到回执,登记后还会看到议题链接,我们也可能在这里回复或向你提问。":
|
||||
"If we file it as a GitHub issue, your text, nickname and screenshots are posted in the esengine/DeepSeek-Reasonix repository, where anyone can read them; if we do not, it stays private. You will see a receipt here and, once filed, the issue link, and we may reply or ask you questions here.",
|
||||
"请不要在文字或截图里写入密钥、账号密码、聊天记录或私有代码:一旦公开就收不回来。":
|
||||
"Do not put secrets, passwords, chats or private code in the text or in screenshots: once public it cannot be taken back.",
|
||||
"发现的是安全漏洞?请不要在这里提交,{link}。": "Found a security vulnerability? Do not post it here; {link}.",
|
||||
"按安全策略私下报告": "report it privately under the security policy",
|
||||
"反馈内容": "Feedback details",
|
||||
"反馈类别": "Feedback category",
|
||||
"发生了什么?": "What happened?",
|
||||
"描述你遇到的情况,或你希望它怎样。": "Describe what you ran into, or what you would like instead.",
|
||||
"{n} / {max} 字节": "{n} / {max} bytes",
|
||||
"昵称(登记为议题时公开)": "Nickname (public if filed as an issue)",
|
||||
"如果登记为议题,会显示为「由 {name} 提交」。用昵称即可,不必真名。":
|
||||
"If it is filed as an issue it shows as \"Reported by {name}\". A nickname is fine; no real name needed.",
|
||||
"联系方式(可选,私密)": "Contact (optional, private)",
|
||||
"邮箱或 QQ。只有维护者看得到,绝不会出现在 GitHub 上。": "Email or QQ. Only the maintainers see it; it is never posted on GitHub.",
|
||||
"截图(可选,登记为议题时公开)":
|
||||
"Screenshots (optional, public if filed as an issue)",
|
||||
"移除截图 {name}": "Remove screenshot {name}",
|
||||
"添加截图…": "Add screenshots…",
|
||||
"选择截图文件": "Choose screenshot files",
|
||||
"也可以直接粘贴或拖进来。PNG 或 JPEG,最多 {n} 张,每张不超过 {size} MB,较大的图会自动缩小。":
|
||||
"You can also paste or drop them here. PNG or JPEG, up to {n} images, {size} MB each; large images are shrunk automatically.",
|
||||
"如果这份反馈被登记为公开议题,截图会和文字一起公开。发送前请确认没有露出密钥、聊天记录或私有代码。":
|
||||
"If this report is filed as a public issue, your screenshots are published with your text. Before sending, check they show no keys, chats or private code.",
|
||||
"{name}:只支持 PNG 或 JPEG": "{name}: only PNG or JPEG is supported",
|
||||
"{name}:超过 {size} MB,没有添加": "{name}: larger than {size} MB, not added",
|
||||
"{name}:最多 {n} 张,没有添加": "{name}: the limit is {n} images, not added",
|
||||
"随反馈一起发送的环境信息": "Environment sent with your report",
|
||||
"入口": "Surface",
|
||||
"渠道": "Channel",
|
||||
"只读。不包含密钥、地址或任何文件内容。": "Read-only. It contains no keys, addresses or file contents.",
|
||||
"去 GitHub": "Go to GitHub",
|
||||
"重试发送": "Retry sending",
|
||||
"正文不能为空,请写下你遇到的情况。": "The text is empty. Please describe what happened.",
|
||||
"正文太长了,请缩短后再发。": "The text is too long. Please shorten it and send again.",
|
||||
"请填写昵称。": "Please enter a nickname.",
|
||||
"昵称太长了,请缩短后再发。": "The nickname is too long. Please shorten it.",
|
||||
"联系方式太长了,请缩短后再发。": "The contact is too long. Please shorten it.",
|
||||
"请重新选择反馈类别。": "Please pick a category again.",
|
||||
"截图数量超出上限,请去掉几张后再发。": "Too many screenshots. Remove a few and send again.",
|
||||
"截图只支持 PNG 或 JPEG。": "Screenshots must be PNG or JPEG.",
|
||||
"有截图太大了,请换小一点的图。": "A screenshot is too large. Use a smaller one.",
|
||||
"有截图无法读取,请换一张图。": "A screenshot could not be read. Try another image.",
|
||||
"内容不符合要求,请检查后重试。": "Something in the form was not accepted. Please check it and try again.",
|
||||
"没能连上反馈服务。已填的内容都还在,网络恢复后可以直接重试,不会重复提交。":
|
||||
"Could not reach the feedback service. Everything you entered is kept; retry once the network is back and it will not be filed twice.",
|
||||
"这次的内容没能被解析,请检查后重试。": "The request could not be read. Please check the form and try again.",
|
||||
"内容太大,服务端没有接收。请缩短文字,或去掉一张截图后再发。": "The report was too large for the service. Shorten the text or remove a screenshot, then send again.",
|
||||
"提交得太频繁了,请等 {secs} 秒后再试。": "You are sending too fast. Try again in {secs} seconds.",
|
||||
"提交得太频繁了,请稍等一会儿再试。": "You are sending too fast. Please wait a moment and try again.",
|
||||
"相同内容刚刚已经提交过,不需要再发一次。可以在「我的反馈」里查看它。": "The same text was just sent, so there is no need to send it again. You can find it under My feedback.",
|
||||
"反馈通道今天的接收量已满,不是你发得太多。内容都还在,请明天再试。": "The feedback channel has reached its capacity for today; you did not send too much. Your text is kept, so please try again tomorrow.",
|
||||
"有一张截图没能清除其中的隐藏信息。请换一张,或重新导出后再添加。": "A screenshot could not be cleaned of its hidden metadata. Pick another one, or re-export it and add it again.",
|
||||
"反馈通道暂时关闭。你可以直接到 GitHub 提交问题。": "The feedback channel is switched off for now. You can file an issue on GitHub directly.",
|
||||
"反馈服务没有认出这台电脑,请稍后重试。": "The feedback service did not recognise this computer. Please try again later.",
|
||||
"反馈服务暂时出了问题。已填的内容都还在,请稍等片刻再试,也可以直接到 GitHub 提交问题。": "The feedback service is having trouble. What you typed is kept. Try again in a moment, or file an issue on GitHub directly.",
|
||||
"反馈服务暂时出了问题。你写的回复还在,请稍等片刻;重发前先刷新列表确认是否送达,也可以直接到 GitHub 提交问题。": "The feedback service is having trouble. Your reply is kept. Wait a moment, then refresh the list to check whether it arrived before resending, or file an issue on GitHub directly.",
|
||||
"本机保存反馈记录时出错,请重试。": "Saving the feedback record on this machine failed. Please try again.",
|
||||
"反馈没有发出去,请稍后重试。": "The feedback was not sent. Please try again later.",
|
||||
"需要补充信息": "Needs more info",
|
||||
"维护者已回复": "Answered by us",
|
||||
"已关闭此反馈": "Closed",
|
||||
"需要你回复": "Needs your reply",
|
||||
"等你补充信息": "Waiting for your information",
|
||||
"维护者": "Maintainers",
|
||||
"新": "New",
|
||||
"显示更早的 {n} 条": "Show {n} earlier messages",
|
||||
"回复": "Reply",
|
||||
"回复维护者": "Reply to the maintainers",
|
||||
"你的回复": "Your reply",
|
||||
"发送回复": "Send reply",
|
||||
"离线时暂时不能回复。": "You cannot reply while offline.",
|
||||
"这份反馈已有公开议题 #{n}:你的回复会被公开转发到那个议题里。请不要写入密钥、密码或私有内容。": "This report already has a public issue, #{n}: your reply is copied there publicly. Do not include secrets, passwords or private content.",
|
||||
"只发给维护者,不会公开。请不要写入密钥、密码或私有代码。": "Sent to the maintainers only; it is not made public. Do not include secrets, passwords or private code.",
|
||||
"找不到这条反馈,请刷新列表后再试。": "That report could not be found. Refresh the list and try again.",
|
||||
"回复不能为空。": "The reply is empty.",
|
||||
"回复太长了,请缩短后再发。": "The reply is too long. Please shorten it and send again.",
|
||||
"回复不符合要求,请检查后重试。": "The reply was not accepted. Please check it and try again.",
|
||||
"这份反馈的回复次数已到上限,或你回复得太频繁了。请稍后再试,必要时另外提交一条新反馈。": "This report has reached its reply limit, or you replied too often. Try again later, or send a new report.",
|
||||
"这份反馈现在不接收回复,它可能已经处理完毕或被关闭。请刷新列表查看最新状态。": "This report takes no reply right now; it may be finished or closed. Refresh the list to see its latest status.",
|
||||
"这台电脑的反馈身份已经变了,这份反馈不能再从这里回复。可以另外提交一条新反馈。": "This computer's feedback identity has changed, so this report can no longer be answered from here. You can send a new report.",
|
||||
"反馈通道暂时关闭,暂时不能回复。": "The feedback channel is switched off for now, so replies are not possible.",
|
||||
"没能连上反馈服务。你写的回复还在;如果不确定它有没有送达,请先刷新列表看看,再决定要不要重发。": "Could not reach the feedback service. Your reply is kept; if you are not sure it arrived, refresh the list first, then decide whether to send it again.",
|
||||
"回复没有发出去,请稍后重试。": "The reply was not sent. Please try again later.",
|
||||
"服务暂时要求额外验证,当前版本还不能显示验证步骤。请稍后再试,或直接到 GitHub 提交问题。": "The service temporarily asks for an extra verification step that this version cannot show. Try again later, or file an issue on GitHub directly.",
|
||||
"{n} 项待查看": "{n} to review",
|
||||
"有 {n} 份反馈收到了新回复。": "{n} of your reports have new replies.",
|
||||
"回复已发送。": "Reply sent.",
|
||||
};
|
||||
@@ -3,35 +3,6 @@
|
||||
// code adds one here. i18n.test.ts is what stops one from being forgotten.
|
||||
|
||||
export const EN_KERNEL: Record<string, string> = {
|
||||
"递归删除目标受保护或超出授权范围;请使用工作区或授权目录内的字面路径": "The recursive delete target is protected or outside the granted roots; use a literal path inside the workspace or a granted directory",
|
||||
"无法确定递归删除范围;请使用字面命令名、路径和解释器内容": "The recursive delete extent is unknown; use a literal command name, path and interpreter payload",
|
||||
"请拆分命令;递归删除前只能使用字面路径切换目录,不能依赖变量赋值": "Split the call; only literal directory changes may precede a recursive delete, without relying on variable assignments",
|
||||
"请使用授权目录内的字面删除路径,不要使用变量、展开、通配符或未知管道输入": "Use a literal delete path inside a granted directory, without variables, expansions, globs or unknown pipeline input",
|
||||
"删除选项无法识别;请使用已知选项并提供公共参数的值": "The delete option is unknown; use known options and supply common parameter values",
|
||||
"命令语法有误;请修正后重试": "The command has a syntax error; correct it and retry",
|
||||
"主机命令解析器不可用;请在主机恢复后重试": "The host shell parser is unavailable; retry after the host parser is restored",
|
||||
"主机命令解析超时或被取消;请重试": "The host shell parser timed out or was canceled; retry the call",
|
||||
"命令超过主机长度限制;请拆分命令或从文件读取长文本": "The command exceeds the host length limit; split it or read long text from a file",
|
||||
"另一个会话持有重叠的写入范围,取得所需范围后会自动继续": "Another session holds an overlapping write claim; this session will continue when its claim is available",
|
||||
"这个会话已取得所需的写入范围,现已继续": "This session has acquired its requested write claim and continued",
|
||||
"尚未取得所需的写入范围,这次等待已结束": "The wait ended before the requested write claim was granted",
|
||||
"会话 {holder}({session})持有写入范围:{paths}": "Session {holder} ({session}) holds write claim: {paths}",
|
||||
"所需写入范围:{paths}": "Requested write claim: {paths}",
|
||||
"另一个会话持有所需的写入范围。本次操作未执行;请结束当前轮次,待该范围释放后再重试": "Another session holds the requested write claim. This operation did not run; end the current turn and retry after the claim is released",
|
||||
"反馈通道今日已满,请明天再试": "Feedback is at capacity for today - try again tomorrow",
|
||||
"这份反馈的回复次数已到上限,或回复太频繁了,请稍后再试": "This report has reached its reply limit, or you replied too often - try again later",
|
||||
"这份反馈现在不接收回复": "This report takes no reply right now",
|
||||
"反馈服务要求额外验证,请稍后再试": "The feedback service asks for extra verification - try again later",
|
||||
"截图无法清除其中的元数据,请换一张或先用截图工具重新导出": "An image could not be cleaned of its metadata - pick another or re-export it",
|
||||
"反馈内容不符合要求,请检查后重试": "That feedback could not be accepted - check the fields and try again",
|
||||
"反馈内容过大,请缩短文字或减少截图": "That feedback is too large - shorten the text or attach fewer screenshots",
|
||||
"提交太频繁了,请稍后再试": "Too many submissions - try again a little later",
|
||||
"反馈通道暂时关闭,请稍后再试,或直接到 GitHub 提交问题": "Feedback is switched off for now - try again later, or open an issue on GitHub",
|
||||
"相同内容的反馈刚刚提交过了": "The same feedback was just sent",
|
||||
"反馈服务没有接受本机的反馈身份,请重试": "The feedback service did not accept this install's identity - try again",
|
||||
"无法连接反馈服务,请检查网络后重试,已填内容会保留": "The feedback service cannot be reached - check the network and retry; what you wrote is kept",
|
||||
"反馈服务暂时无法处理请求,请稍后再试": "The feedback service could not handle that - try again later",
|
||||
"本机保存反馈记录失败,请重试": "Feedback could not be recorded on this machine - try again",
|
||||
"该决定已不符合当前状态:计划在你回答前已发生变更": "That decision is no longer current - the plan changed before you answered",
|
||||
"该条已发送给模型,无法撤回": "That line already reached the model - it cannot be taken back",
|
||||
// 能力开关:名字、这台机器的存档、以及服务器自己
|
||||
@@ -148,7 +119,7 @@ export const EN_KERNEL: Record<string, string> = {
|
||||
"任务正在运行,请先停止再重载扩展": "A task is running — stop it before reloading extensions",
|
||||
"该来源正在使用中,请先切换模型再删除": "This source is in use — switch models before deleting it",
|
||||
"请为该来源填写名称": "Give this source a name",
|
||||
"名称只能用字母、数字、点、连字符和下划线,以字母或数字开头,最长 64 个字符": "Use only letters, digits, dots, hyphens and underscores, starting with a letter or digit, up to 64 characters",
|
||||
"名称只能包含字母、数字、点、连字符和下划线": "A name may use letters, digits, dot, dash and underscore only",
|
||||
"已经有名为「{name}」的连接了,换一个名称": "A connection named “{name}” already exists; pick another name",
|
||||
"读不到配置文件,没法安全地选择密钥存放位置,请检查配置后重试": "The configuration could not be read, so there is no safe place to store the key; check the config and try again",
|
||||
"请填写接口地址": "Enter an endpoint address",
|
||||
@@ -176,34 +147,17 @@ export const EN_KERNEL: Record<string, string> = {
|
||||
"互联网连接暂时不可用,请确认电脑端 Studio 已登录并保持在线。": "Internet access is temporarily unavailable. Make sure desktop Studio is signed in and stays online.",
|
||||
"地址 {ip} 不是本机的局域网地址。": "{ip} is not a private address on this computer.",
|
||||
"无法在 {ip} 上开启监听:{error}": "Could not listen on {ip}: {error}",
|
||||
"端口 {port} 已被其他程序占用,请换一个端口,或清空后让系统自动选择。": "Port {port} is already in use by another program. Choose a different port, or clear it to let the system pick one.",
|
||||
"端口需要在 {min}–{max} 之间,或清空后让系统自动选择。": "The port must be between {min} and {max}, or cleared to let the system pick one.",
|
||||
"无法保存端口设置:{error}": "Could not save the port setting: {error}",
|
||||
"没有这台已配对的设备,可能已被移除。": "No paired device has that id; it may already have been removed.",
|
||||
"这个内核没有窗口,打不开本机的编辑器。": "This kernel has no window, so it cannot open an editor on this machine.",
|
||||
"这个文件夹不在当前窗口的项目列表里。": "That folder is not a project in this window.",
|
||||
"项目文件夹已不在磁盘上。": "The project folder is no longer on disk.",
|
||||
"这个内核不在本机,没法在系统文件管理器中显示它的文件。": "This kernel is not on this machine, so its files cannot be shown in the system file manager.",
|
||||
"这个系统没有可用的文件夹选择框,请直接填写路径。": "This system has no folder picker available; type the path instead.",
|
||||
"打不开文件夹选择框:{error}": "The folder picker could not be opened: {error}",
|
||||
"名称「{name}」不能用来存放密钥:密钥槽位由名称推导,而它不能以数字开头。改一个以字母开头的名称即可,密钥本身没有问题。": "The name “{name}” cannot hold a key: the credential slot is derived from it, and a slot may not start with a digit. Use a name starting with a letter — the key itself is fine.",
|
||||
"这个内核没有带界面,只提供接口": "This kernel serves no interface, only its API",
|
||||
"当前模型还没有 API key,请先在设置里添加": "The current model has no API key yet. Add one in Settings first.",
|
||||
"请填写 API key": "An API key is required",
|
||||
"该 key 长度异常,可能粘贴了错误内容": "That key is too long — something else was probably pasted",
|
||||
"已连接,无需重复配置": "Already connected — no need to set it up again",
|
||||
"远端配置未完成,请稍后重试": "The remote setup did not complete; try again shortly",
|
||||
"没有这个模型连接": "There is no such model connection",
|
||||
"该连接没有可存放密钥的变量,请先在配置中为它指定 api_key_env": "This connection has no variable to store a key in — set its api_key_env in the configuration first",
|
||||
"该 key 含有无法安全保存的字符组合,请重新复制": "That key contains a character combination that cannot be stored safely — copy it again",
|
||||
"该 key 超长或含换行,请重新复制": "That key is too long or spans several lines — copy it again",
|
||||
"已保存的密钥在此期间被改动,请重新打开配置再试": "The stored keys changed in the meantime — reopen the setup and try again",
|
||||
"密钥已保存,但连接尚未生效,请重试": "The key was saved but the connection is not active yet — try again",
|
||||
"服务商拒绝了该 key,请检查是否复制完整": "The provider rejected that key — check it was copied in full",
|
||||
"服务商没有及时应答,请稍后重试": "The provider did not answer in time — try again shortly",
|
||||
"连不上服务商,请检查网络或服务地址": "Cannot reach the provider — check the network or the service address",
|
||||
"服务商返回了错误,请稍后重试": "The provider returned an error — try again shortly",
|
||||
"连接测试未通过": "The connection test did not pass",
|
||||
"这台服务器已关闭会话切换": "This server has session switching turned off",
|
||||
"该会话正在清理,请稍后再打开": "This session is being cleaned up; open it again in a moment",
|
||||
"该会话正被其他位置占用,请先关闭该处": "This session is held elsewhere — close it there first",
|
||||
@@ -242,7 +196,6 @@ export const EN_KERNEL: Record<string, string> = {
|
||||
"无法解析该会话路径": "That conversation path does not resolve",
|
||||
"打不开这个会话": "Could not open that conversation",
|
||||
"该路径位于会话目录之外": "That path is outside the conversations directory",
|
||||
"没有这个会话": "No such conversation",
|
||||
"该地址不接受此种请求方式": "That address does not accept this request method",
|
||||
"请求体必须是 application/json": "The request body must be application/json",
|
||||
"这台服务器未开放权限编辑": "This server does not open permission editing",
|
||||
@@ -275,7 +228,6 @@ export const EN_KERNEL: Record<string, string> = {
|
||||
"这份备份已不存在": "That backup no longer exists",
|
||||
"备份数量已达上限,请先删除一份旧备份": "The backup limit is reached; delete an older backup first",
|
||||
"有会在本机运行程序或改变密钥去向的项目,需要逐项确认后才能恢复": "Some items run programs here or change where a key is sent; confirm each one before restoring",
|
||||
"项目列表已满(32 个),请先移除一个项目再添加": "The project list is full (32 projects); remove one before adding another",
|
||||
"这台服务器不支持切换工作区": "This server cannot switch workspaces",
|
||||
"不存在该预设": "There is no such preset",
|
||||
"本次拖入 {count} 个,最多允许 {limit} 个": "{count} dropped at once — the limit is {limit}",
|
||||
@@ -333,8 +285,6 @@ export const EN_KERNEL: Record<string, string> = {
|
||||
"写入目标不在工作区可写范围内。这是文件工具的写入范围,不是操作系统沙箱;要允许写入,请在 设置 → 沙箱 → 额外可写目录 中添加目标文件夹": "The target is outside the workspace write scope. This is the file tools' write scope, not an OS sandbox; to allow the write, add the target folder under Settings → Sandbox → Also writable",
|
||||
"无法读取工作区文件列表": "The workspace file list could not be read",
|
||||
"该文件不是可编辑文本或超过大小限制": "The file is not editable text or exceeds the size limit",
|
||||
"不支持这个币种,只能选自动、CNY 或 USD:{detail}": "That currency is not supported; choose Auto, CNY or USD: {detail}",
|
||||
"费用显示币种未能保存:{detail}": "The display currency was not saved: {detail}",
|
||||
"无进展设置未能保存:{detail}": "The progress settings were not saved: {detail}",
|
||||
"主目录或磁盘根目录不能整体信任,请打开具体的项目文件夹": "A home directory or drive root cannot be trusted as a whole - open a specific project folder",
|
||||
"未能记下对此文件夹的信任决定:{detail}": "The trust decision for this folder was not saved: {detail}",
|
||||
|
||||
@@ -131,25 +131,6 @@ export const EN_REMOTE: Record<string, string> = {
|
||||
"请先更新这台电脑上的 Studio,再连接。": "Update Studio on this computer before connecting.",
|
||||
"无法授权 Web Studio,请重新登录后再试。": "Web Studio could not be authorized. Sign in again and retry.",
|
||||
"连接远程 Studio 超时,请检查电脑是否在线。": "Connecting to Remote Studio timed out. Check that the computer is online.",
|
||||
"网络已断开": "You're offline",
|
||||
"当前设备没有网络。请检查网络连接后重新连接。": "This device has no network. Check your connection, then reconnect.",
|
||||
"无法连接远程服务": "Can't reach the remote service",
|
||||
"当前网络访问不到 Reasonix 远程服务,可能是网络受限或服务暂时中断。请切换网络后重新连接。":
|
||||
"This network can't reach the Reasonix remote service. It may be restricted, or the service may be briefly down. Switch networks and reconnect.",
|
||||
"此网址不能使用远程中转": "This address can't use the relay",
|
||||
"打开 reasonix.io": "Open reasonix.io",
|
||||
"远程中转拒绝了当前网页的来源。请从 reasonix.io 打开 Web Studio 后再连接。":
|
||||
"The relay rejected this page's origin. Open Web Studio from reasonix.io, then connect.",
|
||||
"尝试次数过多": "Too many attempts",
|
||||
"请求过于频繁。请等待 {n} 秒后重新连接。": "Too many requests. Wait {n} seconds, then reconnect.",
|
||||
"请求过于频繁。请稍等片刻后重新连接。": "Too many requests. Wait a moment, then reconnect.",
|
||||
"中转拒绝了这次连接": "The relay refused this connection",
|
||||
"连接凭证可能已过期,或这台电脑上同时连接的设备已满。请重新连接;仍然失败请等一分钟再试。":
|
||||
"The connection ticket may have expired, or this computer already has its maximum number of connected devices. Reconnect; if it still fails, wait a minute.",
|
||||
"长时间没有操作": "Inactive for a while",
|
||||
"为节省资源,中转已回收这条空闲连接。点击重新连接即可继续。": "The relay released this idle connection. Reconnect to continue.",
|
||||
"远程连接失败": "Remote connection failed",
|
||||
"{action}({n})": "{action} ({n})",
|
||||
"远程中转服务暂时不可用,请稍后重试。": "The remote relay is temporarily unavailable. Try again shortly.",
|
||||
"远程 Studio 身份校验失败,请停止连接并检查设备。": "Remote Studio identity verification failed. Stop and check the device.",
|
||||
"需要重新登录": "Sign in again",
|
||||
@@ -174,8 +155,6 @@ export const EN_REMOTE: Record<string, string> = {
|
||||
"手机和这台电脑在同一个网络里,扫码后就能看会话、发消息、批准操作。关闭或退出 Studio 会断开所有手机。": "A phone on the same network as this computer can scan a code to follow sessions, send messages and approve actions. Turning this off or quitting Studio disconnects every phone.",
|
||||
"虚拟网卡": "virtual adapter",
|
||||
"换网络会断开已连接的手机,它们需要扫新的二维码。": "Switching networks disconnects the connected phones; they will need to scan the new code.",
|
||||
"留空则每次开启时由系统随机选择,范围 1024–65535。更改端口后,手机需要重新扫码配对。": "Leave empty to let the system pick a port each time, or choose one from 1024 to 65535. After changing the port, phones need to scan a new code to pair again.",
|
||||
"改端口会断开已连接的手机,它们需要扫新的二维码。": "Changing the port disconnects the connected phones; they will need to scan the new code.",
|
||||
"配对二维码": "Pairing QR code",
|
||||
"复制配对链接": "Copy pairing link",
|
||||
"设备 {n}": "Device {n}",
|
||||
|
||||
@@ -187,20 +187,6 @@ export const EN_SETTINGS: Record<string, string> = {
|
||||
"工具": "Tool",
|
||||
"处理方式": "Verdict",
|
||||
"添加规则": "Add a rule",
|
||||
"工具执行前": "Before a tool runs",
|
||||
"工具执行后": "After a tool runs",
|
||||
"工具执行失败后": "After a tool fails",
|
||||
"请求权限时": "When permission is requested",
|
||||
"提交消息时": "When a message is submitted",
|
||||
"本轮结束时": "When the turn ends",
|
||||
"本轮失败时": "When the turn fails",
|
||||
"模型响应完成后": "After a model response completes",
|
||||
"会话开始时": "When a session starts",
|
||||
"会话结束时": "When a session ends",
|
||||
"子代理开始时": "When a subagent starts",
|
||||
"子代理结束时": "When a subagent ends",
|
||||
"需要你关注时": "When your attention is needed",
|
||||
"压缩上下文前": "Before context is compacted",
|
||||
"例如 git push:* (留空 = 整个工具)": "e.g. git push:* (empty = the whole tool)",
|
||||
"例如 *.env* (留空 = 整个工具)": "e.g. *.env* (empty = the whole tool)",
|
||||
"文件工具读写 .env 一律拒绝。bash 中的 cat 属于另一条路径,本规则不覆盖":
|
||||
@@ -282,14 +268,6 @@ export const EN_SETTINGS: Record<string, string> = {
|
||||
"推理强度选「自动」时使用的档位。": "The level used when reasoning effort is set to Auto.",
|
||||
"{n} 个档位": "{n} levels",
|
||||
"{n} 个模型单独设置": "{n} models with their own levels",
|
||||
"按模型设置限制": "Limits per model",
|
||||
"留空的模型沿用上面的值,灰字是它当前继承到的值;填写后只对该模型生效。":
|
||||
"A blank field follows the values above; the grey text is what that model inherits now. A value applies to that model only.",
|
||||
"{model} 的上下文窗口": "Context window for {model}",
|
||||
"{model} 的最大输出": "Max output for {model}",
|
||||
"继承 {n}": "Inherits {n}",
|
||||
"继承:未声明": "Inherits: not declared",
|
||||
"继承:自动": "Inherits: automatic",
|
||||
"按模型设置档位": "Levels per model",
|
||||
"同一接入下有多家厂商的模型时,可为单个模型指定档位;「继承接入设置」沿用上面的档位。模型自己的档位优先。":
|
||||
"When one connection serves several vendors' models, give a model levels of its own. Inherit uses the levels above; a model's own levels win.",
|
||||
@@ -346,7 +324,6 @@ export const EN_SETTINGS: Record<string, string> = {
|
||||
"Use this after editing extension code, or installing, removing, enabling or disabling a package. The current turn is unaffected; the next one uses the new configuration.",
|
||||
"正在重启常驻进程,重新扫描技能、命令和钩子…": "Restarting resident processes and rescanning skills, commands and hooks…",
|
||||
"已生效,下一轮开始用新的扩展": "Live — the next turn runs the new extensions",
|
||||
"更改已保存,运行时未重载:{reason}。请用「重载运行时」重试。": "Changes saved, but the runtime did not reload: {reason}. Use Reload runtime to retry.",
|
||||
"插件包": "Plugin packages",
|
||||
"一个包可以同时提供技能、命令、自动化钩子和外部服务。安装与导入是同一个操作:提供一个仓库地址,或本机的一个文件夹。":
|
||||
"One package can bring skills, commands, automation hooks and external services at once. Installing and importing are the same action: give it a repository address or a folder on this machine.",
|
||||
@@ -432,48 +409,12 @@ export const EN_SETTINGS: Record<string, string> = {
|
||||
"上次填写未通过:{note}": "The last answer was not accepted: {note}",
|
||||
"填写这一项": "Fill this in",
|
||||
"已提交给 {name}": "Sent to {name}",
|
||||
"MCP 服务器 {name} 请求外部操作": "MCP server {name} is asking for an external interaction",
|
||||
"请在自己的浏览器中完成操作,完成后再继续。打开页面不会自动确认,也不会把浏览器内容交给 agent。": "Complete the interaction in your own browser, then continue. Opening the page does not confirm completion or share its contents with the agent.",
|
||||
"此链接指向本机或私有网络。打开前请确认你信任该服务器和目标地址。": "This link targets this device or a private network. Open it only if you trust the server and the destination.",
|
||||
"请核对完整地址:此链接使用 HTTP 或国际化域名。": "Check the full address: this link uses HTTP or an internationalized domain.",
|
||||
"无法打开浏览器。请重试,或手动打开上面的地址。": "Could not open the browser. Retry, or open the address above manually.",
|
||||
"已确认完成": "Completion confirmed",
|
||||
"打开浏览器": "Open browser",
|
||||
"已完成,继续": "Completed, continue",
|
||||
"已拒绝提供": "Declined",
|
||||
"拒绝提供": "Decline",
|
||||
"(补写过一次)": " (repaired once)",
|
||||
"(缺失部分由主机补全)": " (gaps kept by the host)",
|
||||
"正在折叠为简报": "Folding into a digest",
|
||||
"本次未折叠任何内容": "This pass folded nothing",
|
||||
"扩展拒绝了这次折叠": "An extension refused this fold",
|
||||
"折叠结果没能保存": "The folded result could not be saved",
|
||||
"压缩未完成": "Compaction did not complete",
|
||||
"生成摘要的请求失败了": "The request that writes the summary failed",
|
||||
"生成摘要的请求超时了": "The request that writes the summary timed out",
|
||||
"摘要在输出上限处被截断,没有采用": "The summary hit the output limit and was discarded",
|
||||
"待折叠的内容缩减后仍超过单次摘要请求的容量": "The content to fold still exceeds one summary request after shortening",
|
||||
"摘要生成期间对话发生了变化,已放弃这次折叠": "The conversation changed while the summary was written, so this fold was dropped",
|
||||
"摘要没有记下这段内容里的任何一处改动,已放弃这次折叠": "The summary recorded none of this span's changes, so this fold was dropped",
|
||||
"折叠后的上下文不比原来小,没有采用": "The folded context was no smaller, so it was not used",
|
||||
"折叠后仍超过检查点上限(受保护的内容太多),没有采用": "The folded context still exceeds the checkpoint limit (too much protected content), so it was not used",
|
||||
"折叠后仍不低于压缩阈值,没有采用": "The folded context was still at or above the compaction threshold, so it was not used",
|
||||
"折叠后仍超过窗口的物理上限,没有采用": "The folded context still exceeds the window's physical limit, so it was not used",
|
||||
"固定前缀已占满检查点上限,这次折叠省下的空间太少,没有采用": "The fixed prefix fills the checkpoint limit and this fold saved too little, so it was not used",
|
||||
"无法折叠的固定部分本身已超过压缩阈值": "The part that cannot be folded is already over the compaction threshold",
|
||||
"扩展钩子把待折叠的内容清空了": "An extension hook emptied the content to fold",
|
||||
"压缩被取消了": "The compaction was cancelled",
|
||||
"压缩失败,原因未归类;详情见日志": "The compaction failed for a reason the host did not classify; see the log",
|
||||
"有一轮对话或会话切换正在进行,请稍后再试": "A turn or a session change is in progress; try again shortly",
|
||||
"上下文自上次整理后没有变化": "The context has not changed since it was last tidied",
|
||||
"自上个检查点以来没有已结束的内容": "Nothing has closed since the last checkpoint",
|
||||
"新增内容太少,不值得再生成一次摘要": "Too little has been added to pay for another summary",
|
||||
"正在进行的这一轮需要先结束": "The turn in flight has to finish first",
|
||||
"没有可折叠的内容": "No foldable region remains",
|
||||
"没有值得折叠的内容": "Nothing left worth folding",
|
||||
"已压缩": "Compacted",
|
||||
"无需压缩:{why}": "Nothing to compact: {why}",
|
||||
"压缩失败:{why}": "Compaction failed: {why}",
|
||||
"本段的 {n} 处改动均已写入简报": "All {n} changes made here are in the digest",
|
||||
"{kept}/{required} 处改动写进了简报": "{kept}/{required} changes are in the digest",
|
||||
"另有 {n} 处仅保留索引地址,需通过 recall 取回原文": "{n} more are index entries only; recall fetches the original",
|
||||
@@ -510,7 +451,6 @@ export const EN_SETTINGS: Record<string, string> = {
|
||||
" · 缺 key": " · no key",
|
||||
"安装这个版本": "Install this version",
|
||||
"保存中…": "Saving…",
|
||||
"无法保存": "Could not save",
|
||||
"不发送": "Do not send",
|
||||
"当前版本": "Current version",
|
||||
"读取中…": "Reading…",
|
||||
@@ -655,11 +595,8 @@ export const EN_SETTINGS: Record<string, string> = {
|
||||
"在浏览器里打开的页面输入这串代码。没自动打开就手动访问 {uri}":
|
||||
"Type this code into the page your browser opened. If none opened, go to {uri}",
|
||||
"装好了,下一轮就能用": "Installed — the next turn can use it",
|
||||
"装好了,但运行时未重载:{reason}。请用「重载运行时」重试。":
|
||||
"Installed, but the runtime did not reload: {reason}. Use Reload runtime to retry.",
|
||||
"运行时未重载:{reason}。请用「重载运行时」重试。":
|
||||
"The runtime did not reload: {reason}. Use Reload runtime to retry.",
|
||||
"有项目未安装成功,原因见下方。": "Not all items were installed. See the failures below.",
|
||||
"装好了,但这一轮还在跑:等它结束或新建会话后生效":
|
||||
"Installed, but this turn is still running: it takes effect when the turn ends, or in a new session",
|
||||
"没装上": "Not installed",
|
||||
"这个地址上已经有「{name}」了。留空 key": "This address already holds “{name}”. Leave the key blank",
|
||||
"已就位 · {n} 个工具,下一轮就能用": "In place · {n} tools, ready next turn",
|
||||
@@ -690,8 +627,6 @@ export const EN_SETTINGS: Record<string, string> = {
|
||||
"检测中…": "Checking…",
|
||||
"验证连接并读取": "Check connection and read models",
|
||||
"连接可用 · 找到 {n} 个模型": "Connection works · {n} models found",
|
||||
"接口地址已补全为 {url}": "Address completed to {url}",
|
||||
"刷新模型目录时,模型列表只在 {url} 下响应。聊天若能正常使用就不用改;可选:把接口地址改成它。": "The model list only answers at {url}. If chat already works, no change is needed; optionally set the address to it.",
|
||||
"启用的模型": "Enabled models",
|
||||
"目录只用于发现,不是白名单。直接输入服务商给出的原始模型 ID 即可。": "The catalogue is for discovery, not a whitelist. You can enter any exact model ID supplied by the service.",
|
||||
"阈值需至少为 1,000,并小于模型上下文窗口。": "The threshold must be at least 1,000 and below the model context window.",
|
||||
@@ -805,30 +740,8 @@ export const EN_SETTINGS: Record<string, string> = {
|
||||
"这个来源包含 {n} 个技能,会全部安装": "This source holds {n} skills, and all of them will be installed",
|
||||
"我已看过这 {n} 个技能,全部安装": "I have read these {n} skills - install all of them",
|
||||
"以下项目需要逐项看过再安装": "Read each of these before installing",
|
||||
"以下项目会在本机运行程序": "These run programs on this machine",
|
||||
"以下项目会改变可用能力": "These change what is available",
|
||||
"以下项目仅添加文件": "These only add files",
|
||||
"运行于 Reasonix 内部,可读取整个会话、绕过权限并直接操作本机": "Runs inside Reasonix, can read the whole session, bypass permissions and operate this machine directly",
|
||||
"自动化钩子": "Automatic hooks",
|
||||
"在会话生命周期内自动执行": "Run automatically during the session lifecycle",
|
||||
"为 agent 提供的能力与内置工具等同": "Provides capabilities equivalent to the agent's built-in tools",
|
||||
"{n} 个 MCP 服务": "{n} MCP services",
|
||||
"1 个 MCP 服务": "1 MCP service",
|
||||
"1 条钩子": "1 hook",
|
||||
"1 个技能": "1 skill",
|
||||
"1 个命令": "1 command",
|
||||
"1 个子代理": "1 subagent",
|
||||
"1 个提示词": "1 prompt",
|
||||
"1 套配色": "1 palette",
|
||||
"将启动": "Will start",
|
||||
"将连接至": "Will connect to",
|
||||
"一个常驻进程": "a long-running process",
|
||||
"工具 {names}": "tools {names}",
|
||||
"一条钩子": "a hook",
|
||||
"一个外部服务": "an external service",
|
||||
"{n} 个外部服务": "{n} external services",
|
||||
"一个独立的 MCP 服务": "a standalone MCP service",
|
||||
"{n} 个独立的 MCP 服务": "{n} standalone MCP services",
|
||||
"无法读取 {name}": "Could not read {name}",
|
||||
"发布者": "Publisher",
|
||||
"审核版本": "Reviewed version",
|
||||
@@ -837,8 +750,6 @@ export const EN_SETTINGS: Record<string, string> = {
|
||||
"未固定——审核时没有记录内容摘要": "Not pinned - no content digest was recorded at review",
|
||||
"仓库": "Repository",
|
||||
"标签": "Tags",
|
||||
"已有标签 {n}": "Existing tag {n}",
|
||||
"新增标签": "Add tags",
|
||||
"已安装 {version}": "Installed {version}",
|
||||
"先列出将安装的全部内容,确认后才会写入": "Everything it would install is listed first; nothing is written until you confirm",
|
||||
"审核时没有记录内容摘要,需要信任发布者后安装": "No content digest was recorded at review; trust the publisher to install it",
|
||||
@@ -854,8 +765,6 @@ export const EN_SETTINGS: Record<string, string> = {
|
||||
// 社区市场:发布
|
||||
"浏览": "Browse",
|
||||
"我的发布": "My packages",
|
||||
"发布新版本": "Publish new version",
|
||||
"从 {slug} 复用发布资料;请填写本次发布的来源地址。": "Reuse details from {slug}; enter the source address for this release.",
|
||||
"发布": "Publish",
|
||||
"登录后可以在这里发布技能、插件、MCP 服务和主题,并查看审核进度。": "Sign in to publish skills, plugins, MCP servers and themes here, and to follow their review.",
|
||||
"去登录": "Sign in",
|
||||
@@ -864,10 +773,8 @@ export const EN_SETTINGS: Record<string, string> = {
|
||||
"再发布一个": "Publish another",
|
||||
"查看我的发布": "See my packages",
|
||||
"以 @{handle} 的名义提交,审核通过后公开。只收来源地址,不上传文件。": "Submitted as @{handle} and made public once approved. Only the source address is sent; no files are uploaded.",
|
||||
"以 @{handle} 的名义保存,仅自己可见,不提交审核。只收来源地址,不上传文件。": "Saved as @{handle}, visible only to you and not submitted for review. Only the source address is sent; no files are uploaded.",
|
||||
"主题以插件包发布,包里只能有主题;带技能、钩子或 MCP 服务的包请按插件发布。": "A theme is published as a plugin package that holds only themes; a package with skills, hooks or MCP servers is published as a plugin.",
|
||||
"小写字母、数字、点、下划线、连字符,最多 64 个字符。": "Lowercase letters, digits, dots, underscores and hyphens, up to 64 characters.",
|
||||
"要发布这个包的新版本,请保持名称不变;改名会发布为另一个包。": "Keep this name to publish a new version of this package. Changing it publishes a different package.",
|
||||
"留空时新包为 0.1.0,更新自动加一个补丁号。": "Left empty, a new package starts at 0.1.0 and an update bumps the patch number.",
|
||||
"来源地址": "Source address",
|
||||
"摘要": "Summary",
|
||||
|
||||
@@ -9,9 +9,7 @@ export const EN_STORAGE: Record<string, string> = {
|
||||
"选择旧版数据目录…": "Choose older data folder…",
|
||||
"正在扫描…": "Scanning…",
|
||||
"已找回 {n} 个会话。": "Recovered {n} sessions.",
|
||||
"没有发现尚未导入的旧会话。": "Nothing new to import.",
|
||||
"这个文件夹里没有找到旧版会话。请选择 Reasonix 的数据文件夹(里面有 sessions、projects 或 desktop-sessions-v5)。":
|
||||
"No older sessions were found in this folder. Pick the Reasonix data folder (the one holding sessions, projects or desktop-sessions-v5).",
|
||||
"没有发现尚未导入的旧会话。": "No sessions remain to be imported.",
|
||||
"有 {n} 项无法读取。": "{n} items could not be read.",
|
||||
"未能扫描这个文件夹。请确认它是旧版 Reasonix 的数据目录。": "This folder could not be scanned. Make sure it is an older Reasonix data folder.",
|
||||
存储: "Storage",
|
||||
@@ -23,8 +21,8 @@ export const EN_STORAGE: Record<string, string> = {
|
||||
"目录过大,已统计到时间上限,实际占用不小于此数": "This folder is too large to finish measuring in time; the real size is at least this much.",
|
||||
占用: "Space used",
|
||||
原位置仍有残留数据: "Data remains in the previous location",
|
||||
"以下内容仍位于 {dir}:{names}。迁移存储位置时未一并迁移,因此项目列表、全局指令与记忆、计划任务、壁纸、主题包或更新回滚备份可能显示为缺失。下次启动会自动并入当前位置;若仍显示,请退出后把这些文件或目录复制到当前位置,已有的文件不要覆盖。":
|
||||
"These are still in {dir}: {names}. A move left them behind, so the project list, global instructions and memory, scheduled runs, a wallpaper, a theme pack or the backups an update rolls back to can look gone on this machine. The next launch merges them into the current location; if they are still listed, quit and copy them there without overwriting files that already exist.",
|
||||
"以下内容仍位于 {dir}:{names}。迁移存储位置时未一并迁移,因此本机的壁纸、主题包或更新回滚备份可能显示为缺失。手动将这些目录复制到当前位置即可恢复。":
|
||||
"These are still in {dir}: {names}. A move left them behind, so a wallpaper, a theme pack or the backups an update rolls back to can look gone on this machine. Copying those folders into the current location restores them.",
|
||||
位置: "Locations",
|
||||
迁移: "Migrate",
|
||||
"迁移未能启动。": "The move could not be started.",
|
||||
|
||||
@@ -22,30 +22,11 @@ export const EN_USAGE: Record<string, string> = {
|
||||
"以检查点版本覆盖": "Overwrite with the checkpoint's",
|
||||
"保留当前版本": "Keep the current one",
|
||||
"用量": "Usage",
|
||||
"费用显示币种": "Cost display currency",
|
||||
"只决定费用以哪种货币显示,不改动各来源的价目表。「自动」跟随账户余额的币种,没有余额信息时按价目表的币种显示。":
|
||||
"Only decides which currency costs are shown in; each source's price list is untouched. Auto follows the account balance's currency, and falls back to the price list's currency when there is no balance.",
|
||||
"显示币种": "Display currency",
|
||||
"当前会话的费用、余额与用量页立即按所选币种显示,无需重启。": "The session's cost, the balance and the usage page switch to the chosen currency at once, no restart.",
|
||||
"人民币 CNY": "Chinese yuan (CNY)",
|
||||
"美元 USD": "US dollar (USD)",
|
||||
"无法读取币种设置。": "Could not read the currency setting.",
|
||||
"用量与成本": "Usage & cost",
|
||||
"本机记录的 token 用量与花费,仅保存在这台机器上,不会上传。命中缓存的输入按缓存价计费,因此命中率直接影响费用。":
|
||||
"Token usage and cost recorded on this machine only; nothing is uploaded. Input that hits the cache is billed at the cache rate, so the hit rate drives the bill.",
|
||||
"正在读取记录…": "Reading the records…",
|
||||
"时间范围": "Time range",
|
||||
"自然月": "Calendar month",
|
||||
"开始日期": "Start date",
|
||||
"结束日期": "End date",
|
||||
"应用": "Apply",
|
||||
"选择月份": "Choose month",
|
||||
"每日明细": "Daily details",
|
||||
"精确到每天": "Exact totals for each day",
|
||||
"左右滚动查看": "Scroll sideways for all columns",
|
||||
"日期": "Date",
|
||||
"支出": "Spend",
|
||||
"请求": "Requests",
|
||||
"7 天": "7 days",
|
||||
"30 天": "30 days",
|
||||
"全部": "All",
|
||||
@@ -73,7 +54,6 @@ export const EN_USAGE: Record<string, string> = {
|
||||
"另有 {n} 天有用量但没有成本记录 —— 不是那几天没花费,是成本字段后来才开始持久化。": "{n} more days have usage but no cost recorded — not free days; the cost field started being persisted later.",
|
||||
"按模型": "By model",
|
||||
"按来源": "By source",
|
||||
"未知来源": "Unknown source",
|
||||
"尚无 key?前往获取": "No key yet? Get one",
|
||||
"打开 {name} 控制台": "Open the {name} console",
|
||||
"无法打开浏览器,请手动访问 {at}": "Could not open a browser — visit {at} yourself",
|
||||
|
||||
@@ -14,12 +14,6 @@ export const EN_WINDOW: Record<string, string> = {
|
||||
"运行详情": "Runtime details",
|
||||
"查看全部指标": "View all metrics",
|
||||
"子代理": "Subagents",
|
||||
"查看完整记录": "Open full transcript",
|
||||
"查看完整记录:{name}": "Open full transcript: {name}",
|
||||
"子代理完整记录:{name}": "Full transcript of subagent: {name}",
|
||||
"还没有记录到步骤": "No steps recorded yet",
|
||||
"显示全部": "Show all",
|
||||
"输出过长,仅显示前 200000 个字符": "Output too long, showing the first 200000 characters",
|
||||
"成本构成": "Cost breakdown",
|
||||
"计划待确认": "The plan is yours to decide",
|
||||
"计划已生成,如何执行由你决定。": "The plan is written. What happens next is your call.",
|
||||
|
||||
@@ -24,17 +24,6 @@ const coded = (message: string, code: string, params?: Record<string, string | n
|
||||
// upstream of this — dotted codes, sentinels, typed families — buys nothing if
|
||||
// the last hop prints the English that rode along for the log.
|
||||
describe("what a reader is told a refusal was", () => {
|
||||
it("renders shell refusal causes in Chinese and English", () => {
|
||||
for (const code of ["shell.destructive_target", "shell.analysis_unknown", "shell.delete_sequence", "shell.delete_nonliteral", "shell.delete_option", "shell.syntax_error", "shell.parser_unavailable", "shell.parser_timeout", "shell.command_line_too_long"]) {
|
||||
localStorage.setItem(STORAGE, "zh"); boot();
|
||||
const chinese = reason(coded("fixture fallback", code));
|
||||
expect(chinese).not.toBe("fixture fallback");
|
||||
localStorage.setItem(STORAGE, "en"); boot();
|
||||
const english = reason(coded("fixture fallback", code));
|
||||
expect(english).not.toBe(chinese);
|
||||
expect(english).not.toBe("fixture fallback");
|
||||
}
|
||||
});
|
||||
it("says a coded refusal in the window's own language", () => {
|
||||
expect(reason(coded("inbox item not found", "inbox.not_found"))).toBe("该条已不在待送达队列中");
|
||||
});
|
||||
@@ -42,14 +31,6 @@ describe("what a reader is told a refusal was", () => {
|
||||
// The gate the codes were for. The kernel rewords its own English whenever a
|
||||
// sentence reads better in a log; if that moves what a reader sees, wording
|
||||
// authority never actually left the kernel.
|
||||
it("explains the project limit in Chinese and English", () => {
|
||||
const error = coded("storage limit", "workspace.limit_reached");
|
||||
expect(reason(error)).toBe("项目列表已满(32 个),请先移除一个项目再添加");
|
||||
localStorage.setItem(STORAGE, "en");
|
||||
boot();
|
||||
expect(reason(error)).toBe("The project list is full (32 projects); remove one before adding another");
|
||||
});
|
||||
|
||||
it("does not move when the kernel rewords the same code", () => {
|
||||
const before = reason(coded("inbox item not found", "inbox.not_found"));
|
||||
const after = reason(coded("no such entry in this session's inbox", "inbox.not_found"));
|
||||
|
||||
@@ -18,19 +18,9 @@ export const PROVIDER_EDIT_DISABLED = "provider.editing_disabled";
|
||||
// A paired device answers this for every account route, votes included.
|
||||
export const ACCOUNT_SIGNIN_DISABLED = "account.signin_disabled";
|
||||
// The write landed and only applying it to the open conversation did not.
|
||||
export const SAVED_NOT_APPLIED: readonly string[] = ["provider.saved_while_running", "provider.saved_model_unlisted", "runtime.rebuild_failed", "runtime.saved_while_running"];
|
||||
export const SAVED_NOT_APPLIED: readonly string[] = ["provider.saved_while_running", "provider.saved_model_unlisted", "runtime.rebuild_failed"];
|
||||
|
||||
const SAID: Record<string, string> = {
|
||||
"shell.destructive_target": "递归删除目标受保护或超出授权范围;请使用工作区或授权目录内的字面路径",
|
||||
"shell.analysis_unknown": "无法确定递归删除范围;请使用字面命令名、路径和解释器内容",
|
||||
"shell.delete_sequence": "请拆分命令;递归删除前只能使用字面路径切换目录,不能依赖变量赋值",
|
||||
"shell.delete_nonliteral": "请使用授权目录内的字面删除路径,不要使用变量、展开、通配符或未知管道输入",
|
||||
"shell.delete_option": "删除选项无法识别;请使用已知选项并提供公共参数的值",
|
||||
"shell.syntax_error": "命令语法有误;请修正后重试",
|
||||
"shell.parser_unavailable": "主机命令解析器不可用;请在主机恢复后重试",
|
||||
"shell.parser_timeout": "主机命令解析超时或被取消;请重试",
|
||||
"shell.command_line_too_long": "命令超过主机长度限制;请拆分命令或从文件读取长文本",
|
||||
"workspace.write_conflict": "另一个会话持有所需的写入范围。本次操作未执行;请结束当前轮次,待该范围释放后再重试",
|
||||
// ── 忙:不是出错,是「现在不行」 ─────────────────────────────────
|
||||
"plan.decision_stale": "该决定已不符合当前状态:计划在你回答前已发生变更",
|
||||
"busy.switch_model": "任务正在运行,请先停止再切换模型",
|
||||
@@ -52,7 +42,7 @@ const SAID: Record<string, string> = {
|
||||
|
||||
// ── 来源:填错了什么 ─────────────────────────────────────────────
|
||||
"provider.name_required": "请为该来源填写名称",
|
||||
"provider.name_invalid": "名称只能用字母、数字、点、连字符和下划线,以字母或数字开头,最长 64 个字符",
|
||||
"provider.name_invalid": "名称只能包含字母、数字、点、连字符和下划线",
|
||||
"provider.name_taken": "已经有名为「{name}」的连接了,换一个名称",
|
||||
"provider.config_unreadable": "读不到配置文件,没法安全地选择密钥存放位置,请检查配置后重试",
|
||||
"provider.endpoint_required": "请填写接口地址",
|
||||
@@ -78,7 +68,6 @@ const SAID: Record<string, string> = {
|
||||
"session.bad_path": "无法解析该会话路径",
|
||||
"session.open_failed": "打不开这个会话",
|
||||
"session.outside_dir": "该路径位于会话目录之外",
|
||||
"session.unknown": "没有这个会话",
|
||||
"request.method_not_allowed": "该地址不接受此种请求方式",
|
||||
"request.bad_content_type": "请求体必须是 application/json",
|
||||
"permissions.editing_disabled": "这台服务器未开放权限编辑",
|
||||
@@ -115,7 +104,6 @@ const SAID: Record<string, string> = {
|
||||
"backup.not_found": "这份备份已不存在",
|
||||
"backup.limit_reached": "备份数量已达上限,请先删除一份旧备份",
|
||||
"backup.consent_required": "有会在本机运行程序或改变密钥去向的项目,需要逐项确认后才能恢复",
|
||||
"workspace.limit_reached": "项目列表已满(32 个),请先移除一个项目再添加",
|
||||
"workspace.changing_disabled": "这台服务器不支持切换工作区",
|
||||
"settings.unknown_preset": "不存在该预设",
|
||||
"drop.too_many_paths": "本次拖入 {count} 个,最多允许 {limit} 个",
|
||||
@@ -137,7 +125,6 @@ const SAID: Record<string, string> = {
|
||||
"provider.no_current_model": "当前没有正在使用的模型,无法记录其窗口大小",
|
||||
"context.window_after_this_turn": "窗口大小已记录,将在本轮结束后生效",
|
||||
"provider.saved_while_running": "已保存。当前对话还有未结束的工作(正在运行、等待你回答或有后台任务),仍按原设置进行;结束后再保存一次即可生效",
|
||||
"runtime.saved_while_running": "已保存。当前对话还有未结束的工作(正在运行、等待你回答或有后台任务),仍按原设置进行;结束后再保存一次即可生效",
|
||||
"provider.saved_model_unlisted": "已保存。当前对话使用的模型已不在该来源的列表中,切换模型后才会生效",
|
||||
"provider.extra_body_null": "额外设置中的「{path}」不能为空值(null)",
|
||||
"provider.no_websearch_wire": "该协议不支持由端点自行搜索",
|
||||
@@ -149,8 +136,6 @@ const SAID: Record<string, string> = {
|
||||
"editor.not_installed": "这台机器上没找到 VS Code、Cursor 这类编辑器。装一个,或在配置里用 [desktop] editor 指定路径。",
|
||||
"editor.launch_failed": "编辑器没能启动:{error}",
|
||||
"editor.no_window": "这个内核没有窗口,打不开本机的编辑器。",
|
||||
"workspace.not_listed": "这个文件夹不在当前窗口的项目列表里。",
|
||||
"workspace.folder_missing": "项目文件夹已不在磁盘上。",
|
||||
"workspace.locate_no_window": "这个内核不在本机,没法在系统文件管理器中显示它的文件。",
|
||||
"device.host_only": "这项操作只能在电脑上的窗口里做,已配对的手机做不了。",
|
||||
"device.host_rejected": "这个地址不是本机共享的地址,请重新扫码。",
|
||||
@@ -163,30 +148,15 @@ const SAID: Record<string, string> = {
|
||||
"share.cloud_unavailable": "互联网连接暂时不可用,请确认电脑端 Studio 已登录并保持在线。",
|
||||
"share.address_rejected": "地址 {ip} 不是本机的局域网地址。",
|
||||
"share.listen_failed": "无法在 {ip} 上开启监听:{error}",
|
||||
"share.port_in_use": "端口 {port} 已被其他程序占用,请换一个端口,或清空后让系统自动选择。",
|
||||
"share.port_out_of_range": "端口需要在 {min}–{max} 之间,或清空后让系统自动选择。",
|
||||
"share.port_save_failed": "无法保存端口设置:{error}",
|
||||
"share.device_unknown": "没有这台已配对的设备,可能已被移除。",
|
||||
"picker.unsupported": "这个系统没有可用的文件夹选择框,请直接填写路径。",
|
||||
"picker.failed": "打不开文件夹选择框:{error}",
|
||||
"provider.bad_key_slot": "名称「{name}」不能用来存放密钥:密钥槽位由名称推导,而它不能以数字开头。改一个以字母开头的名称即可,密钥本身没有问题。",
|
||||
"page.not_built": "这个内核没有带界面,只提供接口",
|
||||
"provider.key_missing": "当前模型还没有 API key,请先在设置里添加",
|
||||
"provider.key_required": "请填写 API key",
|
||||
"provider.key_too_large": "该 key 长度异常,可能粘贴了错误内容",
|
||||
"provider.setup_done": "已连接,无需重复配置",
|
||||
"provider.setup_failed": "远端配置未完成,请稍后重试",
|
||||
"provider.unknown": "没有这个模型连接",
|
||||
"provider.no_key_slot": "该连接没有可存放密钥的变量,请先在配置中为它指定 api_key_env",
|
||||
"provider.key_unstorable": "该 key 含有无法安全保存的字符组合,请重新复制",
|
||||
"provider.key_invalid": "该 key 超长或含换行,请重新复制",
|
||||
"provider.credentials_changed": "已保存的密钥在此期间被改动,请重新打开配置再试",
|
||||
"provider.activation_failed": "密钥已保存,但连接尚未生效,请重试",
|
||||
"provider.test_auth": "服务商拒绝了该 key,请检查是否复制完整",
|
||||
"provider.test_timeout": "服务商没有及时应答,请稍后重试",
|
||||
"provider.test_unreachable": "连不上服务商,请检查网络或服务地址",
|
||||
"provider.test_upstream": "服务商返回了错误,请稍后重试",
|
||||
"provider.test_failed": "连接测试未通过",
|
||||
|
||||
"memory.unavailable": "该会话未启用记忆",
|
||||
|
||||
@@ -218,21 +188,6 @@ const SAID: Record<string, string> = {
|
||||
"prompt_refine.failed": "优化失败,请重试",
|
||||
"prompt_refine.bad_request": "优化请求格式不正确",
|
||||
|
||||
"commit.no_repository": "这个工作区不是 git 仓库,无法提交",
|
||||
"commit.nothing_staged": "暂存区是空的,先用 git add 暂存要提交的文件",
|
||||
"commit.staged_changed": "暂存区在你确认之后又变了,请重新起草",
|
||||
"commit.secrets_staged": "暂存的内容里有疑似密钥的文件,需要你确认后才能提交",
|
||||
"commit.empty_message": "提交说明是空的",
|
||||
"commit.message_invalid": "提交说明含有无法记录的字符",
|
||||
"commit.message_too_long": "提交说明超过 {max_bytes} 字节",
|
||||
"commit.identity_missing": "git 没有配置提交者姓名和邮箱(user.name、user.email)",
|
||||
"commit.failed": "git 没能记录这次提交",
|
||||
"commit.no_model": "当前会话没有可用的模型,无法起草提交说明",
|
||||
"commit.timeout": "起草提交说明超时,请重试",
|
||||
"commit.no_answer": "模型没有给出提交说明,请重试",
|
||||
"commit.git_failed": "读取暂存区失败",
|
||||
"commit.bad_request": "提交请求格式不正确",
|
||||
|
||||
// ── 会话 ─────────────────────────────────────────────────────────
|
||||
"session.disabled": "这台服务器已关闭会话切换",
|
||||
"session.pending_cleanup": "该会话正在清理,请稍后再打开",
|
||||
@@ -308,8 +263,6 @@ const SAID: Record<string, string> = {
|
||||
"compaction.no_soft_limit": "本次请求未包含阈值,未做任何修改",
|
||||
"browser_tools.save_failed": "内置浏览器设置未能保存:{detail}",
|
||||
"browser_tools.no_enabled": "本次请求未说明开关状态,未做任何修改",
|
||||
"display_currency.invalid": "不支持这个币种,只能选自动、CNY 或 USD:{detail}",
|
||||
"display_currency.save_failed": "费用显示币种未能保存:{detail}",
|
||||
"progress_watch.save_failed": "无进展设置未能保存:{detail}",
|
||||
"workspace.untrustable": "主目录或磁盘根目录不能整体信任,请打开具体的项目文件夹",
|
||||
"workspace.trust_save_failed": "未能记下对此文件夹的信任决定:{detail}",
|
||||
@@ -374,20 +327,6 @@ const SAID: Record<string, string> = {
|
||||
"wallet.unauthorized": "该供应商拒绝了当前密钥,无法读取余额",
|
||||
"wallet.unreachable": "该供应商的余额接口无响应",
|
||||
"wallet.unreadable": "无法解析该供应商余额接口返回的内容",
|
||||
"feedback.invalid": "反馈内容不符合要求,请检查后重试",
|
||||
"feedback.too_large": "反馈内容过大,请缩短文字或减少截图",
|
||||
"feedback.rate_limited": "提交太频繁了,请稍后再试",
|
||||
"feedback.disabled": "反馈通道暂时关闭,请稍后再试,或直接到 GitHub 提交问题",
|
||||
"feedback.duplicate": "相同内容的反馈刚刚提交过了",
|
||||
"feedback.bad_token": "反馈服务没有接受本机的反馈身份,请重试",
|
||||
"feedback.offline": "无法连接反馈服务,请检查网络后重试,已填内容会保留",
|
||||
"feedback.unavailable": "反馈服务暂时无法处理请求,请稍后再试",
|
||||
"feedback.internal": "本机保存反馈记录失败,请重试",
|
||||
"feedback.busy": "反馈通道今日已满,请明天再试",
|
||||
"feedback.image_metadata": "截图无法清除其中的元数据,请换一张或先用截图工具重新导出",
|
||||
"feedback.reply_limit": "这份反馈的回复次数已到上限,或回复太频繁了,请稍后再试",
|
||||
"feedback.not_replyable": "这份反馈现在不接收回复",
|
||||
"feedback.challenge_required": "反馈服务要求额外验证,请稍后再试",
|
||||
|
||||
// ── 远程连接停下来问的那一句 ───────────────────────────────────
|
||||
"ask.not_found": "不存在该待回答的问题",
|
||||
|
||||
@@ -6,9 +6,9 @@ export const NOTICE_TEXT: Record<string, string> = {
|
||||
empty_final: "这一轮模型没有给出回答,正在让它重说一次",
|
||||
executor_handoff: "模型直接给了答案却没动手,正在要求它先用工具",
|
||||
verification_stalled: "同一个检查连着几轮都报同样的结果,是否继续由你定",
|
||||
workspace_lease: "另一个会话持有重叠的写入范围,取得所需范围后会自动继续",
|
||||
workspace_lease_resumed: "这个会话已取得所需的写入范围,现已继续",
|
||||
workspace_lease_abandoned: "尚未取得所需的写入范围,这次等待已结束",
|
||||
workspace_lease: "另一个会话正在写这个工作区,等它写完会自动继续",
|
||||
workspace_lease_resumed: "工作区空出来了,这个会话已经继续",
|
||||
workspace_lease_abandoned: "还没轮到这个会话写,这次等待就结束了",
|
||||
permission_saved: "已记住这项授权,以后同样的操作不再询问",
|
||||
permission_covered: "已有的授权规则覆盖了这项操作,无需另存",
|
||||
permission_save_failed: "授权没能保存,只在本次会话内有效",
|
||||
@@ -16,14 +16,6 @@ export const NOTICE_TEXT: Record<string, string> = {
|
||||
project_program_changed: "这个项目的程序在批准后被改动过,本次没有运行,需要重新批准",
|
||||
suspected_injection: "一条外部内容看起来在向智能体下指令,已提醒它只当资料看待",
|
||||
await_user: "等待你的输入",
|
||||
default_model_unavailable: "配置里保存的默认模型已不在已配置的供应商中,本次改用第一个可用的模型;在设置里重新选择默认模型即可替换,配置文件未被改动",
|
||||
approval_mode_unrecognized: "配置里的默认审批档位本版本不认识,已按每次询问处理;在界面里选一个档位即可替换",
|
||||
memory_migration_backup: "旧版记忆文件没能先备份,所以没有迁移,保持原样;详情里有具体原因",
|
||||
unapplied_steer: "引导没有生效:这一轮在处理它之前就结束了。如果仍然需要,请再发送一次:",
|
||||
context_budget: "上下文已用到压缩阈值的 {percent}%,已告知模型约剩 {remaining} 个词元的空间。",
|
||||
display_currency: "费用显示币种已设为 {mode}",
|
||||
compacted: "已压缩",
|
||||
compact_declined: "无需压缩:{why}",
|
||||
compact_failed: "压缩失败:{why}",
|
||||
perseveration_loop: "模型卡在重复输出同一段文字,这一轮已停止;可以重试、补充引导,或换一个供应商/模型",
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
// Why a held queue entry stopped, keyed by the kernel's block code. A code with
|
||||
// no entry shows the kernel's own diagnostic.
|
||||
export const BLOCK_WHY: Record<string, string> = {
|
||||
steer_unapplied: "这条插话在本轮结束前没来得及送达,没有发给模型",
|
||||
turn_snapshot_failed: "本轮已结束,但对话记录没能保存",
|
||||
turn_ack_failed: "本轮已结束,但队列没能确认这一条",
|
||||
owner_inactive: "运行这一条的会话已不在了",
|
||||
manifest_salvaged: "队列记录损坏,这一条是从残留文件里找回的",
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { boot, STORAGE, t } from "./index";
|
||||
import { NOTICE_TEXT } from "./notices";
|
||||
import { say } from "./kernel";
|
||||
import { workspaceLeaseDetail } from "./workspace_lease";
|
||||
|
||||
afterEach(() => { localStorage.setItem(STORAGE, "zh"); boot(); });
|
||||
|
||||
describe("write claim attribution", () => {
|
||||
for (const language of ["zh", "en"]) {
|
||||
it(`carries holder identity and extent in ${language}`, () => {
|
||||
localStorage.setItem(STORAGE, language);
|
||||
boot();
|
||||
const claim = { contended: 0, heldMs: 0, idleMs: 0, holder: "Fixture A", holderSessionId: "session-a", paths: ["src/a.go"], requestedPaths: ["src/b.go"] };
|
||||
const detail = workspaceLeaseDetail(claim);
|
||||
for (const value of ["Fixture A", "session-a", "src/a.go", "src/b.go"]) expect(detail).toContain(value);
|
||||
expect(workspaceLeaseDetail(claim, false)).not.toContain("Fixture A");
|
||||
expect(t(NOTICE_TEXT.workspace_lease_resumed)).toBe(language === "en"
|
||||
? "This session has acquired its requested write claim and continued"
|
||||
: "这个会话已取得所需的写入范围,现已继续");
|
||||
expect(say({ code: "workspace.write_conflict" }, "")).not.toBe("");
|
||||
});
|
||||
}
|
||||
});
|
||||