Compare commits

...
Author SHA1 Message Date
YHH d338338d54 Merge pull request #12022 from esengine/docs/studio-2.28.0-notes
docs(release): Studio 2.28.0 notes
2026-10-04 16:58:29 -07:00
yhh 8df12a049a docs(release): Studio 2.28.0 notes 2026-10-04 16:51:12 -07:00
YHH ab25352696 Merge pull request #12018 from esengine/fix/bash-rule-wrapper-peeling
fix(permission): match Bash deny and ask rules through transparent command wrappers
2026-10-04 16:33:38 -07:00
yhh 1fa63d16f0 fix(permission): match Bash deny and ask rules through transparent command wrappers
A deny or ask rule such as Bash(rm:*) compared only the words the model wrote,
so `env X=1 rm`, `sudo rm`, `command rm`, `nohup rm`, `nice rm`, `time rm` and
`/bin/rm` were judged by fallback mode instead of the rule.

shellparse.PeelWrappers reads one simple command from the shell AST and
follows a fixed set of wrappers (env, sudo, doas, command, builtin, exec,
nohup, time, nice) down to the program they run, naming a path-spelled program
by its base name. Deny and ask matching test every layer; allow and session
rules are unchanged. A wrapper whose target cannot be named statically needs a
human instead of falling through to the mode.
2026-10-04 16:24:22 -07:00
YHH e40b3b070f Merge pull request #12006 from esengine/fix/model-list-two-keys-one-endpoint
fix(serve): keep models from two accounts at one endpoint in the model list
2026-10-04 14:12:44 -07:00
yhh fc2887582e fix(serve): keep models from two accounts at one endpoint in the model list
The model list collapsed entries by endpoint + model, so two providers
sharing a base URL with different API keys listed each model once and
the second account's refs vanished from the picker.
2026-10-04 13:58:32 -07:00
YHH 8f8785c4c4 Merge pull request #11697 from KHG420/codex/studio-plugin-profile-diagnostics
fix(studio): diagnose rejected plugin profile declarations
2026-10-04 13:58:02 -07:00
xiexian e6d5dab212 fix(studio): diagnose rejected plugin profile declarations 2026-10-05 04:39:40 +08:00
YHH 403ae88dd2 Merge pull request #12004 from esengine/fix/boot-flaky-tests
test(boot): deflake URL-interaction and fullsidecar effect tests
2026-10-04 13:25:38 -07:00
YHH f7eb4e26a6 Merge pull request #12002 from esengine/fix/flaky-instances-differ-test
test(context-retrieval): assert live runs are seeded from entropy, not that two answers differ
2026-10-04 13:19:31 -07:00
yhh a4e456a43e test(boot): wait for lazy MCP swap before URL-interaction run; give sidecar build a 5m bound and name its timeout 2026-10-04 13:16:21 -07:00
YHH 42455e61fb Merge pull request #11844 from KHG420/codex/studio-sdk-tool-call-identifiers
fix(sdk): validate provider tool call identifiers
2026-10-04 13:12:36 -07:00
esengine 677e3eabe2 test(context-retrieval): assert live runs are seeded from entropy, not that two answers differ
TestInstancesDifferBetweenRuns instantiated every task twice and failed
whenever two independent draws collided. Three tasks answer with a bounded
number (i70 quota 1200..9800, i47 lease 15..300, i112 lease 20..420), so a
run fails with probability 1/8601 + 1/286 + 1/401, about 0.61%.

liveRand now delegates to liveRandFrom(io.Reader). The test asserts the
invariant at the seed: two live generators do not share a stream, the same
entropy yields identical instances and different entropy yields different
values.
2026-10-04 12:59:18 -07:00
YHH 090060507c Merge pull request #11742 from KHG420/codex/studio-unicode-agent-inventory
fix(studio): match package agent inventory to runtime names
2026-10-04 12:56:01 -07:00
xiexian cb1b36d726 fix(studio): match package agent inventory to runtime names 2026-10-05 03:41:01 +08:00
YHH 255001cead Merge pull request #11843 from KHG420/codex/studio-sdk-usage-counts
fix(sdk): reject negative provider token usage
2026-10-04 12:30:56 -07:00
YHH 7ed449841b Merge pull request #12000 from esengine/fix/state-root-walk-windows-delete
test(config): treat a directory mid-deletion on Windows as vanished in the state-root walk
2026-10-04 12:30:30 -07:00
yhh 28455421fb test(config): treat a directory mid-deletion on Windows as vanished in the state-root walk
The walk tolerated only not-exist, but a directory being removed on Windows
fails to open with a sharing violation or access denied. Those errors now
count as vanished only when a re-stat shows the path gone.
2026-10-04 12:16:04 -07:00
YHH 8cfb5f75c7 Merge pull request #11702 from KHG420/codex/studio-directory-agent-inventory
fix(studio): guard agent inventory source residency
2026-10-04 12:00:42 -07:00
xiexian 116d0b4176 fix(studio): guard agent inventory source residency 2026-10-05 02:42:01 +08:00
YHH fc1f7fdcfa Merge pull request #11837 from KHG420/codex/studio-sdk-chunk-types
fix(sdk): reject unregistered provider chunk types
2026-10-04 11:33:32 -07:00
xiexian 9d5f2a7dd9 fix(sdk): validate provider tool call identifiers 2026-10-05 02:28:31 +08:00
xiexian 63754d56d2 fix(sdk): reject negative provider token usage 2026-10-05 02:25:05 +08:00
xiexian 0e06829846 fix(sdk): reject unregistered provider chunk types 2026-10-05 02:22:10 +08:00
YHH 54f4fbb19f Merge pull request #11667 from Harbor404/fix/studio-11605-copy-session-info
feat(studio): copy session handoff information
2026-10-04 11:02:22 -07:00
YHH 03147a71cd Merge pull request #11839 from KHG420/codex/studio-sdk-echo-provider
feat(sdk): add an installable offline provider example
2026-10-04 11:01:31 -07:00
YHH 65a06434fb Merge pull request #11997 from esengine/fix/sdk-unused-time-import
fix(sdk): drop the import the merged intercept-budget change left unused
2026-10-04 10:45:05 -07:00
yhh fdba7c3210 fix(sdk): drop the import the merged intercept-budget change left unused 2026-10-04 10:36:19 -07:00
YHH 254140a866 Merge pull request #11795 from KHG420/codex/studio-sdk-provider-start-cancel
fix(sdk): cancel provider streams while opening
2026-10-04 10:20:50 -07:00
YHH b02849f54f Merge pull request #11719 from KHG420/codex/studio-runtime-doctor-executable
fix(cli): detect non-executable plugin runtimes in doctor
2026-10-04 10:13:26 -07:00
YHH 66bfceb85b Merge pull request #11789 from KHG420/codex/studio-sdk-externalized-intercept-budget
fix(sdk): include externalized reads in the intercept budget
2026-10-04 10:10:08 -07:00
xiexian 39fe0acdc5 fix(sdk): cancel provider streams while opening 2026-10-05 01:04:37 +08:00
YHH 931b9aea42 Merge pull request #11968 from esengine/docs/cli-migration-fixes
docs(migrating): match the verified 2.x CLI behaviour for 1.x users
2026-10-04 09:45:01 -07:00
YHH cb2485a0b8 Merge pull request #11990 from esengine/fix/model-check-shows-status
fix(providers): show HTTP status and endpoint text when a model check is refused
2026-10-04 09:43:44 -07:00
YHH 172d05191a Merge pull request #11852 from chuiwenwei/test/studio-verification-stress-solutions
test(e2ebench): prove every declared-solvable verification-stress task can pass
2026-10-04 09:41:03 -07:00
YHH 8c87da114b Merge pull request #11982 from esengine/fix/link-open-failed-page-no-second-browser
fix(studio): a link whose page failed to load no longer also opens the system browser
2026-10-04 09:35:37 -07:00
yhh 8634cf3da2 fix(providers): show the HTTP status and the endpoint's words when a model check is refused
Cause: the per-model check classified any 4xx it could not name as
unknown/rejected and the row printed only "request refused, not confirmed".
The kernel already carried httpStatus; the settings UI dropped it, and the
endpoint's own error text was never sent, so a local Ollama 404 (base URL
without /v1, model id without its tag), a 400 for a model that cannot take
tools, and a missing key on a System One entry all read identically.

Blast radius: POST /providers/check/model gains an optional `detail` field:
the endpoint's error text as one printable line. It is built in this order:
control characters and ANSI sequences replaced, the probe key masked in raw,
URL-escaped and JSON-escaped form (skipped for keys under 8 characters),
secrets.RedactCredentials, then cut to 300 runes. Folding and the key
replacement see the whole text; only the pattern scrub is bounded (8 KiB cut
on a word boundary, after folding). The model row shows an HTTP badge and the text inline. Status and
reason are unchanged and never derived from the text.

Neighbouring behaviours tested: Ollama-shaped upstream (correct, base without
/v1, tagless id, System One kind); 400/404/405/422 classification; detail
equals the upstream text, masks raw/escaped/plain keys, Bearer, ?token=,
Cookie, neutralises newlines/ANSI/NUL, cuts after redaction, cuts at the scan cap with the key split at every offset, with and
without leading whitespace, bounds a 4 MB body, leaves short keys alone (each mutation-checked); add and edit flows
share one fact helper; full ui vitest suite and the serve package.

Why this layer: the serve handler owns the typed result and the row is the only
renderer of it; classification stays where the failure is first known.
2026-10-04 09:31:09 -07:00
YHH 0a66bd417d Merge pull request #11791 from KHG420/codex/studio-sdk-stream-context-lifetime
fix(sdk): cancel provider contexts when streams finish
2026-10-04 09:25:48 -07:00
YHH 72030d75a3 Merge pull request #11806 from KHG420/codex/studio-market-blocked-previews
fix(studio): keep blocked market previews out of confirmation
2026-10-04 09:21:30 -07:00
YHH 5dfcc2d086 Merge pull request #11987 from esengine/fix/acp-resume-status-notif
test(acp): resume invariant is no transcript replay, not no notification
2026-10-04 09:16:32 -07:00
yhh 42d4ab374c test(acp): resume invariant is no transcript replay, not no notification 2026-10-04 09:06:20 -07:00
YHH 2921ddff26 Merge pull request #11969 from esengine/fix/tui-unknown-slash-as-message
fix(tui): send an unknown slash command to the model with a visible notice
2026-10-04 09:06:09 -07:00
YHH 750ed087db Merge pull request #11981 from esengine/fix/list-folder-of-first-turn
fix(serve): list the folder a first turn is held in
2026-10-04 08:58:31 -07:00
YHH a2a5855f23 Merge pull request #11973 from esengine/fix/compaction-block-release
fix(compaction): release a transient summary failure once the input has grown
2026-10-04 08:56:26 -07:00
yhh 9052fd32f6 fix(studio): a link whose page failed to load no longer also opens the system browser
POST /browser/open answers 400 when the navigation fails even though the
window's tab already exists, and the link handler read every refusal as
"nothing opened" and sent the same address to the machine's browser.
The refusal now names the tab it left behind and the handler only falls
back when there is none.
2026-10-04 08:54:43 -07:00
yhh 3a3b474b10 fix(serve): list the folder a first turn is held in
A pane adopted at launch is not a folder anyone picked, so nothing listed it
once a later launch opened another one. Its conversations stayed on disk with
no sidebar row to open them from. The folder is now listed behind the existing
ones when the pane's first turn mints its session path.
2026-10-04 08:48:09 -07:00
YHH 8fb66a86da Merge pull request #11976 from esengine/fix/queue-withdraw-races
fix(studio): queue withdraw races (double click, stale pull, late receipt)
2026-10-04 08:47:03 -07:00
YHH 2766f23beb Merge pull request #11979 from esengine/fix/state-root-walk-vanishing-dir
test(config): the state-root guard skips directories removed mid-walk
2026-10-04 08:42:19 -07:00
yhh 58a94b30d6 fix(studio): keep taking back a queued line from erring, ghosting or flickering back
A queued line taken back could fail with a spurious error on a repeated click,
leave a pending transcript row when its receipt landed after the withdrawal,
and reappear in the panel when an older queue pull answered last.
2026-10-04 08:36:56 -07:00
esengine 7db5d0af31 fix(compaction): release a transient summary failure once the input has grown
A truncated, timed-out or failed summary request blocked automatic
compaction for the whole generation, so context kept growing past the
fold trigger with no further attempt. Block release now reads the typed
failure code: transient request failures are released after the input
grows by one retry step; every other class stays blocked.
2026-10-04 08:32:39 -07:00
YHH 779fe8a04d Merge pull request #11972 from esengine/fix/compaction-summary-effort
fix(compaction): summary request asks for the lowest approved reasoning depth
2026-10-04 08:31:42 -07:00
yhh 25eff5352d test(config): the state-root guard skips directories removed mid-walk 2026-10-04 08:31:16 -07:00
YHH 0a96b4bf8e Merge pull request #11975 from esengine/fix/compaction-notice-codes
fix(compaction): typed codes for /compact notices and aborted cards, localised by the frontends
2026-10-04 08:26:55 -07:00
esengine c24586d34d fix(compaction): summary request asks for the lowest approved reasoning depth
The digest request inherited the session's reasoning effort, so reasoning
tokens spent the same 16384-token cap the digest had to fit in and a
finish_reason=length dropped the whole summary. It now carries
EffortOverride=low; adapters apply it only when the endpoint's own effort
vocabulary admits it, so endpoints without a low tier keep their configured
depth.

Refs #11970
2026-10-04 08:21:01 -07:00
esengine 3c96792e13 fix(compaction): host notices and aborted cards carry a typed code the frontends word
/compact notices were raw English sentences built from err.Error(), and an
aborted card with no code read "compaction done / folded nothing". Add
compacted/compact_declined/compact_failed notice codes (detail = the
compaction code), give cancellation, unclassified failures and a busy
session their own codes, and word them in the desktop and TUI catalogues.

Refs #11970
2026-10-04 08:16:44 -07:00
esengine 294d179cb5 fix(tui): send an unknown slash command to the model with a visible notice
The terminal UI asked the kernel to refuse any slash line the registry does
not answer, so a typed path or half-remembered command dead-ended with
"unknown command". 1.x sends it as an ordinary message and says so (#5756).

The TUI client no longer sets refuseUnknownSlash; the controller's existing
default path runs the line through the normal turn pipeline. That notice is
now localised (en/zh/zh-TW) and carries the unknown_command code.

A built-in named first is that built-in whatever follows it, as in 1.x: the
kernel matches /context, /new and /clear by first word, and the TUI does the
same for /help, /clear, /setup, /version, /mouse, /resume and its display
commands, so none of them is sent as prose when given arguments.

Refs #11493, #5756
2026-10-04 08:08:01 -07:00
YHH adde7dd6a7 Merge pull request #11928 from esengine/feat/first-run-key-gate
feat(tui): a keyless first run says why and refuses the turn locally
2026-10-04 07:55:35 -07:00
esengine 610e28f6d7 feat(tui): a keyless first run says why and refuses the turn locally
With no API key the connection panel opened silently and a typed message went
to the provider, which answered 401 with a pointer to a shell command. The
panel now names the model that lacks a key, and /submit refuses a turn for it
with provider.key_missing before anything is sent. The TUI returns the line to
the composer and points at /setup.
2026-10-04 07:45:09 -07:00
YHH 27d6e6f2ad Merge pull request #11967 from esengine/fix/usage-test-no-time-local
test(serve): pass the usage window its zone instead of writing time.Local
2026-10-04 07:43:59 -07:00
esengine 7f453a62f3 docs(migrating): match the verified 2.x CLI behaviour for 1.x users
Correct the YOLO shortcut, the --inline form and the unknown slash command row, and list the permission, slash command and compaction differences.
2026-10-04 07:42:42 -07:00
esengine 5fcf468fb0 test(serve): pass the usage window its zone instead of writing time.Local
The usage range test assigned the process-global time.Local while goroutines from earlier tests still read it, so the race detector failed the package. usageWindow now delegates to usageWindowIn, which takes the zone trailing windows count days in; the test passes a fixed zone and never touches the global.
2026-10-04 07:33:03 -07:00
YHH abd863407f Merge pull request #11964 from esengine/fix/backup-malformed
fix(backup): accept the private credential slot a provider key lives in
2026-10-04 07:10:43 -07:00
esengine 2d43186219 fix(backup): accept the private credential slot a provider key lives in
A provider whose natural key slot is taken gets REASONIX_CONNECTION_<id>_KEY. Collect wrote that provider and its key, but restore validation denied every REASONIX_ name, so the backup sealed fine and then failed to open as malformed.
2026-10-04 06:59:49 -07:00
YHH d30d9cec70 Merge pull request #11960 from esengine/fix/11955-11956
fix(studio): feedback opens with no pane, visible refusals, backup button explains itself
2026-10-04 06:59:33 -07:00
esengine 542a6076a3 fix(studio): feedback opens with no pane, refusals are visible, backup button says why it is disabled
Settings and feedback are answered by a pane's kernel. With no pane open, feedback silently did nothing and the settings refusal never cleared. Opening either now starts a session in the current folder, a refusal is shown for both, and it is withdrawn once a folder or pane exists. The cloud-backup button now states what it is waiting for.
2026-10-04 06:50:58 -07:00
YHH 692aecd6b7 Merge pull request #11343 from par73e/fix/studio-acp-mcp-health-9904
fix(studio): report per-server MCP health over ACP
2026-10-04 06:49:46 -07:00
par73e c5b3dbbfe0 fix(studio): complete MCP health review fixes
Preserve ACP frame-order assertions while skipping only MCP status updates,
and use t.Context in the serve regression test.

Report servers requiring launch approval as pending rather than failed in
controller, ACP and /mcp snapshots. Keep pending rows disabled without a retry
action while retaining their recorded transport and sanitized error.

Document connecting and awaiting-approval notifications, style the pending
indicator in both themes, and derive frontend state coverage from Go constants.
Add controller, endpoint, ACP and row-rendering regression coverage.
2026-10-04 21:15:01 +08:00
par73e d550f754fc fix(studio): address review on per-server MCP health
- Map the new `pending` state in the desktop panel with an i18n label. A
  repository-declared server nobody has answered for is off, but not because
  the user switched it off, and the row rendered the raw state string.
- Announce what actually happened. RecordFailure and
  RecordLaunchApprovalRequired now share one store path and name their own
  state, so a project MCP awaiting authorization stops reporting a failed
  connection.
- Correct the MCPSurfaceReady contract: it fires on every change to what /mcp
  answers, not once per server. Audited every consumer before changing it —
  the ACP sink republishes a full snapshot, the serve broadcaster numbers the
  frame and the SPA refetches /mcp, the headless stream passes it through — so
  repeats are harmless and the documentation was the stale part.
- Keep the spawn claim beside the rest of the deferred-spawn bookkeeping
  instead of a file of its own.
- Add the /mcp endpoint regression test the refactor was missing (live
  transport and source, recorded status code, in-flight spawn, cached
  description and tools), and a panel test tying every state label to the
  English catalogue.
2026-10-04 21:06:00 +08:00
par73e b3a5541069 fix(studio): expose per-server MCP health over ACP 2026-10-04 21:05:59 +08:00
YHH 63195e95a5 Merge pull request #11935 from esengine/feat/tui-mcp-panel
feat(tui): /mcp opens a panel to enable, reconnect and inspect MCP servers
2026-10-04 06:02:53 -07:00
esengine 878fc8a53d feat(tui): /mcp opens a panel to enable, reconnect and inspect MCP servers 2026-10-04 05:51:34 -07:00
YHH f7d6d25930 Merge pull request #11929 from esengine/feat/tui-model-provider-pickers
feat(tui): /model and /provider open a searchable picker and switch the session
2026-10-04 05:50:26 -07:00
YHH 0594ee9e0c Merge pull request #11923 from esengine/fix/credential-roundtrip-11901
fix(config): a stored API key reads back byte-for-byte
2026-10-04 05:42:25 -07:00
esengine 290e787c47 feat(tui): /model and /provider open a searchable picker and switch the session 2026-10-04 05:38:56 -07:00
YHH dbb7b8498f Merge pull request #11950 from esengine/feat/settings-display-currency
feat(settings): choose the cost display currency (Auto / CNY / USD)
2026-10-04 05:32:04 -07:00
esengine 4e7067a17b fix(config): a stored API key reads back byte-for-byte (#11901)
SetCredential and SetCredentialIfRevision built a KEY=value line and parsed it
with the dotenv reader, so ${...} was expanded and wrapping quotes stripped
before the key was ever written. The writer also left a value containing $
bare, which the reader then expanded. Store the assignment directly and pick
the first encoding the reader returns unchanged; a value no encoding can hold
(for example a trailing backslash next to a quote or $) is refused before
anything is written, so one entry can never make the whole file unreadable.
2026-10-04 05:31:41 -07:00
YHH ac16c15950 Merge pull request #11934 from esengine/feat/tui-skills-panel
feat(tui): /skills opens a panel to search, filter by source and toggle skills
2026-10-04 05:29:20 -07:00
esengine c8f57aeb69 feat(tui): /skills opens a panel to search, filter by source and toggle skills 2026-10-04 05:16:09 -07:00
YHH 3134b0f18c Merge pull request #11953 from esengine/fix/response-language-11951
fix(control): constrain the visible answer language per turn in auto mode
2026-10-04 05:14:40 -07:00
YHH 6ab2051411 Merge pull request #11949 from esengine/dependabot/github_actions/actions-dfc3086b10
build(deps): bump pnpm/action-setup from 6.0.9 to 6.1.0 in the actions group
2026-10-04 05:12:21 -07:00
yhh 6a9a821998 feat(settings): choose the cost display currency (Auto / CNY / USD)
Adds GET/POST /display-currency over the controller's new
DisplayCurrencySettings/SaveDisplayCurrency (shared with /currency), a
Settings > Usage control, and makes the quote sink follow the
display_currency notice so rounds after a change are priced in it.
2026-10-04 05:08:29 -07:00
YHH 3083ef303d Merge pull request #11945 from esengine/dependabot/go_modules/go-284eff489b
build(deps): bump the go group with 2 updates
2026-10-04 05:07:02 -07:00
esengine 5bba3bf0b4 fix(control): constrain the visible answer language per turn in auto mode 2026-10-04 05:04:16 -07:00
YHH 35da5f70be Merge pull request #11948 from esengine/dependabot/npm_and_yarn/desktop/frontend-next/npm-b63839cc01
build(deps): bump the npm group in /desktop/frontend-next with 4 updates
2026-10-04 05:04:15 -07:00
YHH 27f78af346 Merge pull request #11944 from esengine/feat/render-chart-tool
feat(tools): add the render_chart built-in, hidden from the provider schema
2026-10-04 05:03:04 -07:00
YHH ae6f621bb8 Merge pull request #11946 from esengine/dependabot/npm_and_yarn/desktop/electron/npm-c15dc57556
build(deps-dev): bump electron from 44.4.5 to 44.5.1 in /desktop/electron in the npm group
2026-10-04 05:01:22 -07:00
YHH 28dd27d033 Merge pull request #11927 from esengine/fix/import-1x-session-layouts
fix(migration): import 1.x sessions from project, v4 and desktop v5 stores
2026-10-04 05:00:36 -07:00
dependabot[bot] 6ee1bbd3aa build(deps): bump pnpm/action-setup
Bumps the actions group with 1 update: [pnpm/action-setup](https://github.com/pnpm/action-setup).


Updates `pnpm/action-setup` from 6.0.9 to 6.1.0
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v6.0.9...ea17c68df8912ef543352723c149a84f56e3d413)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 6.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-04 11:55:05 +00:00
dependabot[bot] c8152a8f7e build(deps): bump the npm group in /desktop/frontend-next with 4 updates
Bumps the npm group in /desktop/frontend-next with 4 updates: [@lezer/highlight](https://github.com/lezer-parser/highlight), [katex](https://github.com/KaTeX/KaTeX), [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) and [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest).


Updates `@lezer/highlight` from 1.2.4 to 1.2.5
- [Changelog](https://github.com/lezer-parser/highlight/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lezer-parser/highlight/commits)

Updates `katex` from 0.18.9 to 0.18.10
- [Release notes](https://github.com/KaTeX/KaTeX/releases)
- [Changelog](https://github.com/KaTeX/KaTeX/blob/main/CHANGELOG.md)
- [Commits](https://github.com/KaTeX/KaTeX/compare/v0.18.9...v0.18.10)

Updates `vite` from 8.3.0 to 8.3.2
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.3.2/packages/vite)

Updates `vitest` from 5.0.1 to 5.0.3
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.3/packages/vitest)

---
updated-dependencies:
- dependency-name: "@lezer/highlight"
  dependency-version: 1.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: katex
  dependency-version: 0.18.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: vite
  dependency-version: 8.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
- dependency-name: vitest
  dependency-version: 5.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-04 11:54:18 +00:00
dependabot[bot] 0487b131a0 build(deps-dev): bump electron in /desktop/electron in the npm group
Bumps the npm group in /desktop/electron with 1 update: [electron](https://github.com/electron/electron).


Updates `electron` from 44.4.5 to 44.5.1
- [Release notes](https://github.com/electron/electron/releases)
- [Commits](https://github.com/electron/electron/compare/v44.4.5...v44.5.1)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 44.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-04 11:53:26 +00:00
dependabot[bot] 87d1ebd1d4 build(deps): bump the go group with 2 updates
Bumps the go group with 2 updates: [github.com/klauspost/compress](https://github.com/klauspost/compress) and [modernc.org/sqlite](https://gitlab.com/cznic/sqlite).


Updates `github.com/klauspost/compress` from 1.20.0 to 1.20.1
- [Release notes](https://github.com/klauspost/compress/releases)
- [Commits](https://github.com/klauspost/compress/compare/v1.20.0...v1.20.1)

Updates `modernc.org/sqlite` from 1.59.0 to 1.60.1
- [Changelog](https://gitlab.com/cznic/sqlite/blob/master/CHANGELOG.md)
- [Commits](https://gitlab.com/cznic/sqlite/compare/v1.59.0...v1.60.1)

---
updated-dependencies:
- dependency-name: github.com/klauspost/compress
  dependency-version: 1.20.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: go
- dependency-name: modernc.org/sqlite
  dependency-version: 1.60.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-10-04 11:53:05 +00:00
yhh 5b4b4ccb97 fix(migration): import 1.x sessions from project, v4 and desktop v5 stores
Picking a 1.x data folder only looked at <root>/sessions, so per-project
sessions, sessions-v4 and desktop-sessions-v5/by-id imported nothing and the
UI reported no old sessions.
2026-10-04 04:49:41 -07:00
YHH ab8835393f Merge pull request #11932 from esengine/fix/cask-waits-for-cli-release
ci(release): write the Homebrew cask only after the CLI release exists
2026-10-04 04:49:29 -07:00
yhh dc809e1150 feat(tools): add the render_chart built-in, hidden from the provider schema
render_chart validates a chartspec v1 spec with chartspec.Parse and answers
with a chart_id and the deterministic text summary, never the rows. It is a
registered built-in left out of the provider allow list, so it is found and
called through use_capability and the cache-stable prefix is unchanged. The
spec persists in the call arguments the session already stores (#11564).
2026-10-04 04:46:12 -07:00
YHH 2bfe43a62d Merge pull request #11926 from esengine/cli/rename-by-index
fix(tui): /rename <n> <title> names the nth saved session and the picker shows the name
2026-10-04 04:41:55 -07:00
YHH 81ce4da64b Merge pull request #11919 from esengine/feat/chartspec-v1
feat(chartspec): add the closed v1 chart spec, validator and text summary
2026-10-04 04:38:49 -07:00
YHH 6a335ae9d7 Merge pull request #11939 from esengine/fix/editconn-save-failed-wording
fix(settings): distinguish failed save from saved-but-not-applied in provider edit
2026-10-04 04:33:07 -07:00
esengine f89c934e47 ci(release): the Homebrew cask is written only after the CLI release it points at exists, and never moves backwards
cli-channels and cli-pointer both waited only for cli-tag, so the cask could be
pushed while the v<version> release holding its archives was still being
created, and stayed pointing at nothing if cli-pointer then failed. The cask
step also overwrote the tap whatever its version, so a workflow_dispatch of an
older tag moved Homebrew backwards; it now skips a tap that is ahead and fails
closed when the tap's version cannot be read.
2026-10-04 04:32:09 -07:00
YHH bfe2ca68b8 Merge pull request #11931 from esengine/fix/cli-archive-build-identity
ci(release): CLI archives carry the same build identity as the npm packages
2026-10-04 04:31:21 -07:00
YHH c3837f5a93 Merge pull request #11937 from esengine/feat/slash-remote-preset-pasteimage
feat(control): answer /remote, /preset (/work-mode, /profile) and /paste-image
2026-10-04 04:28:17 -07:00
yhh 2abe2784e3 feat(chartspec): add the closed v1 chart spec, its validator and text summary
Leaf package internal/contract/chartspec: spec types for bar/line/pie, a
strict Parse (size cap first, unknown fields refused, scalar cells only,
unsafe integers refused), typed refusals with dotted codes and caps, and a
deterministic Summary that quotes cell data. No consumer yet (#11564).
2026-10-04 04:28:10 -07:00
YHH 0d7fd85452 Merge pull request #11920 from esengine/fix/run-untrusted-folder-exit-code
fix(run): exit 4 when an untrusted folder refused the work, and 1.x migration notes
2026-10-04 04:20:25 -07:00
YHH ac31510363 Merge pull request #11933 from esengine/fix/tui-finished-todos-stay
fix(tui): a finished to-do list stays docked until the next turn starts
2026-10-04 04:14:00 -07:00
YHH 753996a897 Merge pull request #11936 from esengine/feat/tui-rate-band
feat(tui): the footer and turn receipt name the peak or off-peak rate a spend was billed at
2026-10-04 04:13:55 -07:00
yhh 2f45ce03f3 fix(settings): say a refused provider save failed, and a saved-but-unapplied one is saved
EditConn titled every outcome "not saved" at warn level, so a write that
landed but was not applied to the live session read as a failure, and a real
failure read as a soft notice. Split by outcome: error alert for a refused
save, status note for saved-not-applied, own title for a catalog refresh.
2026-10-04 04:08:35 -07:00
esengine 4e4004f0f4 fix(tui): /rename <n> <title> names the nth saved session and the picker shows the name 2026-10-04 04:05:59 -07:00
YHH 939ff9f6f7 Merge pull request #11930 from esengine/fix/tui-blank-thinking-marker
fix(tui): a thinking block with no text draws no marker
2026-10-04 04:04:50 -07:00
YHH feb4a51051 Merge pull request #11925 from esengine/fix/windows-icon-fill
fix(desktop): fill the Windows icon tile so the Start menu icon matches its neighbours
2026-10-04 03:46:57 -07:00
esengine 69464792d2 feat(control): answer /remote, /preset (/work-mode, /profile) and /paste-image instead of 'unknown command' 2026-10-04 03:46:00 -07:00
esengine 75c640b9d2 ci(release): CLI archives carry the commit, build time and docs identity npm builds already carry
The archives that reasonix upgrade, the Homebrew cask and the GitHub release
install were linked with main.version only, so they reported no git_commit or
build_time_utc and the embedded product docs identified themselves as dev. The
npm packages of the same release were linked with all of them.
2026-10-04 03:43:25 -07:00
esengine a970662e35 feat(tui): the footer and turn receipt name the peak or off-peak rate a spend was billed at
CostQuote carries a rate band for a rate confirmed as the vendor's own
schedule, aggregates fold it to peak, off-peak or mixed (none when any call
has no schedule), and the TUI prints it beside the cost as 1.x does.
2026-10-04 03:39:23 -07:00
YHH 19c86553ae Merge pull request #11924 from esengine/cli/status-details
fix(tui): /status lists the work tree, the effort and the model ref as 1.x does
2026-10-04 03:36:09 -07:00
esengine 2719b42f8b fix(run): exit 4 when an untrusted folder refused the work, and write the 1.x migration notes
A headless run in a folder with no trust decision refused every edit and
command and exited 0, so scripts that relied on 1.x defaults looked
successful. It now exits 4 (3 under --fail-on-unverified); the gate and
what it allows are unchanged. docs/MIGRATING.md gains the 1.x differences:
trust prompt, untrusted-folder runs, compact-ratio range, env vars.

Refs #11493
2026-10-04 03:35:56 -07:00
esengine a15b269c7f fix(tui): a finished to-do list stays docked until the next turn starts
The panel dropped a list as soon as every item was completed, so the final
N/N never showed. It now stays through the ask panel and the idle prompt and
goes when the next turn begins; a rebuilt screen (resume, rewind) still does
not draw a list that finished before it was opened.
2026-10-04 03:35:51 -07:00
esengine 597153d377 fix(tui): a thinking block with no text draws no marker
Reasoning that is only whitespace has nothing to fold, so the transcript no
longer prints a collapsed "thought for 0s (Ctrl+O)" line for it.
2026-10-04 03:32:15 -07:00
yhh ebe758959e fix(desktop): fill the Windows icon tile so the Start menu icon matches its neighbours 2026-10-04 03:27:32 -07:00
esengine ae22f4b171 fix(tui): /status lists the work tree, the effort and the model ref as 1.x does 2026-10-04 03:25:25 -07:00
YHH 0b638eb96e Merge pull request #11534 from esengine/feat/commit-message-helper
feat(studio): draft a commit message from the staged diff, commit locally on confirm
2026-10-04 01:56:13 -07:00
esengine 7c2a81eb24 feat(studio): draft a commit message from the staged diff and commit locally on confirm
Adds a typed Commits port on the controller: ProposeCommit reads the staged
index through gitcmd, frozen as a tree object, and asks the session's model
for a conventional-commit message in a request of its own (prefix untouched,
billed as commit-message). CommitStaged writes the reviewed tree with
commit-tree and moves HEAD only if it has not moved, so an index that changes
after confirmation cannot enter the commit. Files named as secret holders or
whose added lines carry a private key are withheld from the model and need an
acknowledgement. Hooks and signing never run. Nothing pushes.

Refs #11518
2026-10-04 01:45:20 -07:00
YHH b3d091139f Merge pull request #11342 from par73e/fix/studio-glm-effort-9642
fix(studio): expose GLM reasoning effort levels
2026-10-04 01:38:46 -07:00
par73e 6913fc9765 fix(studio): address review on GLM reasoning effort
- Add the pre-5.3 GLM catalogs to shippedPresets so installs holding the
  old list are moved forward, plus upgrade tests (old shape upgrades, a
  curated catalog stays untouched).
- Declare the per-model ladder once in provider.ZhipuEffortContract and
  read it from the /effort menu, the normalizer and the request builder,
  instead of repeating the model lists across three switches.
- Cite Zhipu's Core Parameters reference for the documented levels
  (5.2's none/minimal/low/medium/xhigh folds, 5.3's forced thinking).
- Add a boot.Build effect test at the provider-request boundary covering
  the stored enabled/disabled -> EffortDisplay -> request path.
- Surface the disabled->low landing for GLM-5.3 in the /effort menu and
  document the 1M context window the added models reuse.
2026-10-04 16:28:33 +08:00
YHH 18ed0649c9 Merge pull request #11914 from esengine/fix/sidebar-open-version-twice
fix(studio): list a version a pane has open once, not under its parent and as its own row
2026-10-04 01:15:13 -07:00
YHH 90afa32c27 Merge pull request #11913 from esengine/fix/gitcmd-codeql-cut
fix(gitcmd): make the option screen a CodeQL barrier at the exec exit
2026-10-04 00:59:24 -07:00
esengine 7363b44c25 fix(studio): list a version a pane has open once, not under its parent and as its own row 2026-10-04 00:58:28 -07:00
esengine f74ae6d352 fix(gitcmd): classify every argument by the option test before it reaches the exec
CodeQL's go/command-injection models regexp matches as barriers only when the
match is the branch condition dominating each use. screen() folded the match
into an || condition and the -C directory skipped it, so alert 1014 kept its
flows through screen() and Args.
2026-10-04 00:49:46 -07:00
YHH c5b43c410c Merge pull request #11665 from Harbor404/fix/studio-11615-usage-ranges
feat(usage): add calendar ranges and precise daily details
2026-10-04 00:26:39 -07:00
Harbor404 080a8d7ac1 fix(usage): clip long rows without hiding totals 2026-10-04 15:11:03 +08:00
Harbor404 86ba9227ee fix(usage): preserve structured range validation
- keep typed field details for days and explicit ranges
- reject from/to combined with days
- treat explicit dates as zone-less calendar days
- relocate and extend usage range tests
2026-10-04 14:10:41 +08:00
YHH 9b1ec952e1 Merge pull request #11909 from esengine/fix/11905-probe-resolved-base
fix(providers): save the base URL the model-list probe answered at
2026-10-03 22:38:14 -07:00
YHH c28884609c Merge pull request #11911 from esengine/fix/rebuild-busy-saved-identity
fix(serve): settings saved while the runtime is busy no longer read as a failed rebuild
2026-10-03 22:33:29 -07:00
YHH 5f7827c9b2 Merge pull request #11906 from esengine/fix/gitcmd-arg-injection
fix(gitcmd): refuse program-running git options at the exec exit
2026-10-03 22:32:26 -07:00
YHH 711966ef31 Merge pull request #11908 from esengine/fix/no-reload-shortcut
fix(studio): leave F5 and Ctrl/Cmd+R to the page; app-window reload moves to Ctrl/Cmd+Shift+R
2026-10-03 22:26:32 -07:00
esengine fb297c3483 fix(serve): a settings save that meets a busy runtime says it was saved, by code
rebuildFailed collapsed the busy refusal into runtime.rebuild_failed with
the English sentence as detail, so the compaction, shell, sandbox,
permission, browser and MCP-load saves told a zh reader the write had
failed. It now answers runtime.saved_while_running, which the UI renders.
2026-10-03 22:16:18 -07:00
esengine 99ee5cdfe0 fix(gitcmd): refuse program-running git options at the exec exit (CodeQL go/command-injection) 2026-10-03 22:13:54 -07:00
esengine fc2f4b495c fix(providers): save the base URL the model-list probe actually answered at
A bare https://host passed the add-provider probe because {base}/v1/models
answered, while chat posts to the saved base + /chat/completions and failed.
The probe now reports the base that was proven (typed address completed with
/v1 only when the listing answered there alone, openai-style kinds only), and
add, onboarding and edit-refresh adopt it into the address field before save.
2026-10-03 22:13:18 -07:00
esengine 1a38708fda fix(studio): leave F5 and Ctrl/Cmd+R to the page; reload the app window on Ctrl/Cmd+Shift+R
F5 and Ctrl/Cmd+R reloaded the whole Studio UI from any focus, so a page
or browser-pane user pressing them lost the window state. The app window
now reloads only on Ctrl/Cmd+Shift+R (white-screen recovery, crash reload
unchanged); the browser pane reloads its own page on F5 and Ctrl/Cmd+R and
ignores the cache on the shifted chord.
2026-10-03 22:10:30 -07:00
Harbor404 6a508efac6 test(usage): isolate boot effect provider registration 2026-10-04 13:08:32 +08:00
YHH 11b0513747 Merge pull request #11903 from esengine/fix/feedback-keep-shots
fix(studio): keep feedback screenshots in the held draft
2026-10-03 22:03:25 -07:00
esengine 47d219e966 fix(studio): keep feedback screenshots in the held draft 2026-10-03 21:54:57 -07:00
YHH 72d4e9f28e Merge pull request #11711 from KHG420/codex/studio-export-json-encodings
fix(studio): export plugins using supported JSON encodings
2026-10-03 21:26:03 -07:00
Harbor404 46783d365f fix(usage): declare calendar range actions 2026-10-04 12:05:57 +08:00
YHH d486cfe71f Merge pull request #11808 from KHG420/codex/studio-remote-manifest-size
fix(install): require complete bounded remote manifests
2026-10-03 21:02:13 -07:00
xiexian 0f8dfa34f9 fix(studio): export plugins using supported JSON encodings 2026-10-04 11:58:15 +08:00
xiexian dabc3481d2 fix(install): require complete bounded remote manifests 2026-10-04 11:51:27 +08:00
YHH 0195f8241a Merge pull request #11897 from esengine/fix/model-switch-not-default
fix(serve): a session's model switch no longer rewrites default_model
2026-10-03 20:51:23 -07:00
esengine bc13eddb53 fix(serve): a session's model switch no longer rewrites default_model
POST /model persisted every switch as the user's default model, so changing
the model inside one conversation silently changed what every new session and
the next launch started on. The typed /model command, the terminal UI and ACP
never did this; only the HTTP picker path did.

The request now carries an explicit `default` flag. The Settings default-model
row and the first-run model choice send it; the composer picker does not, so
it changes only that session. A pane resolving through the hub's resolver
still never writes this machine's config.
2026-10-03 20:40:02 -07:00
YHH d98832f695 Merge pull request #11816 from esengine/feat/fix-11785
fix(mcp): redact endpoint credentials fail-closed in every display, preview and export surface
2026-10-03 19:32:40 -07:00
Harbor404 310ffda281 fix(usage): align source bars with chart tokens 2026-10-04 10:30:45 +08:00
YHH d7744a1616 Merge pull request #11895 from esengine/fix/powershell-parser-cold-start
fix(bash): give the PowerShell delete parser a cold-start budget
2026-10-03 19:22:45 -07:00
esengine 0da518ce84 fix(mcp): redact endpoint credentials fail-closed in every display, preview and export surface
One structural rule (secrets.RedactEndpoint / CredentialKey / RedactArgs)
replaces mcpsetup.RedactURL's query-only masking. Userinfo and fragments are
dropped or masked, query and fragment keys are decoded before classification,
unparseable endpoints render as <redacted>, and transport diagnostics keep
error identity while omitting peer-supplied prose.

Operational values (install, approval fingerprints, persistence) are untouched;
preview and plan responses add separate display fields that the desktop mirror
and confirmation UI render.

Rebased onto the launcher-aware NameFromArgv: names derive from the original
argv but never from a value the display projection masked, so build-flag values
such as `go run -exec` still resolve. Install classification reads the typed
HTTP status instead of sniffing the server name out of the message.

Fixes #11785
2026-10-03 19:19:58 -07:00
esengine 358fb4cff2 fix(bash): give the PowerShell delete parser a cold-start budget separate from per-call latency 2026-10-03 19:07:06 -07:00
YHH 7aeae1990d Merge pull request #11824 from esengine/feat/fix-images-fixture
test(boot): fix the shell used by the restart image effect test
2026-10-03 18:52:49 -07:00
YHH b2db65b417 Merge pull request #11831 from esengine/feat/fix-queue-withdraw
fix(studio): clear the composer on submit and restore a withdrawn queued message exactly once
2026-10-03 18:51:52 -07:00
YHH 43372e6ce7 Merge pull request #11893 from esengine/fix/focus-test-flaky
test(studio): wait for the plugin recovery focus move
2026-10-03 18:04:28 -07:00
esengine b650e362df test(studio): wait for the plugin recovery focus move instead of asserting it immediately
The primary button is focused from a passive effect that runs after the
element is committed, so asserting document.activeElement right after a
findBy* resolves races that effect. Use waitFor, as settings-layers does.
2026-10-03 17:37:55 -07:00
YHH 5bfdfb0bd6 Merge pull request #11891 from esengine/fix/release-credit-owner
fix(release): never credit the repository owner in release notes
2026-10-03 17:21:59 -07:00
YHH 3c060de3e2 Merge pull request #11890 from esengine/docs/studio-2.27.0-notes
docs(release): prepare Studio 2.27.0 notes
2026-10-03 17:14:40 -07:00
esengine de789afafb fix(release): never credit the repository owner in release notes 2026-10-03 17:12:08 -07:00
esengine c83f5931fc docs(release): prepare Studio 2.27.0 notes 2026-10-03 17:07:07 -07:00
YHH 028179b37f Merge pull request #11889 from esengine/perf/boot-test-close-catalogs-before-tempdir
test(boot): close process-global catalogs before the temp dir is removed
2026-10-03 16:31:54 -07:00
esengine 880dcc5b64 test(boot): close process-global catalogs before the temp dir is removed
testenv.TempDir retries RemoveAll for two seconds when a file stays held.
On Windows the usage and history SQLite catalogs, which the shared helpers
left open, held a file under nearly every test's directory, so each of those
tests paid the full retry budget at teardown. Closing the catalogs from the
helper's own cleanup, which runs before the removal, makes it converge on
the first attempt.
2026-10-03 16:22:09 -07:00
YHH 8576a0b0ff Merge pull request #11740 from KHG420/codex/studio-unicode-skill-inventory
fix(studio): align package skill inventory with runtime names
2026-10-03 15:53:09 -07:00
YHH 37056a8dba Merge pull request #11865 from KHG420/codex/studio-go-mcp-names
fix(studio): unify MCP launcher name inference
2026-10-03 15:52:29 -07:00
xiexian a078f8e30f fix(studio): unify MCP launcher name inference 2026-10-04 06:38:46 +08:00
YHH 875dd8e67a Merge pull request #11569 from KHG420/codex/studio-compatible-command-example
docs(ecosystem): add compatible command example and host proof
2026-10-03 15:19:23 -07:00
YHH 558b9cbbb2 Merge pull request #11814 from KHG420/codex/studio-theme-import-ignored-files
fix(studio): reject mixed theme archive selections
2026-10-03 15:18:08 -07:00
YHH 461e03770f Merge pull request #11811 from KHG420/codex/studio-theme-asset-freshness
fix(studio): revalidate theme assets with content ETags
2026-10-03 15:16:35 -07:00
YHH dbc79359c0 Merge pull request #11787 from KHG420/codex/studio-mcp-endpoint-transport-path
fix(studio): infer MCP transport from endpoint path segments
2026-10-03 15:14:38 -07:00
YHH 5dc827a1c0 Merge pull request #11821 from KHG420/codex/studio-publish-kind-keys
fix(studio): share keyboard navigation for market radios
2026-10-03 15:06:26 -07:00
YHH 9de8fe7374 Merge pull request #11886 from esengine/perf/boot-scan-limit
test(boot): make the workspace scan limit injectable (refs #11885)
2026-10-03 14:02:07 -07:00
YHH 075db5672f Merge pull request #11888 from esengine/fix/shellrun-quiet-timeout-test
test(shellrun): stop the quiet-timeout test racing interpreter startup
2026-10-03 13:52:23 -07:00
YHH 3dd8fd4dfa Merge pull request #11887 from esengine/perf/bash-probe-memo
perf(sandbox): remember a bash.exe that already passed the launch probe (refs #11885)
2026-10-03 13:51:38 -07:00
esengine 4a82273735 perf(sandbox): do not relaunch bash -c true for a bash.exe already proven under the same size and mtime
A successful Windows bash probe is remembered per executable identity (path, size, mtime) for the life of the process; failures are never remembered. Every boot.Build used to spawn Git bash for it.
2026-10-03 13:45:23 -07:00
esengine 65f5a0c6f3 test(boot): make the workspace scan limit injectable so the over-limit case needs 65 files, not 50,001
agent.Options.WorkspaceScanLimit (zero keeps the 50,000 production bound) is threaded from boot.Options. The incomplete-prose-scan effect test now sets it to 64 and writes 65 files instead of 50,001.
2026-10-03 13:44:58 -07:00
esengine 7e469c1c59 test(shellrun): inject the child in the quiet-timeout test so interpreter startup cannot race the deadline
The deadline starts at launch, so with a real interpreter the test measured startup time. Under load Git bash needed more than 500 ms and the pre-deadline write never landed.
2026-10-03 13:39:36 -07:00
YHH e190049a70 Merge branch 'studio' into codex/studio-theme-import-ignored-files 2026-10-03 13:11:29 -07:00
YHH a3fd97ab74 Merge pull request #11810 from KHG420/codex/studio-theme-import-lock
fix(studio): serialize theme import feedback
2026-10-03 13:00:46 -07:00
xiexian b8e25a4bb4 fix(studio): reject mixed theme archive selections 2026-10-04 03:56:56 +08:00
xiexian 98e1ebe28f fix(studio): serialize theme import feedback 2026-10-04 03:45:20 +08:00
YHH 963fd1eec1 Merge branch 'studio' into codex/studio-unicode-skill-inventory 2026-10-03 12:30:55 -07:00
YHH 09ba9cddc5 Merge pull request #11882 from esengine/fix/flaky-shellrun-quiet-timeout
test(shellrun): give the quiet-timeout case a deadline that outlasts PowerShell startup
2026-10-03 12:30:23 -07:00
YHH 599ba7cb77 Merge pull request #11883 from esengine/fix/flaky-remember-permission-waits
test(boot): bound permission-rule contention by writer count and process readiness by testenv.Budget
2026-10-03 12:27:25 -07:00
YHH 4e41da5a81 Merge pull request #11881 from esengine/fix/flaky-control-wait-budget
test(control): wait for turn events within testenv.Budget instead of fixed 2-10s
2026-10-03 12:26:21 -07:00
esengine 8e87a1557b test(boot): bound permission-rule contention by writer count and process readiness by testenv.Budget
TestRememberPermissionRuleSerializesConcurrentWriters queued 32 writers on a
lock that waits 5s, so a slow Windows filesystem ran the last waiters out of
time (lock timeout at 5.08s). The cross-process variant had the same shape:
each helper waited a fixed 5s for the others to spawn. Eight writers still
interleave, and the readiness waits now read the shared hang budget.
2026-10-03 12:07:30 -07:00
esengine 39fddef450 test(shellrun): give the quiet-timeout case a deadline that outlasts PowerShell startup
The deadline is armed at launch, so a 500ms budget races interpreter startup:
on a loaded Windows runner PowerShell had not printed anything when it
fired, and the test reported the pre-deadline write as lost. PowerShell now
gets a 10s deadline named for what it has to outlast; POSIX shells keep 500ms.
2026-10-03 12:05:48 -07:00
esengine 8f61e66cf1 test(control): wait for turn events within testenv.Budget instead of fixed 2-10s
Hang-catching waits in the control tests were hard-coded to 2, 3, 5 or 10
seconds. A whole turn through the real agent loop can exceed that on a
loaded Windows runner, so TestTurnOutcomeClassifiesPerseverationPause failed
with 'timed out waiting for TurnDone' on a run that passed on rerun. They now
read testenv.Budget(t), which is the single named bound for a wait that only
exists to report a hang.
2026-10-03 12:05:09 -07:00
xiexian 00c97a6036 fix(studio): align package skill inventory with runtime names 2026-10-04 02:43:46 +08:00
YHH 94d1a293f5 Merge branch 'studio' into codex/studio-compatible-command-example 2026-10-03 11:22:13 -07:00
YHH c66775519f Merge branch 'studio' into codex/studio-theme-asset-freshness 2026-10-03 11:22:04 -07:00
YHH a87f125b27 Merge pull request #11622 from Harbor404/fix/11619-ssh-login-path
fix(remote): cancel SSH session opening during bootstrap
2026-10-03 11:18:31 -07:00
Harbor404 c13fad9b1c fix(remote): limit session-open cancellation side effects 2026-10-04 02:04:27 +08:00
YHH 9a2a6bb03b Merge pull request #11857 from esengine/feat/fix-11846
fix(memory): back up legacy memory files before the V2 migration rewrites them
2026-10-03 10:44:08 -07:00
YHH f6b1c6b7f7 Merge pull request #11788 from KHG420/codex/studio-sdk-host-test-guide
docs(sdk): explain local tests and custom host shutdown
2026-10-03 10:32:30 -07:00
esengine 8787ef30a2 fix(memory): back up legacy memory files before the V2 migration rewrites them 2026-10-03 10:30:54 -07:00
xiexian 024b50c173 docs(sdk): explain local tests and custom host shutdown 2026-10-04 01:18:05 +08:00
xiexian 65c7316669 fix(studio): share market radio keyboard navigation 2026-10-04 01:10:46 +08:00
xiexian a4b5a9a485 docs(ecosystem): add compatible command example and host proof 2026-10-04 01:02:54 +08:00
YHH 26ab215cbf Merge pull request #11749 from KHG420/codex/studio-local-mcp-pending-scope
fix(studio): retain local scope for pending MCP installs
2026-10-03 09:40:19 -07:00
YHH a9de4f4f3d Merge pull request #11879 from esengine/feat/fix-device-ordinal-pr0
fix(serve): reuse the lowest free ordinal for Internet controllers
2026-10-03 09:37:41 -07:00
YHH d82fa6aee1 Merge pull request #11477 from KHG420/codex/studio-update-lock-feedback
fix(studio): show disabled update navigation and cover lock release
2026-10-03 09:30:00 -07:00
YHH 34f7203713 Merge pull request #11578 from KHG420/codex/studio-theme-author-guide
docs(studio): add current theme author guide and starter
2026-10-03 09:28:25 -07:00
YHH 6a30dfaca0 Merge pull request #11877 from esengine/feat/fix-snapshot-bound-test
test(boot): bound snapshot walk goroutines by pool size, not total growth
2026-10-03 09:26:56 -07:00
esengine 8e24f4c89c fix(serve): reuse the lowest free ordinal for Internet controllers
cloudPresence kept a monotonic counter, so every reconnect of the same phone
took the next number. Allocate the lowest ordinal not held by a live
connection instead; it stays fixed for the connection's lifetime.
2026-10-03 09:23:51 -07:00
xiexian 53a27ecb58 fix(studio): show disabled update navigation and cover lock release 2026-10-04 00:19:58 +08:00
YHH 8e5a124070 Merge branch 'studio' into fix/studio-11615-usage-ranges 2026-10-03 09:17:32 -07:00
esengine cdab4ade0b test(boot): bound walk goroutines by pool size, not total goroutine growth
The wide-workspace effect test compared total goroutine growth against a
magic 64, which host background work (sqlite watchers in the history
catalog) pushed past under load. Count only goroutines inside the walkers
and bound them by pool size times concurrent walk kinds; assert none
survive the turn.
2026-10-03 09:13:38 -07:00
YHH 7b09c445c5 Merge pull request #11869 from esengine/feat/fix-11838
fix(compaction): name why an automatic fold failed instead of "folded nothing"
2026-10-03 09:13:05 -07:00
YHH f87f3acd8f Merge pull request #11784 from KHG420/codex/studio-market-install-result-recovery
fix(studio): recover failed market install results
2026-10-03 09:09:16 -07:00
YHH bc73c23120 Merge pull request #11716 from KHG420/codex/studio-private-publish-summary
fix(studio): explain private market drafts before saving
2026-10-03 09:07:11 -07:00
Harbor404 419899ab5e fix(remote): cancel while opening an SSH session 2026-10-04 00:05:22 +08:00
xiexian 29791a36c6 docs(studio): add current theme author guide and starter 2026-10-04 00:04:23 +08:00
Harbor404 6d4b5dcb6a fix(usage): label sources without display names 2026-10-04 00:03:42 +08:00
YHH 949d237c34 Merge pull request #11875 from esengine/feat/fix-writepaths-win-test
test(builtin): compare ambiguous writer grant targets against the resolved root
2026-10-03 08:58:52 -07:00
YHH 1e6b185c2d Merge pull request #11651 from KHG420/codex/studio-plugin-failure-recovery
fix(studio): recover failed plugin installs through fresh previews
2026-10-03 08:55:22 -07:00
YHH 960a5a0105 Merge pull request #11796 from KHG420/codex/studio-package-refresh-recovery
fix(studio): recover from extension inventory read failures
2026-10-03 08:54:27 -07:00
esengine 6fe0755a24 fix(compaction): name why an automatic fold failed instead of "folded nothing"
A fold that was refused or failed ended with an aborted card reading
"folded nothing" and a blocked receipt whose code was empty; the cause lived
only in a sentence. Checkpoint rejections now carry a code per rule, summary
failures are classified by sentinel (truncated, timeout, input too large,
context changed, provider failure), and the code rides both the receipt and
the aborted compaction frame. The Studio card shows a localized cause, and
keeps its before/after token pair on one line when the row wraps.
2026-10-03 08:47:02 -07:00
xiexian c023b9f117 fix(studio): explain private market drafts before saving 2026-10-03 23:45:25 +08:00
esengine 9e2664b8b9 test(builtin): compare ambiguous writer grant targets against the resolved root
The Windows runner's temp dir is an 8.3 short name. The grant target for an
ambiguous path is resolved to the long form, so the expectation built from the
unresolved root never matched.
2026-10-03 08:45:06 -07:00
YHH 482f93af21 Merge pull request #11874 from esengine/feat/fix-market-own-review-test
test(studio): drive own-review regression through the real inventory reload
2026-10-03 08:38:35 -07:00
YHH fdc2b8d94b Merge pull request #11269 from zcxads666/fix-studio-inherited-subagent-effort
fix(boot): let inherited subagent effort yield to execution model (studio)
2026-10-03 08:34:56 -07:00
esengine 25b43266b7 test(studio): drive own-review regression through the real inventory reload
The regression held row and button references across the refetch that
PackageList.submit now triggers after a successful review submission, and
its deferred submitMarket fixtures never updated the inventory myMarket
returns. Query live rows after each reload and delegate to the mock's real
submitMarket so the reloaded inventory reflects the new status.
2026-10-03 08:29:18 -07:00
xiexian c0856e8231 fix(studio): revalidate theme assets with content ETags 2026-10-03 23:26:41 +08:00
zcxads666 0d9c11d0a5 fix: keep inherited subagent effort within the execution model's declared capability 2026-10-03 08:20:03 -07:00
YHH bf03b2eafb Merge pull request #11847 from KHG420/codex/studio-mcp-scope-keyboard
fix(studio): support keyboard MCP installation locations
2026-10-03 08:03:04 -07:00
YHH 8fd6d1ae63 Merge pull request #11568 from esengine/feat/share-port
feat(share): let the phone-access LAN link use a fixed port
2026-10-03 08:01:19 -07:00
YHH 68a311ad02 Merge pull request #11807 from KHG420/codex/studio-install-confirmation-lock
fix(studio): expose market confirmation busy and error states
2026-10-03 07:46:12 -07:00
YHH 97c35fbd83 Merge pull request #11845 from KHG420/codex/studio-publish-enter-submit
fix(studio): submit publishing drafts with Enter
2026-10-03 07:38:34 -07:00
YHH 5495a4b404 Merge pull request #11580 from KHG420/codex/studio-market-review-pending
fix(studio): serialize own-package review submissions
2026-10-03 07:34:00 -07:00
YHH 95985b806a Merge pull request #11800 from KHG420/codex/studio-publisher-feedback-semantics
fix(studio): add semantics to publisher feedback
2026-10-03 07:33:09 -07:00
YHH 99ddf11b31 Merge pull request #11813 from KHG420/codex/studio-theme-preview-recovery
fix(studio): retry previews after theme inventory refresh
2026-10-03 07:30:53 -07:00
YHH ed24735e30 Merge pull request #11832 from KHG420/codex/studio-sdk-provider-lifecycle
test(studio): cover installed SDK provider streams at boot
2026-10-03 07:30:37 -07:00
xiexian 4ddf8bd730 fix(studio): expose market confirmation busy and error states 2026-10-03 22:29:36 +08:00
xiexian d974353edf fix(studio): submit publishing drafts with Enter 2026-10-03 22:18:18 +08:00
YHH 70b380471e Merge pull request #11556 from KHG420/codex/studio-fullsidecar-demo-guide
docs(sdk): align fullsidecar demo guidance
2026-10-03 07:16:37 -07:00
YHH 8306c2f66a Merge pull request #11823 from esengine/feat/fix-11815
fix(session): let sessions that write disjoint paths of one workspace proceed in parallel
2026-10-03 07:06:44 -07:00
YHH 8c25d59ac3 Merge pull request #11812 from KHG420/codex/studio-theme-read-recovery
fix(studio): retain themes when inventory reads fail
2026-10-03 07:05:47 -07:00
YHH 5faff41805 Merge pull request #11792 from KHG420/codex/studio-conformance-scratch-cleanup
test(extension): clean conformance scratch before exiting
2026-10-03 07:01:40 -07:00
YHH c249eb6500 Merge pull request #11818 from KHG420/codex/studio-install-source-labels
fix(studio): name installation source inputs accessibly
2026-10-03 06:59:09 -07:00
YHH 8bc1d025a0 Merge pull request #11738 from KHG420/codex/studio-export-whole-env-refs
fix(pluginpkg): redact literals mistaken for environment references
2026-10-03 06:58:10 -07:00
xiexian 5350d3ac34 fix(studio): support keyboard MCP installation locations 2026-10-03 21:51:35 +08:00
YHH 85b5e76d4d Merge pull request #11819 from KHG420/codex/studio-plugin-toggle-reload
fix(studio): retain plugin switch reload outcomes
2026-10-03 06:48:26 -07:00
YHH 9b03193a00 Merge pull request #11809 from KHG420/codex/studio-mcp-operation-errors
fix(studio): show current MCP operation diagnostics
2026-10-03 06:47:36 -07:00
YHH 030b625208 Merge pull request #11805 from KHG420/codex/studio-mcp-inspection-source-lock
fix(studio): keep MCP inspection input stable while reading
2026-10-03 06:46:42 -07:00
YHH b2225b6d72 Merge pull request #11781 from KHG420/codex/studio-market-vote-signout-recovery
fix(studio): recover signed-out market vote writes
2026-10-03 06:41:16 -07:00
YHH ec5f629213 Merge pull request #11866 from esengine/feat/fix-studio-red
fix(shell): only a leading tilde makes a delete target non-literal
2026-10-03 06:32:50 -07:00
esengine ed6b2ea02f fix(session): resolve delegated writes before the grant check so valid targets are not refused 2026-10-03 06:31:29 -07:00
esengine 20f452eb3f fix(session): claim the paths the writers use, close the Windows alias gap and avoid cross-session deadlock 2026-10-03 06:31:29 -07:00
esengine 0a009c71dc fix(session): let sessions that write disjoint paths of one workspace proceed in parallel 2026-10-03 06:31:12 -07:00
YHH c8dcf82efd Merge pull request #11850 from KHG420/codex/sdk-ui-result-ack
fix(sdk): require explicit UI result acknowledgements
2026-10-03 06:21:19 -07:00
YHH 7d630f5ff0 Merge pull request #11841 from esengine/feat/audit-effort-switch
fix(boot): a subagent that names the parent's model inherits the parent's effort
2026-10-03 06:11:44 -07:00
xiexian df5b824df9 fix(sdk): require explicit UI result acknowledgements 2026-10-03 21:05:36 +08:00
esengine 3589efed7b fix(shell): judge delete targets by expansion shape, accept only 8.3 short-name tildes
A tilde inside a Windows 8.3 short-name segment (RUNNER~1) of a drive-letter
path is an ordinary character; any other tilde can expand to a home
directory and stays refused as non-literal. Unescaped braces are refused
too, since a brace word expands to several targets.

Also give the PowerShell delete-analysis subprocess test a hang guard
instead of a 15 s latency assertion that fails on loaded runners.
2026-10-03 06:00:55 -07:00
YHH f66930fd12 Merge pull request #11840 from esengine/feat/fix-11820
fix(state): bound the workspace snapshot walk so goroutines and threads stay limited
2026-10-03 05:59:51 -07:00
esengine 2a3c2cd1f9 fix(boot): a subagent that names the parent's model inherits the parent's effort 2026-10-03 05:53:42 -07:00
YHH 87f1912c1f Merge pull request #11854 from esengine/feat/fix-compact-ratio
fix(cli): validate compact-ratio against the config layer bounds
2026-10-03 05:52:10 -07:00
YHH 5be2913263 Merge pull request #11862 from esengine/feat/ops-events-workflow
ci(ops): register the ops events relay deploy workflow on the default branch
2026-10-03 05:31:04 -07:00
esengine 1e1dbaf07c ci(ops): register the ops events relay deploy workflow on the default branch
workflow_dispatch workflows must exist on the default branch; the job itself only runs from platform, like the other worker deploys.
2026-10-03 05:14:17 -07:00
xiexian 16aeb9124d fix(studio): add semantics to publisher feedback 2026-10-03 19:56:39 +08:00
xiexian 74d977a5c9 fix(studio): recover failed market install results 2026-10-03 19:48:51 +08:00
YHH b7682ae672 Merge pull request #11835 from KHG420/codex/studio-sdk-response-format
fix(sdk): validate provider response format types
2026-10-03 04:36:21 -07:00
YHH fca92a5675 Merge pull request #11826 from KHG420/codex/studio-fullsidecar-null-arguments
fix(sdk): handle null arguments in fullsidecar demo
2026-10-03 04:24:13 -07:00
YHH 4fdaa75397 Merge pull request #11828 from KHG420/codex/studio-sdk-tool-argument-presence
fix(sdk): require tool argument presence
2026-10-03 04:15:13 -07:00
YHH 474ba205d1 Merge pull request #11802 from KHG420/codex/studio-sdk-cancelled-host-calls
fix(sdk): stop already-cancelled host requests before writing
2026-10-03 04:12:43 -07:00
YHH 5717f930f0 Merge pull request #11798 from KHG420/codex/studio-sdk-shutdown-callback-recovery
fix(sdk): recover shutdown callback panics
2026-10-03 04:09:32 -07:00
YHH 28c45b59d0 Merge pull request #11829 from esengine/feat/docs-change-discipline
docs: add the change discipline rules to REASONIX.md
2026-10-03 03:56:40 -07:00
xiexian 2e9045a300 fix(sdk): include externalized reads in the intercept budget 2026-10-03 18:20:33 +08:00
esengine 9692e499bb fix(cli): validate compact-ratio against the config layer bounds 2026-10-03 03:20:23 -07:00
esengine 801fd6b216 fix(state): publish immutable observation objects with a non-replacing link so concurrent observers cannot race the rename 2026-10-03 03:19:33 -07:00
YHH e4b862cf77 Merge pull request #11804 from KHG420/codex/studio-copied-mcp-launcher
fix(studio): recognize copied MCP add launcher paths
2026-10-03 03:14:29 -07:00
xiexian 8ecfa8c084 fix(studio): infer MCP transport from endpoint path segments 2026-10-03 18:13:11 +08:00
YHH e01622522d Merge pull request #11793 from KHG420/codex/studio-sdk-notification-close
fix(sdk): synchronize notification queue shutdown
2026-10-03 03:05:52 -07:00
xiexian cf490285b0 fix(sdk): cancel provider contexts when streams finish 2026-10-03 18:02:43 +08:00
Will ff8e467102 test(e2ebench): prove every declared-solvable verification-stress task can pass
The reference-solution guard fired only for the train corpus. For every other corpus a missing solution/ was a silent skip, so a grader that can never pass stayed indistinguishable from a task with no solution - the case the rule above it exists to prevent. #11380 supplied the ten vs-pos solutions; this extends the guard to the whole verification-stress corpus and supplies the ten tasks it was still skipping.

The e2e suite keeps the skip, and the skip message now says why: its handwritten tasks do not commit a reference solution.

Verified: go test ./cmd/e2ebench -run TestCorpusGradersPassTheReferenceSolution is green across e2e, verification-stress and train. Removing any single solution/ fails the guard at corpus_test.go:254 (checked by hand for vs-neu-cfgkey), so the added solutions are what the guard grades.
2026-10-03 17:51:55 +08:00
YHH 744fbd4f33 Merge pull request #11759 from esengine/feat/tray-language
fix(studio): pass the system language to the kernel host so the tray follows it
2026-10-03 02:40:15 -07:00
YHH 3b8817d29b Merge pull request #11801 from KHG420/codex/studio-uv-global-options
fix(studio): derive MCP names through uv global options
2026-10-03 02:37:28 -07:00
YHH f2a0893b07 Merge pull request #11799 from KHG420/codex/studio-node-mcp-option-values
fix(studio): skip Node option values in MCP names
2026-10-03 02:36:30 -07:00
YHH d0d7631a7d Merge pull request #11779 from KHG420/codex/studio-mcp-command-continuations
fix(studio): parse continued MCP setup commands
2026-10-03 02:34:07 -07:00
esengine b441b0da8b fix(state): bound the workspace snapshot walk so goroutines and threads stay limited 2026-10-03 00:33:21 -07:00
xiexian 54c8e7ab68 feat(sdk): add installable offline provider example 2026-10-03 15:16:42 +08:00
xiexian 74b6705484 fix(sdk): validate provider response format types 2026-10-03 14:37:10 +08:00
xiexian a5fe9d6b5f test(studio): cover installed SDK provider streams at boot 2026-10-03 14:26:05 +08:00
esengine bce7224270 fix(studio): clear the composer on submit and restore a withdrawn queued message exactly once 2026-10-02 23:19:21 -07:00
esengine 647c1baa5b test(host): call assemble with the startup phase recorder in the tray language effect test 2026-10-02 23:19:01 -07:00
esengine 1bc7fc11e9 docs: add the change discipline rules to REASONIX.md 2026-10-02 22:55:56 -07:00
xiexian e741ffe34b fix(sdk): require tool argument presence 2026-10-03 13:38:53 +08:00
xiexian 3b4a2a7619 fix(sdk): handle null arguments in fullsidecar demo 2026-10-03 13:27:39 +08:00
YHH 6e1a0cccaa Merge pull request #11786 from esengine/feat/fix-11782
fix(shell): refuse recursive deletes whose target is not a literal path inside the granted roots
2026-10-02 22:04:08 -07:00
par73e a0cc22bfc0 fix(studio): expose GLM 5.2 and 5.3 reasoning effort levels 2026-10-03 13:02:02 +08:00
esengine 3552dc1412 test(boot): fix the shell used by the restart image effect test 2026-10-02 22:00:06 -07:00
esengine 265dd7a393 fix(i18n): treat the C and POSIX locales as an explicit English choice and test the tray language end to end 2026-10-02 21:26:52 -07:00
xiexian b407cdceb0 fix(studio): retain plugin switch reload outcomes 2026-10-03 12:21:02 +08:00
xiexian 1d9b15589b fix(studio): name installation source inputs accessibly 2026-10-03 11:57:41 +08:00
YHH f9344a7eed Merge pull request #11760 from esengine/feat/startup-handshake
feat(host): log elapsed milliseconds per startup phase
2026-10-02 20:26:57 -07:00
xiexian 0d2893219f fix(studio): retry previews after theme inventory refresh 2026-10-03 10:57:36 +08:00
xiexian f4bd08549a fix(studio): retain themes when inventory reads fail 2026-10-03 10:48:18 +08:00
xiexian d27be95211 fix(studio): show current MCP operation diagnostics 2026-10-03 10:09:34 +08:00
xiexian 5a10bb1d7f fix(studio): keep blocked market previews out of confirmation 2026-10-03 09:30:11 +08:00
YHH cc5d768001 Merge pull request #11803 from esengine/feat/fix-prev-resp
fix(responses): fall back to full history when a relay rejects previous_response_id
2026-10-02 18:28:13 -07:00
YHH aee132a2f1 Merge pull request #11780 from esengine/feat/fix-11776
fix(evidence): do not demand verification after supporting-only edits when no checks exist
2026-10-02 18:27:16 -07:00
esengine 34f599dcc0 test(shellsafe): cover the delete classification inside its own package 2026-10-02 18:27:03 -07:00
xiexian 5bdaed66d1 fix(studio): keep MCP inspection input stable while reading 2026-10-03 09:13:14 +08:00
xiexian 4ec2f582c2 fix(studio): recognize copied MCP add launcher paths 2026-10-03 09:04:15 +08:00
esengine 3c0a655e90 fix(responses): keep continuation after an expired response id 2026-10-02 18:00:37 -07:00
esengine 813e6932ed fix(evidence): require every mutated path to be a prose path inside the observed root 2026-10-02 17:58:22 -07:00
YHH 11a4734d07 Merge pull request #11504 from Harbor404/fix/powershell-shell-probe
fix(shell): skip a PowerShell Store alias that will not start; record which ran
2026-10-02 17:58:09 -07:00
esengine 6ec0de92be fix(responses): fall back to full history when a relay rejects previous_response_id
A relay that rejected the continuation field with a message the client did not recognise surfaced the HTTP 400 to the user, and the stored response id was kept so the next turn sent the same rejected request. Any HTTP 400 on a request that carried previous_response_id now retries once with the full history; a completed recovery stops sending the field for the client's lifetime, a failed one surfaces its own error naming the attempt.
2026-10-02 17:57:57 -07:00
YHH 35c8104af2 Merge pull request #11546 from Harbor404/feat/effort-model-same-switch-fastpath
perf(studio): skip rebuilds for unchanged model or effort
2026-10-02 17:57:12 -07:00
xiexian cf3460a48f fix(sdk): stop already-cancelled host requests before writing 2026-10-03 08:47:02 +08:00
xiexian a8a632e775 fix(studio): derive MCP names through uv global options 2026-10-03 08:34:35 +08:00
esengine 5069d522bb test(shell): correct the glob cause and make the backslash root case Windows-only 2026-10-02 17:30:05 -07:00
xiexian ee24b43d47 fix(studio): skip Node option values in MCP names 2026-10-03 08:15:19 +08:00
xiexian 92c6944d9b fix(sdk): recover shutdown callback panics 2026-10-03 08:07:59 +08:00
xiexian d52ea1e1d1 fix(studio): recover from extension inventory read failures 2026-10-03 07:59:04 +08:00
esengine 412dd1c865 test(shellparse): cover the delete analysis inside its own package and document the workspace-root refusal 2026-10-02 16:56:38 -07:00
esengine 0ef669e872 style(agent): use the shared receiver name for the prose scan check 2026-10-02 16:28:12 -07:00
xiexian 431dd159a8 fix(sdk): synchronize notification queue shutdown 2026-10-03 07:05:17 +08:00
esengine 72a59c76d4 fix(shell): judge only recursive deletes, attribute each refusal, and close the structural bypasses found in review
Revision after the security review: ordinary commands with dynamic executable names, non-recursive deletes and non-delete wrappers launch again; each refusal carries its own cause and remedy; bounded compound sequences with literal targets inside the granted roots are allowed; default-parameter, pipeline, alias, .NET, find, xargs, cmd and static interpreter payload deletes are judged; symlink components are resolved before dot-dot cleaning; the session temp root is a granted scope; the PowerShell parser is reused across calls; bilingual copy for the new codes.
2026-10-02 16:00:21 -07:00
esengine 83f13a3b1c Merge branch 'studio' of https://github.com/esengine/DeepSeek-Reasonix into HEAD 2026-10-02 15:59:59 -07:00
esengine 93c5419f94 test(evidence): make the prose-only tests independent of fixture files and filesystem case 2026-10-02 15:59:52 -07:00
Harbor404 93bb4eb606 Revert "fix(cli): align plugin doctor root diagnostics"
This reverts commit 41eeae6cd0.
2026-10-03 06:56:15 +08:00
Harbor404 791eca3111 Revert "fix(cli): align plugin doctor root diagnostics"
This reverts commit 73431b67e1.
2026-10-03 06:52:43 +08:00
xiexian b32768b6e0 test(extension): clean conformance scratch before exiting 2026-10-03 06:41:20 +08:00
YHH 7d5bcfd5a9 Merge pull request #11790 from esengine/feat/studio-red
fix(cli): print package warnings before plugin doctor rejects an agent root
2026-10-02 15:39:31 -07:00
esengine b8af495238 fix(cli): print package warnings before plugin doctor rejects an agent root
Doctor now validates declared agent roots fatally, which the warning test added by the root-diagnostics change did not expect, so the two merged changes failed together. The warnings are printed first and the root rejection keeps its exit status.
2026-10-02 15:28:27 -07:00
Harbor404 41eeae6cd0 fix(cli): align plugin doctor root diagnostics 2026-10-03 05:57:48 +08:00
Harbor404 fa5816d74e chore(merge): sync with latest studio 2026-10-03 05:57:46 +08:00
Harbor404 73431b67e1 fix(cli): align plugin doctor root diagnostics 2026-10-03 05:56:05 +08:00
Harbor404 38def5abc8 chore(merge): sync with latest studio 2026-10-03 05:53:54 +08:00
esengine 9d4f5ab8d1 test(evidence): cover prose-only waiver edge cases through boot.Build 2026-10-02 14:27:50 -07:00
esengine 5834500883 fix(shell): refuse recursive deletes whose target is not a literal path inside the granted roots
Approval alone did not stop a recursive delete: in unattended Auto and YOLO a PowerShell sequence whose first assignment failed non-terminally went on to delete the real home directory. A pre-launch gate now parses the command (bash AST, PowerShell parser run as data) and refuses recursive deletes whose target is the home directory, a drive or filesystem root, a workspace ancestor, an expansion, variable or glob, or outside the workspace and granted roots, with a typed shell.destructive_target code; parser failure is shell.analysis_unknown. The gate runs after approval and before every launch route.
2026-10-02 14:26:53 -07:00
YHH 3068d41e3d Merge pull request #11566 from KHG420/codex/studio-market-version-draft
feat(studio): prepare market release drafts from own packages
2026-10-02 14:16:34 -07:00
YHH 02d96f2b8b Merge pull request #11506 from Harbor404/fix/disabled-tools-followups
fix(mcp): preserve disabled tool policy
2026-10-02 14:14:46 -07:00
esengine 20ad469a89 wip: waive verification only in prose-only workspaces 2026-10-02 14:00:50 -07:00
Harbor404 ffc9c3b738 docs(tool): clarify disabled MCP comments 2026-10-03 04:58:46 +08:00
Harbor404 9200563b99 docs(shell): trim review comments 2026-10-03 04:57:05 +08:00
Harbor404 e5e30b6a99 test(boot): filter shell fallback notices 2026-10-03 04:55:28 +08:00
Harbor404 88c3af6913 chore(merge): sync with latest main 2026-10-03 04:53:47 +08:00
Harbor404 d90d071453 test(boot): isolate disabled MCP effect build 2026-10-03 04:53:18 +08:00
Harbor404 4c5df4360f chore(merge): sync with latest main 2026-10-03 04:52:52 +08:00
Harbor404 526544fdf3 docs(serve): trim switch comments 2026-10-03 04:52:32 +08:00
Harbor404 311b5885de chore(merge): sync with latest main 2026-10-03 04:51:26 +08:00
xiexian 7b1ba8b87d feat(studio): prepare market release drafts from own packages 2026-10-03 04:46:18 +08:00
YHH 4f8f8a5bd5 Merge pull request #11553 from KHG420/codex/studio-sdk-tool-example
docs(sdk): add an installable tool extension example
2026-10-02 13:25:12 -07:00
YHH 3c1ad1153b Merge pull request #11584 from KHG420/codex/studio-mcp-author-example
feat(examples): add a runnable stdio MCP plugin
2026-10-02 13:23:31 -07:00
YHH 323b1857f9 Merge pull request #11503 from KHG420/codex/studio-starter-sidecar-lifecycle
fix(sdk): make starter interceptor work on composed inputs
2026-10-02 13:21:33 -07:00
YHH 54b6f28f80 Merge pull request #11748 from KHG420/codex/studio-command-markdown-suffix
fix(studio): drop mixed-case Markdown command suffixes
2026-10-02 13:18:03 -07:00
YHH 7ef30437f7 Merge pull request #11768 from KHG420/codex/studio-mcp-url-scheme-case
fix(studio): recognize HTTP URL schemes regardless of case
2026-10-02 13:17:06 -07:00
YHH f725e9fb27 Merge pull request #11766 from KHG420/codex/studio-node-server-command-name
fix(studio): derive Node MCP names from script entry points
2026-10-02 13:16:08 -07:00
YHH 9ad79c5727 Merge pull request #11767 from KHG420/codex/studio-uv-dependency-server-name
fix(studio): skip uv dependency values when deriving MCP names
2026-10-02 13:12:13 -07:00
YHH ebc29fc2bc Merge pull request #11507 from KHG420/codex/studio-installed-hook-context
test(studio): verify installed hook context lifecycle
2026-10-02 13:04:51 -07:00
YHH b8e07b42a9 Merge pull request #11508 from KHG420/codex/studio-installed-command-effect
test(boot): verify copied slash commands across their lifecycle
2026-10-02 13:03:58 -07:00
YHH 971cea87dc Merge pull request #11777 from esengine/feat/fix-11769
fix(studio): show a signed-in account consistently when the identity service is unreachable
2026-10-02 12:56:43 -07:00
xiexian cc47e5a0ad fix(studio): recover signed-out market vote writes 2026-10-03 03:39:05 +08:00
YHH 594c16e421 Merge pull request #11725 from KHG420/codex/studio-export-valid-env-names
fix(pluginpkg): generate expandable numeric server aliases
2026-10-02 12:37:11 -07:00
YHH 66705ae660 Merge pull request #11689 from KHG420/codex/studio-plugin-root-diagnostics
fix(studio): diagnose native contribution roots that name files
2026-10-02 12:34:00 -07:00
YHH fb54b4cfef Merge pull request #11696 from KHG420/codex/studio-empty-plugin-guidance
fix(studio): correct empty plugin list install guidance
2026-10-02 12:33:01 -07:00
esengine 4b79709cdb fix(evidence): do not demand verification after supporting-only edits when no checks exist
A write to a markdown or text file in a workspace with no declared or captured checks still added a stale_verification debt and, in Balanced, a readiness floor demanding a verification command. When every proven mutation is a supporting-role path and nothing declares or captured a check, neither applies. Mixed, opaque, unproven or partially observed effects and declared checks keep their behaviour.
2026-10-02 12:32:43 -07:00
xiexian 9e0ef76d56 fix(studio): parse continued MCP setup commands 2026-10-03 03:23:59 +08:00
Harbor404 5b07c3dae9 test(shell): prove fallback notice reaches sink 2026-10-03 03:12:18 +08:00
Harbor404 71d23f0ba1 chore(merge): sync with latest main 2026-10-03 03:10:38 +08:00
Harbor404 b5b9f3d523 fix(agent): surface disabled tool refusals first 2026-10-03 03:10:01 +08:00
Harbor404 f65d5aec06 chore(merge): sync with latest main 2026-10-03 03:04:34 +08:00
Harbor404 56792a367c fix(acp): persist no-op config choices 2026-10-03 03:03:58 +08:00
Harbor404 3d9d65b19e chore(merge): sync with latest main 2026-10-03 02:59:47 +08:00
Harbor404 63bbb1334d docs(ui): remove session info helper comment 2026-10-03 02:59:04 +08:00
Harbor404 d2d5571a00 chore(merge): sync with latest main 2026-10-03 02:58:12 +08:00
esengine 69e7f2c7b6 fix(studio): show a signed-in account consistently when the identity service is unreachable
The sidebar row and the account panel disagreed when a token was present but the identity lookup failed: the row showed a signed-in state with a question-mark avatar while the panel drew the sign-in form and hid the error. Both now render the signed-in state, the identity error, a retry and confirmed sign-out.
2026-10-02 11:58:00 -07:00
Harbor404 e730dafd27 fix(usage): keep range errors inline 2026-10-03 02:57:13 +08:00
Harbor404 e1ca40f84a chore(merge): sync with latest main 2026-10-03 02:52:21 +08:00
YHH 15a6b02f1f Merge pull request #11765 from KHG420/codex/studio-registry-resolve-pagination
fix(studio): follow Registry pages for exact MCP installation names
2026-10-02 11:45:59 -07:00
YHH cb880f030c Merge pull request #11543 from Harbor404/fix/remote-open-absolute-workspace
fix(remote): open absolute Windows workspaces from tree
2026-10-02 11:42:48 -07:00
YHH e4f71647c8 Merge pull request #11764 from KHG420/codex/studio-python-server-command-name
fix(studio): derive Python MCP names from interpreter entry points
2026-10-02 11:41:51 -07:00
YHH 1cde2048cc Merge pull request #11698 from KHG420/codex/studio-ci-supervisor-crash-fixture
test(schedrun): wait for child startup before supervisor crash
2026-10-02 11:32:18 -07:00
xiexian 2c40d8905b fix(studio): skip uv dependency values when deriving MCP names 2026-10-03 02:19:40 +08:00
xiexian f8843f4b2c fix(cli): detect non-executable plugin runtimes in doctor 2026-10-03 02:15:08 +08:00
YHH 2902ca1bde Merge pull request #11705 from KHG420/codex/studio-ci-stage-fixture
test(gitcmd): mark rewritten staging fixture stat dirty
2026-10-02 11:10:50 -07:00
YHH c72c073c26 Merge pull request #11715 from KHG420/codex/studio-quoted-prose-paragraphs
fix(repolint): keep quoted paragraphs separate
2026-10-02 11:09:51 -07:00
YHH 8612f792e6 Merge pull request #11724 from KHG420/codex/studio-export-mcp-header-effect
test(boot): exercise exported MCP recipient headers
2026-10-02 11:07:31 -07:00
YHH e8aba147d9 Merge pull request #11699 from Guan0923/codex/rewind-backup-cleanup
fix(checkpoint): 回退成功后清理临时备份
2026-10-02 11:04:02 -07:00
YHH c8339e2cdd Merge pull request #11755 from KHG420/codex/studio-uvx-pinned-server-name
fix(studio): keep uvx version pins out of MCP server names
2026-10-02 10:56:34 -07:00
YHH aff4de8a9b Merge pull request #11756 from KHG420/codex/studio-uvx-from-server-name
fix(studio): skip uvx from values when deriving MCP names
2026-10-02 10:54:36 -07:00
YHH c8e7d97057 Merge pull request #11753 from KHG420/codex/studio-mcp-runner-extension-case
fix(studio): derive MCP names through mixed-case runner extensions
2026-10-02 10:47:49 -07:00
YHH 95334537fe Merge pull request #11752 from KHG420/codex/studio-mcp-empty-definitions
fix(studio): omit empty MCP definitions from pasted previews
2026-10-02 10:45:11 -07:00
YHH 46c6649939 Merge pull request #11758 from KHG420/codex/studio-sdk-served-tool-declarations
fix(sdk): declare only callable tool handlers
2026-10-02 10:43:12 -07:00
YHH 7f575027ed Merge pull request #11747 from KHG420/codex/studio-mcp-pasted-cli-command
fix(studio): parse copied MCP add commands as declarations
2026-10-02 10:37:47 -07:00
YHH 6c11051633 Merge pull request #11745 from KHG420/codex/studio-doctor-agent-roots
fix(studio): validate declared agent roots in plugin doctor
2026-10-02 10:36:48 -07:00
xiexian c4f6c84852 fix(studio): recognize HTTP URL schemes regardless of case 2026-10-03 01:13:22 +08:00
xiexian 9097d2a036 fix(studio): derive Node MCP names from script entry points 2026-10-03 00:38:57 +08:00
YHH e443274913 Merge pull request #11737 from KHG420/codex/studio-package-contribution-rows
fix(studio): replace duplicate contribution rows with current package inventory
2026-10-02 09:35:22 -07:00
YHH e9a0801a58 Merge pull request #11733 from KHG420/codex/studio-package-diagnostic-rows
fix(studio): refresh repeated compatibility issue rows
2026-10-02 09:34:24 -07:00
YHH 9d806ed477 Merge pull request #11736 from KHG420/codex/studio-switch-skill-store
fix(boot): retain configured skill discovery after Studio model switches
2026-10-02 09:18:31 -07:00
xiexian f2cef6dfa8 fix(studio): follow Registry pages for exact MCP installation names 2026-10-03 00:17:39 +08:00
xiexian cefd143bc1 fix(studio): derive Python MCP names from interpreter entry points 2026-10-03 00:00:16 +08:00
esengine 49b5b934a5 feat(host): log elapsed milliseconds per startup phase 2026-10-02 08:56:39 -07:00
esengine d3d3cba4ea fix(studio): pass the system language to the kernel host so the tray follows it 2026-10-02 08:56:31 -07:00
xiexian 9aa53444c7 fix(sdk): declare only callable tool handlers 2026-10-02 23:45:31 +08:00
xiexian 8742e86433 fix(studio): skip uvx from values when deriving MCP names 2026-10-02 23:12:14 +08:00
xiexian da0221a66f fix(studio): keep uvx version pins out of MCP server names 2026-10-02 23:04:40 +08:00
xiexian be46a8575f fix(studio): derive MCP names through mixed-case runner extensions 2026-10-02 22:53:17 +08:00
xiexian 7fb8810372 fix(studio): omit empty MCP definitions from pasted previews 2026-10-02 22:45:25 +08:00
xiexian b854806701 fix(studio): retain local scope for pending MCP installs 2026-10-02 22:27:30 +08:00
xiexian 9ce710a540 fix(studio): drop mixed-case Markdown command suffixes 2026-10-02 22:13:50 +08:00
xiexian 4a963d4b9f fix(studio): parse copied MCP add commands as declarations 2026-10-02 22:04:35 +08:00
xiexian 0f98adf813 fix(studio): validate declared agent roots in plugin doctor 2026-10-02 21:55:49 +08:00
YHH 7bdaae2025 Merge pull request #11731 from esengine/fix/11723-narrow-sidebar-overlap
fix(studio): keep the narrow workspace column legible
2026-10-02 06:36:17 -07:00
YHH db267ac05c Merge pull request #11732 from esengine/fix/11720-run-analysis-inline-detail
fix(studio): open a run-analysis record under the row that was clicked
2026-10-02 06:35:19 -07:00
YHH 9ef965a9d6 Merge pull request #11729 from esengine/fix/11727-menu-position-at-scale
fix(studio): place rail context menus correctly at a zoomed interface size
2026-10-02 06:34:22 -07:00
xiexian 01044c54b7 fix(pluginpkg): preserve only expandable whole environment references 2026-10-02 21:05:11 +08:00
xiexian 0bb6aff2e7 fix(studio): refresh duplicate package contribution rows 2026-10-02 20:53:08 +08:00
xiexian 2e423c43df fix(boot): preserve live skill discovery across Studio model switches 2026-10-02 20:44:44 +08:00
esengine 620aede3d8 fix(studio): open a run-analysis record under the row that was clicked
The record panel was rendered after the whole active-turns list, so with a few
hundred rows it sat thousands of pixels below the click. It now renders inside
the clicked row's list item (or under the signal track for a signal), the same
item stays mounted, and clicking again closes it.
2026-10-02 05:40:13 -07:00
esengine d3f2a601c3 fix(studio): keep the narrow workspace column legible
The session filter row gave four equal cells to labels with counts, so at the
old 176px floor the labels collided; the new-session and search rows wrapped
around their shortcut hints. Raise the column floor to 232px, let the filter
cells size to their labels, ellipsize labels, and drop counts and shortcut
hints below a container width.
2026-10-02 05:38:52 -07:00
YHH 91c13b306c Merge pull request #11726 from esengine/docs/release-2.26.0-notes
docs(release): prepare Studio 2.26.0 notes
2026-10-02 05:28:58 -07:00
xiexian 1bc3c46128 fix(studio): refresh repeated compatibility issue rows 2026-10-02 20:26:02 +08:00
esengine 001473b7a8 docs(release): keep 2.26.0 note bullets within the prose limit 2026-10-02 05:20:20 -07:00
esengine 1f11055c1f fix(studio): place rail context menus correctly at a zoomed interface size
The session and project menus wrote clientX/clientY into left/top, but the
interface scale is CSS zoom, so a fixed element's left/top is multiplied by the
zoom on its way in. Both now go through pinToViewport, which divides by the
zoom and clamps the measured box inside the viewport. The session menu's
max-height is divided by the zoom as well.
2026-10-02 05:17:40 -07:00
esengine 415445ffbc docs(release): mention the author line and Douyin in the 2.26.0 notes 2026-10-02 05:11:42 -07:00
esengine 0e0b79b8c5 docs(release): prepare Studio 2.26.0 notes 2026-10-02 05:11:42 -07:00
YHH 0d9792e8b0 Merge pull request #11718 from esengine/fix/studio-stale-default-model
fix(studio): start with a fallback model when the saved default model is not configured
2026-10-02 05:10:47 -07:00
xiexian 1e2b3189b9 fix(pluginpkg): generate expandable numeric server aliases 2026-10-02 20:04:13 +08:00
YHH b33fe75d7e Merge pull request #11712 from esengine/fix/11706-subagent-stuck-running
fix(studio): keep sub-agent progress previews out of the dispatching call's card
2026-10-02 05:01:00 -07:00
xiexian c1006585e5 test(boot): exercise exported MCP recipient headers 2026-10-02 19:56:51 +08:00
esengine d1dddb53f3 fix(studio): start with a fallback model when the saved default model is not configured
The official DeepSeek entry's model list migrates the retired
deepseek-v4-flash name to deepseek-flash, and qualified refs move with it,
but a bare default_model = "deepseek-v4-flash" (the shape a shared 1.x/Studio
config carries) was left behind. It then resolved to nothing, and because an
unnamed boot preserves an unknown default for its error, the Studio host's
only pane failed to build and the host exited 1 before its handshake.

- config: migrate a bare retired flash ref (default_model and every other
  model-ref field) to the official entry's successor, unless another
  provider still serves that name.
- boot: a new OpenOnFallbackModel option, set only by the Studio host and
  hub panes, lets an unnamed build pass over an unknown non-plugin default
  like a keyless one, open on the first configured chat model, and report
  default_model_unavailable. The config file is not written. run, the TUI
  and every other build keep the actionable unknown-model error and never
  move to another provider on their own.
- serve: removing the provider in use moves the conversation to the startup
  model when the saved default names nothing, instead of answering 500
  after the removal; session titles follow the same startup model.
2026-10-02 04:54:09 -07:00
esengine d2a8d0b190 fix(studio): keep sub-agent progress out of the dispatching call's card
A background sub-agent keeps emitting reasonix.subagent.* progress after the
dispatching call has returned its job id. Folding those frames into the call's
card re-opened it, renamed it and its execution, and nothing closed it again.
Progress now lands in per-call phase state beside the cards; the Subagents
panel reads a child as running while its call is open or its last phase is not
terminal.
2026-10-02 04:51:56 -07:00
YHH efd0a4430a Merge pull request #11704 from esengine/docs/readme-alipay
docs(readme): add Alipay to the Support section
2026-10-02 04:46:07 -07:00
YHH b6f7e2e8e8 Merge pull request #11714 from esengine/fix/11708-guidance-pending-chip
fix(studio): count in-flight guidance from the queue the panel draws
2026-10-02 04:37:24 -07:00
YHH 81d1c2e709 Merge pull request #11713 from esengine/fix/11707-queue-takeback-composer
fix(studio): put a taken-back queued line back in the composer
2026-10-02 04:36:23 -07:00
xiexian 3818e93430 fix(repolint): keep quoted paragraphs separate 2026-10-02 19:18:05 +08:00
esengine 6271a7e264 docs(readme): put the Douyin line in its own quote so no paragraph passes the prose width 2026-10-02 04:15:43 -07:00
esengine afb73be003 docs(readme): add Alipay next to WeChat Pay in the Support section 2026-10-02 04:12:28 -07:00
YHH 694d4d79c8 Merge pull request #11701 from esengine/docs/readme-douyin
docs(readme): add the Douyin account to the community links
2026-10-02 04:02:45 -07:00
YHH 77990ed9eb Merge pull request #11660 from esengine/feat/studio-community
feat(studio): add the community group, Discord and contributors to the app
2026-10-02 04:00:17 -07:00
esengine f4766d1369 fix(studio): put a taken-back queued line back in the composer
Take back cancelled the entry and dropped its text, so the label promised a
retrieval nothing performed. The body is now read before the entry is given
up and appended to the composer, under whatever is already typed. A line whose
body cannot be read stays queued rather than being lost.
2026-10-02 03:59:02 -07:00
esengine 7358ceb085 fix(studio): count in-flight guidance from the queue the panel draws
The header chip counted client-side pending rows, which a delivery reported
before its HTTP receipt left pending forever, so it named a line the queue
panel (the only place to act on one) no longer showed. It now counts the
kernel's waiting guidance through the same predicate the panel filters by,
and a receipt that arrives after its delivery no longer re-opens the line.
2026-10-02 03:56:57 -07:00
YHH d65ae94a50 Merge pull request #11682 from esengine/feat/cli-setup
feat(tui): first-run connection panel and /setup (/auth)
2026-10-02 03:42:17 -07:00
esengine 54ef19b12a feat(studio): add a Douyin entry and an author line to About 2026-10-02 03:42:00 -07:00
xiexian 21bb3f3564 test(gitcmd): mark rewritten staging fixture stat dirty 2026-10-02 18:37:33 +08:00
esengine ca96e644b4 docs(readme): add the Douyin account to the community links 2026-10-02 03:25:53 -07:00
esengine 5d7a599ffd feat(studio): add the community group, Discord and contributors to the app 2026-10-02 03:23:14 -07:00
Guan09 5aabf0cfbd fix(checkpoint): clean backups after committed rewinds 2026-10-02 18:06:20 +08:00
YHH c0d4b5a582 Merge pull request #11688 from KHG420/codex/studio-plugin-prompt-invocations
fix(studio): show qualified plugin prompt invocations
2026-10-02 03:04:41 -07:00
xiexian 8990aca7cc test(schedrun): wait for child startup before supervisor crash 2026-10-02 18:03:27 +08:00
YHH a2deb8cf51 Merge pull request #11684 from KHG420/codex/studio-theme-contribution-names
fix(studio): identify directory theme contributions by pack name
2026-10-02 02:54:44 -07:00
esengine 41f3c1e6dd feat(tui): add first-run connection panel and /setup (/auth)
The terminal UI opens a Configure connection panel when the session owes a
key, and /setup or /auth opens it at any time. The key is tested without
being stored, saved to the credential store only, and applied to the running
session when it belongs to the active connection; config.toml is untouched.
2026-10-02 02:54:20 -07:00
YHH ead1e05b4e Merge pull request #11683 from esengine/feat/cli-queue
feat(tui): /queue, /steer, /takeover, /status, /export and /copy
2026-10-02 02:52:49 -07:00
Harbor404 777e32333c Merge remote-tracking branch 'origin/studio' into feat/effort-model-same-switch-fastpath
# Conflicts:
#	internal/frontend/serve/serve.go
2026-10-02 17:48:26 +08:00
YHH c2b55b9824 Merge pull request #11694 from KHG420/codex/studio-native-prompt-author-example
docs(plugins): add native prompt author example
2026-10-02 02:46:36 -07:00
YHH faf26a3c81 Merge pull request #11695 from KHG420/codex/studio-plugin-hook-diagnostics
fix(studio): report plugin hook declaration warnings
2026-10-02 02:45:38 -07:00
YHH 6b9c38c2a0 Merge pull request #11686 from xxoingr/fix/deepseek-h2-preface
fix(provider): hold DeepSeek request bodies for the HTTP/2 server preface
2026-10-02 02:44:40 -07:00
xiexian c81500adfa fix(studio): correct empty plugin list install guidance 2026-10-02 17:29:12 +08:00
YHH 790fdfca4b Merge pull request #11685 from esengine/feat/cli-misc
feat: /cls /rename /todo /forget /reload /effort /verbose /diff-fold /sandbox /output-style /reasoning-language /theme /language /currency
2026-10-02 02:27:01 -07:00
xiexian 7a8dfa5499 fix(studio): report plugin hook declaration warnings 2026-10-02 17:17:59 +08:00
xiexian 4e76141a41 docs(plugins): add native prompt author example 2026-10-02 17:09:22 +08:00
YHH 921e7bdb95 Merge pull request #11691 from esengine/fix/11338-studio-subagent-rows
fix(studio): keep fleet worker tool rows inside their worker card
2026-10-02 02:06:15 -07:00
xiexian 77a30b1c9e fix(studio): show qualified plugin prompt invocations 2026-10-02 17:05:49 +08:00
esengine 6565ce0c85 fix(cli): warn when the project config overrides /language; use slices.Backward 2026-10-02 01:53:09 -07:00
esengine 7a70fa4bf8 feat(tui): add /queue, /steer, /takeover, /status, /export and /copy slash commands
The Studio terminal UI answers the 1.x commands that read or steer the
kernel's own state, with 1.x argument shapes and output. Queue verbs and
steer go through the existing inbox routes, so the controller still owns
admission and ordering. /export is a controller capability: the file is
created exclusively and never through a symlink. /takeover resumes the
target without taking a session from another process.
2026-10-02 01:38:49 -07:00
esengine 86eeae3d6d fix(cli): keep settings verbs workspace-scoped and user-level, route currency through a typed notice 2026-10-02 01:38:10 -07:00
esengine 932736c19e feat(tui): add /cls /todo /verbose /diff-fold /theme and an effect test for the settings verbs 2026-10-02 01:33:51 -07:00
esengine 7c2890aeff feat(control): answer /rename /forget /sandbox /output-style /language /currency /reasoning-language /effort and /reload as slash commands 2026-10-02 01:33:45 -07:00
esengine a5984a25bb fix(desktop): keep a fleet worker's tool calls inside the group card
foldTool looked for a call's parent only among top-level cards, so a
worker's own calls, whose parent is the per-worker task card nested in the
fleet group, fell through to the main list.
2026-10-02 01:32:32 -07:00
YHH b66b2c34ff Merge pull request #11687 from esengine/feat/cli-modes
feat(tui): YOLO on the mode cycle with one Ctrl+Y, git footer row, paste folding
2026-10-02 01:32:27 -07:00
xiexian 1b47d23ab3 fix(studio): diagnose native contribution roots that name files 2026-10-02 16:31:09 +08:00
YHH 51163562f1 Merge pull request #11681 from KHG420/codex/studio-plugin-guide-current-controls
docs(studio): align plugin guide with current controls
2026-10-02 01:28:37 -07:00
esengine 6749731ea3 feat(tui): fold multi-line pastes the way 1.x does
Pastes of five lines or a thousand characters become [Pasted text #N · M lines],
counted whatever row ending the terminal uses, and expand on send into the
labelled Begin/End block 1.x sends. Numbering continues past labels a resumed
session already holds, and panels receive pastes unfolded.
2026-10-02 01:10:30 -07:00
esengine 1ace88e63d feat(tui): show workspace@branch with dirt counts in the footer and name compaction as 1.x does
The footer's data row now leads with workspace@branch and the +added -removed ?untracked
counts from a new GET /workspace/git, read at start and after each turn. The compaction
meter label is back to the bare COMPACT word.
2026-10-02 01:10:30 -07:00
esengine db0c478900 feat(tui): put YOLO on the Shift+Tab cycle and enable it with one Ctrl+Y
The one-time confirmation gate is gone: YOLO is always a stop on the cycle and a
single Ctrl+Y enters it, as in the 1.x terminal UI. --yolo on the command line
keeps its one-time notice, and the sandbox still applies in every mode.
2026-10-02 01:10:30 -07:00
xxoingrandClaude Opus 5.5 18043ab882 fix(provider): hold DeepSeek request bodies for the HTTP/2 server preface
On a fresh HTTP/2 connection, api.deepseek.com's edge acknowledges an eagerly
sent request body (about 8 KB or more) with WINDOW_UPDATE before its own
SETTINGS frame. RFC 9113 §3.4 requires SETTINGS first, so Go's client aborts
with "received WindowUpdateFrame before a SETTINGS frame" and the turn fails
with connection error PROTOCOL_ERROR. Studio sends a ~30 KB body on every
turn, so every request fails on affected networks.

Send Expect: 100-continue on chat requests to the official DeepSeek host.
The transport then holds the body until the server answers, by which point
its preface has arrived. HTTP/2 stays negotiated and request bytes are
unchanged; other providers are unaffected.

Moving the header setup into chat_headers.go keeps openai.go within its
file-size budget.

Refs #11679

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 16:07:58 +08:00
xiexian 9c435af496 fix(studio): identify directory theme contributions by pack name 2026-10-02 16:05:58 +08:00
YHH 7c03770b1a Merge pull request #11674 from KHG420/codex/studio-plugin-theme-diagnostics
fix(studio): report plugin theme validation warnings
2026-10-02 00:41:28 -07:00
YHH 78c884b1f2 Merge pull request #11677 from KHG420/codex/studio-theme-glob-file-siblings
fix(plugins): skip file siblings in nested theme globs
2026-10-02 00:40:30 -07:00
xiexian 0f48be263d docs(studio): align plugin guide with current controls 2026-10-02 15:37:32 +08:00
YHH 0bced53fe4 Merge pull request #11670 from KHG420/codex/studio-plugin-replace-recovery-hint
fix(plugins): offer replacement in duplicate recovery hint
2026-10-02 00:29:37 -07:00
YHH b52bb7d4c3 Merge pull request #11676 from esengine/docs/readme-tagline
docs(readme): align the tagline with the repository description
2026-10-02 00:26:31 -07:00
xiexian b69954c912 fix(plugins): skip file siblings in nested theme globs 2026-10-02 15:19:17 +08:00
YHH 754dee6725 Merge pull request #11671 from KHG420/codex/studio-lease-test-distinct-pid
test(control): keep lease holder PID distinct from test process
2026-10-02 00:18:12 -07:00
esengine d03684b080 docs(readme): align the tagline with the repository description 2026-10-02 00:08:48 -07:00
xiexian b0578fca34 fix(studio): report plugin theme validation warnings 2026-10-02 15:03:19 +08:00
YHH 8f9c7806c6 Merge pull request #11664 from KHG420/codex/studio-local-link-path-alias
fix: recognize directory aliases in local link installs
2026-10-01 23:41:23 -07:00
xiexian 23f44c5171 test(control): keep lease holder PID distinct from test process 2026-10-02 14:38:59 +08:00
YHH 28383e9f92 Merge pull request #11663 from KHG420/codex/studio-subagent-author-example
docs: add a packaged read-only subagent exercise
2026-10-01 23:33:54 -07:00
YHH 0a4d0493ee Merge pull request #11658 from KHG420/codex/studio-plugin-agent-inventory
fix(plugin): show packaged subagents in capability inventory
2026-10-01 23:32:57 -07:00
YHH 765bdfc872 Merge pull request #11668 from KHG420/codex/studio-linked-plugin-remove
fix: clean up managed links when removing plugin packages
2026-10-01 23:32:05 -07:00
xiexian 13247c20e3 fix(plugins): offer replacement in duplicate recovery hint 2026-10-02 14:22:11 +08:00
xiexian b30ff93cac fix: recognize directory aliases in local link installs 2026-10-02 13:49:52 +08:00
xiexian e309ee2200 docs: add a packaged read-only subagent exercise 2026-10-02 13:39:32 +08:00
xiexian 782070a0df fix: clean up managed links when removing plugin packages 2026-10-02 13:32:25 +08:00
Harbor404 158d9e2b18 feat(studio): copy session handoff information 2026-10-02 13:31:01 +08:00
Harbor404 49dba2381a feat(usage): add calendar ranges and precise daily details 2026-10-02 13:22:33 +08:00
YHH 9a4d3dfaa7 Merge pull request #11657 from esengine/docs/community-links
docs: point the community links at the official QQ group and Discord
2026-10-01 22:08:19 -07:00
YHH 852b5e65aa Merge pull request #11655 from KHG420/codex/studio-own-install-result-retry
fix(studio): let authors re-preview failed package installs
2026-10-01 21:56:19 -07:00
xiexian c9f698352f fix(studio): recover failed plugin installs through fresh previews 2026-10-02 12:54:12 +08:00
xiexian 9393e594f2 fix(plugin): show packaged subagents in capability inventory 2026-10-02 12:44:36 +08:00
esengine 871687fee7 docs: point the community links at the official QQ group and Discord 2026-10-01 21:34:32 -07:00
xiexian b12683f48b fix(studio): let authors re-preview failed package installs 2026-10-02 12:27:00 +08:00
YHH 88ca5ae82b Merge pull request #11648 from KHG420/codex/studio-plugin-remove-help
fix(cli): show confirmation flag in plugin removal help
2026-10-01 21:13:27 -07:00
YHH a6f744d414 Merge pull request #11649 from KHG420/codex/studio-skill-invocation-catalog
fix(studio): honor skill invocation flags in capability catalog
2026-10-01 21:11:33 -07:00
YHH 580339980f Merge pull request #11646 from KHG420/codex/studio-scenario-skill-frontmatter
fix(studio): keep scenario skill frontmatter valid YAML
2026-10-01 20:45:18 -07:00
xiexian daec47c72a fix(studio): honor skill invocation flags in capability catalog 2026-10-02 11:36:06 +08:00
xiexian 1eb7516562 fix(cli): show confirmation flag in plugin removal help 2026-10-02 11:25:19 +08:00
xiexian f392dd2fa7 fix(studio): keep scenario skill frontmatter valid YAML 2026-10-02 11:13:03 +08:00
YHH c0a7ebe10d Merge pull request #11640 from esengine/fix/ask-stepper-back
fix(studio): keep the ask dialog on the next question after going back
2026-10-01 19:22:52 -07:00
esengine 63da41c794 fix(studio): keep the ask dialog on the next question after going back
Next used the first still-open question, so after going back it skipped answered questions, and with two questions it vanished in favour of Confirm. Stepping is now positional; only the last question looks for an open one.
2026-10-01 19:14:20 -07:00
YHH 1651a909f0 Merge pull request #11637 from esengine/fix/touch-enter-lint
fix(studio): keep Enter newline on touch-first devices (continues #11620)
2026-10-01 17:46:45 -07:00
YHH 8ea3bec3e2 Merge pull request #11635 from esengine/ci/windows-boot-timeout
ci(windows): give the Windows package budget 12m now that boot runs past 8m
2026-10-01 16:39:54 -07:00
esengine eba925da41 ci(windows): give the Windows package budget 12m now that boot runs past 8m 2026-10-01 16:29:29 -07:00
YHH d85fe68a17 Merge pull request #11634 from esengine/docs/studio-2.25.0-notes-2
docs(release): add the fixes merged since the first 2.25.0 notes
2026-10-01 15:05:19 -07:00
esengine 73aca5b612 docs(release): add the fixes merged since the first 2.25.0 notes 2026-10-01 14:57:20 -07:00
YHH 195ee6dcf3 Merge pull request #11590 from KHG420/codex/studio-context-hook-example
docs(studio): add a session-start context author example
2026-10-01 14:37:23 -07:00
esengine a7e6771ae5 fix(studio): keep the composer under the file-size ceiling and cover the touch-device send paths 2026-10-01 14:28:17 -07:00
YHH 0c88b9d0b4 Merge pull request #11632 from esengine/fix/footer-text-clip
fix(studio): stop single-line labels from clipping glyphs of tall fonts
2026-10-01 13:25:26 -07:00
esengine e8ac15d52a fix(studio): stop single-line labels from clipping glyphs of tall fonts
The workspace label in the sidebar footer, the composer's model name and the
workbench file name paired overflow clipping with line-height 1, so a UI font
whose ascent and descent exceed 1em lost the tops and bottoms of its glyphs.
They now use line-height 1.5; the rows centre their children, so nothing moves.
A style contract fails any new rule that clips a line tighter than 1.2.
2026-10-01 13:16:53 -07:00
YHH e4dd0ad1b4 Merge pull request #11629 from esengine/fix/meterrail-tabular-nums
fix(studio): keep the status rail from jumping as its numbers change
2026-10-01 12:20:14 -07:00
esengine 404af94b9e fix(studio): keep the status rail from jumping as its numbers change
The rail items re-declare the font shorthand, which resets
font-variant-numeric, so the digits were proportional and each item changed
width with its value. Restate tabular-nums in those rules and reserve the
width of a three-digit speed.
2026-10-01 12:10:06 -07:00
YHH 8de4c91010 Merge pull request #11624 from esengine/feat/remote-bootstrap-nvm
fix(remote): run bootstrap commands with the remote login-shell PATH
2026-10-01 11:25:20 -07:00
esengine b94c4aa944 fix(remote): run bootstrap commands with the remote login-shell PATH 2026-10-01 11:12:01 -07:00
YHH ef1a339ca9 Merge pull request #11613 from Harbor404/fix/studio-11574-browser-workspace
fix(studio): let browser deployments add workspaces by path
2026-10-01 11:08:02 -07:00
YHH 6d679edf67 Merge pull request #11614 from Harbor404/fix/studio-11586-provider-name-ime
fix(studio): hold Add while a source name is composing
2026-10-01 11:03:23 -07:00
xiexian 29c033cfd5 docs(studio): add session-start context author example 2026-10-02 01:28:24 +08:00
YHH 3eda0b98e4 Merge pull request #11618 from esengine/feat/feedback-unavailable-retry
fix(feedback): retry a transient submit response once and say what to do next
2026-10-01 10:02:38 -07:00
Harbor404 b5485b1649 fix(studio): keep Enter newline on touch-first devices 2026-10-02 01:00:05 +08:00
esengine 5e1ea240fb fix(feedback): retry transient submit responses once
A submit that gets an answer without a recognised service code (an uncoded
5xx or an undecodable success body) is retried once with the existing
backoff, keeping the idempotency key. Coded refusals, refused redirects and
replies are never retried. The failure message now says the typed text is
kept, to try again shortly, and points at the GitHub alternative.
2026-10-01 09:41:30 -07:00
Harbor404 c046c5281f fix(studio): keep add disabled during source-name IME
The source-name field intentionally hides validity errors while an IME candidate is still composing. The Add button did not share that state, so a completed form could submit a partial non-ASCII candidate before composition ended. Hold the button while composing and cover the name boundaries at the UI and serve boundary.
2026-10-01 22:45:16 +08:00
Harbor404 7ad19b82a5 fix(studio): add workspaces from a browser
Probe host capabilities before opening a native picker. Headless and remote browser clients now enter a server-side path and use the existing /tree/workspaces API, while the desktop shell keeps its native dialog.
2026-10-01 22:44:28 +08:00
YHH 7b34dd738d Merge pull request #11610 from Harbor404/fix/studio-11581-legacy-recovery-archive
fix(serve): archive a legacy recovery lineage atomically
2026-10-01 07:42:24 -07:00
YHH d77138966a Merge pull request #11611 from KHG420/codex/studio-mcp-result-retry
fix(studio): retain retry for returned MCP install failures
2026-10-01 07:40:24 -07:00
xiexian 4642a4cbee fix(studio): retain retry for returned MCP install failures 2026-10-01 22:18:28 +08:00
YHH 2df63d9f9a Merge pull request #11609 from KHG420/codex/studio-install-failure-reload
fix(studio): preserve install failures alongside reload errors
2026-10-01 07:17:26 -07:00
Harbor404 99e466f2cc fix(sessionstore): archive recovery lineage atomically
Archive a folded legacy recovery row together with its siblings, keep siblings open in a pane active, and restore sidecars if a multi-file update fails. Refs #11581.
2026-10-01 22:09:57 +08:00
YHH c145598d47 Merge pull request #11599 from KHG420/codex/studio-export-executable
fix(studio): keep exported plugin hooks executable after reinstall
2026-10-01 07:08:00 -07:00
xiexian 5ff8944e9f fix(studio): preserve install failures alongside reload errors 2026-10-01 22:01:39 +08:00
YHH e080b2b972 Merge pull request #11604 from KHG420/codex/studio-install-reload-reason
fix(studio): preserve installation reload failure reasons
2026-10-01 06:55:37 -07:00
YHH 614f9d3831 Merge pull request #11595 from KHG420/codex/studio-my-packages-account
fix(studio): scope My packages to the current account
2026-10-01 06:42:18 -07:00
YHH 9d53a3aad7 Merge pull request #11602 from KHG420/codex/studio-remove-reload-result
fix(studio): retain reload failures after package removal
2026-10-01 06:41:23 -07:00
YHH 8d306bd0e6 Merge pull request #11596 from no-name-touse/fix/onboarding-zoom
fix(studio): make onboarding layout follow zoom-adjusted width
2026-10-01 06:33:27 -07:00
YHH a9cae776f5 Merge pull request #11601 from esengine/fix/context-budget-notice-continues-task
fix(agent): tell the model compaction is automatic and the task continues
2026-10-01 06:32:36 -07:00
xiexian 13c1820de5 fix(studio): preserve installation reload failure reasons 2026-10-01 21:16:34 +08:00
xiexian 5d33e047f4 fix(studio): retain reload failures after package removal 2026-10-01 21:06:03 +08:00
esengine ce860db141 fix(agent): tell the model compaction is automatic and the task continues
The context-budget notice reported "room remaining" and, at the second
rung, told the model to land what it knows now. It never said that the
fold is automatic and the same task goes on, so a model near the trigger
read the figure as the end of its window, announced it would be
compacted, and handed the work back to the user (in-app feedback
FB-J2QR-HQ89).

Both rungs now state that the figure counts to the automatic compaction,
not to the end of the window, and carry one shared continuation clause:
keep working, do not stop, hand the work back or suggest a new session.
The notice still rides the turn tail and the typed ContextBudgetFigures
event is unchanged.

Two fixtures sat within a few tokens of the notice size and are widened.
2026-10-01 06:03:44 -07:00
YHH 688658585b Merge pull request #11600 from esengine/docs/studio-2.25.0-notes
docs(release): prepare Studio 2.25.0 notes
2026-10-01 06:00:43 -07:00
Harbor404 ac0a13dbdd Merge remote-tracking branch 'origin/studio' into feat/effort-model-same-switch-fastpath 2026-10-01 20:54:13 +08:00
Harbor404 7f9cb9dbdc fix(acp): remove unused session config helper 2026-10-01 20:54:11 +08:00
esengine eb6ba928be docs(release): prepare Studio 2.25.0 notes 2026-10-01 05:51:27 -07:00
Harbor404 b47f9144a8 Merge remote-tracking branch 'origin/studio' into ci-reasonix-11504 2026-10-01 20:49:30 +08:00
Harbor404 82a06451dd fix(shell): reject a Store alias that will not start 2026-10-01 20:49:24 +08:00
YHH ac7836f048 Merge pull request #11592 from KHG420/codex/studio-review-refresh-order
fix(studio): keep review results after concurrent install refresh
2026-10-01 05:45:51 -07:00
Harbor404 b1f7ba5dd8 Merge remote-tracking branch 'origin/studio' into ci-reasonix-11504 2026-10-01 20:45:24 +08:00
YHH ac7d7be97d Merge pull request #11588 from Guan0923/codex/session-title-context-menu
fix(studio): 右键打开会话菜单并调整版本角标间距
2026-10-01 05:45:01 -07:00
Harbor404 c33429c731 Merge remote-tracking branch 'origin/studio' into fix/remote-open-absolute-workspace
# Conflicts:
#	internal/platform/remote/bootstrap/resolve_workspace_test.go
2026-10-01 20:44:49 +08:00
Harbor404 1e3bb12e68 Merge remote-tracking branch 'origin/studio' into fix/disabled-tools-followups 2026-10-01 20:42:20 +08:00
Harbor404 29d06f6064 fix(mcp): address disabled tool policy review 2026-10-01 20:41:43 +08:00
xiexian a2e20b8a2f fix(studio): preserve executable bits in exported plugins 2026-10-01 20:34:59 +08:00
YHH 0be8aaf783 Merge pull request #11488 from esengine/fix/host-session-sweep-off-boot-path
fix(serve): sweep session history in the background, not inside the build
2026-10-01 05:25:29 -07:00
YHH 1c12f7c174 Merge pull request #11563 from esengine/fix/inbox-skip-requeues
fix(inbox): a skipped question requeues the guidance queued into its turn
2026-10-01 05:15:47 -07:00
esengine 07b2453403 fix(serve): sweep session history in the background instead of inside the build
Every runtime build ran ReconcileCleanupPending inline: a handful of stat and
read calls per transcript in the session directory. On a Windows box with 8000
sessions the sweep took 11 s of an 11.8 s build and 29 s at 20000 sessions
(about 1.4 ms per session, warm), and it sits before the host handshake, so a
cold start with a large history waits on it. The shell gave up at 20 s.

The Studio host, hub.Open, workspace switch and rebuild paths now hand boot a
reconciler that runs the sweep in a goroutine, once per directory at a time.
8000 sessions: build 11.8 s -> 1.0 s.

Refs #11434
2026-10-01 05:14:42 -07:00
xiexian b619b3dbfe fix(studio): scope author package views to the current account 2026-10-01 20:14:15 +08:00
YHH 4cdd4bde93 Merge pull request #11541 from esengine/fix/remote-open-absolute-workspace
fix(remote): a Windows absolute workspace is not joined after the remote home
2026-10-01 05:09:56 -07:00
挽风 8ac91c3399 fix(studio): make onboarding layout follow zoom-adjusted width 2026-10-01 20:05:59 +08:00
esengine 9fc7f5022c fix(inbox): a skipped question requeues the guidance queued into its turn
Skipping an ask ends the turn through the same cancel as the stop button, so
guidance the user had queued into it was held uncertain and the queue paused.
The cancel now carries a typed cause: only the user declining a question
returns the undelivered item to the queue as an ordinary follow-up and lets
dispatch continue. Stop, errors and crashes keep the pause, and a stop that
lands after a skip still wins. The requeue only moves an accepted steer, so a
repeat delivers nothing twice.

Refs #11524
2026-10-01 05:03:36 -07:00
xiexian 7d6687d03b fix(studio): refresh inventory after review submission 2026-10-01 19:53:38 +08:00
YHH af710d8784 Merge pull request #11585 from esengine/fix/add-source-name-inline
fix(studio): show the source name rule beside the field, not as a connection failure
2026-10-01 04:46:51 -07:00
Guan09 723b2b8ef2 fix(studio): 右键打开会话菜单并调整版本角标间距 2026-10-01 19:35:36 +08:00
esengine 3afb4dea49 fix(studio): explain an unusable source name beside the field, not as a connection failure
The add-source form only learned that a name such as a Chinese one is not
allowed after the kernel refused the save, and then showed the refusal under
the title "cannot connect". The name rule now shows inline as the name is
typed (with the allowed characters stated), the add button waits for a usable
name, coded name refusals from the kernel land on the field, and any other
failed save is titled as a save failure. The identifier rule is unchanged.

Reported through in-app feedback FB-WJ8B-C1XH.
2026-10-01 04:34:52 -07:00
YHH 79b0547997 Merge pull request #11533 from esengine/feat/project-reveal-menu
feat(desktop): show a project's folder in the file manager from the sidebar menu
2026-10-01 04:25:14 -07:00
xiexian 3f35e37388 feat(examples): add a runnable stdio MCP plugin 2026-10-01 19:14:56 +08:00
YHH e6d0d4aebe Merge pull request #11545 from esengine/fix/tray-icon-blurry
fix(studio): sharpen the Windows tray icon at scaled displays
2026-10-01 04:13:59 -07:00
esengine 7b7e09fdcd fix(serve): touch the disk only with the window's own listed root
Refs #11524
2026-10-01 04:13:47 -07:00
xiexian 4d9ba18ae0 fix(studio): serialize own-package review submissions 2026-10-01 18:44:17 +08:00
YHH 631bd6b111 Merge pull request #11419 from BuGlessRB/open/diff-fence-stream-perf
fix(cli): colour a streaming diff fence hunk by hunk / 修复(cli):流式 diff 围栏逐 hunk 着色
2026-10-01 02:57:08 -07:00
YHH 7ac760e44d Merge pull request #11558 from KHG420/codex/studio-market-vote-retry
fix(studio): retry failed market vote lookups
2026-10-01 02:56:13 -07:00
YHH 9dcfdf2e7c Merge pull request #11565 from esengine/fix/onboarding-scroll
fix(studio): make the first-run connect card scroll in short windows
2026-10-01 02:35:06 -07:00
esengine cac890152a feat(share): let the phone-access LAN link use a fixed port
An optional user-level [serve] share_port makes the direct link listen on a
chosen port instead of a random one. Unset keeps today's behaviour. The range is
1024-65535, a project reasonix.toml cannot set it, and the bind range is
unchanged. A taken port is a typed refusal (share.port_in_use). The settings
page saves it to the user config; changing it moves an open door and
phones re-pair.

Refs #11540
2026-10-01 02:31:52 -07:00
YHH f86eb89b74 Merge pull request #11559 from esengine/fix/cancel-flag-follow-up
fix(control): flag a turn cancelled only when it ended on the cancel sentinel
2026-10-01 02:30:14 -07:00
esengine 6292d66e90 fix(studio): make the first-run connect card scroll in short windows
The onboarding stage only had a min-height, so it grew with its content and
never overflowed itself while the body clipped it. Once the connection result
was showing, the Start button sat below the window with nothing to scroll.
The stage is now the window's height and scrolls; the shell no longer shrinks
or acts as a scroll container. Adds a browser guard (perf/onboarding.mjs).
2026-10-01 02:24:17 -07:00
YHH 3bb9dce90a Merge pull request #11562 from esengine/docs/hooks-guide
docs: add a Hooks section to the guide and reasonix-guide skill
2026-10-01 02:23:54 -07:00
Harbor404 ef8c8cbfb0 fix(studio): refresh provider build identity on same selection 2026-10-01 17:20:04 +08:00
Harbor404 a0d1380335 Merge origin/studio into feat/effort-model-same-switch-fastpath 2026-10-01 17:20:03 +08:00
YHH 5d3a70950c Merge pull request #11552 from esengine/fix/11524-notices-localized
fix(notices): word the context-budget and unapplied-guidance notices from typed codes
2026-10-01 02:19:11 -07:00
esengine a27298c462 docs: document hooks (events, settings.json schema, exit codes, scope)
A model told a user that Reasonix has no hooks. The guide's hooks section only
covered SessionStart, and the bundled reasonix-guide skill had no settings
schema, exit-code convention or worked example.
2026-10-01 02:14:37 -07:00
YHH f84126a76f Merge pull request #11555 from esengine/fix/inbox-block-code
fix(inbox): carry why a queue item stopped as a typed code the desktop words itself
2026-10-01 02:09:05 -07:00
Stephen R. van den Berg e8c7feeeef fix(cli): colour a streaming diff fence hunk by hunk
A ```diff fence rendered through the colourised built-in path on every
frame, and a colourised row costs ~1.4 ms to lay out (chroma via
regexp2) against ~1.3 µs for a plain one. While a fence streams every
delta re-renders the whole growing body, so a multifile diff pegged a
core and the display crawled. Changes:

- Each "@@ " hunk header settles the rows before it, so render that
  prefix and leave only the in-progress hunk on the plain rail: the diff
  colours in as it streams instead of only at the closing fence. A
  closed fence settles the whole body. The formatter runs on each
  settled prefix too, so its output appears hunk by hunk.

- Keep the already-formatted rows on screen while a settled body's run
  is pending: growing the body by one hunk changed its key and redrew
  the whole section on the plain rail until the new run landed, so the
  delta output flashed back to plain at every hunk. The renderer now
  draws the largest settled prefix whose result has landed and leaves
  only the newly settled remainder (and the in-progress hunk) on the
  rail.

- Memoize the colourised rows per hunk (hunk, path, width, rail, theme).
  A completed hunk never changes, so it is highlighted once however many
  frames redraw it; caching the whole body instead would re-highlight
  every earlier hunk at each "@@ " and is quadratic in the hunk count
  (measured: 60 hunks, 79 KB took 40 s with a body memo, 2 s per-hunk).
  The theme is in the key so a runtime theme switch cannot serve the old
  palette's colours.

- A fence still with fewer than two hunks keeps the plain rail, and a
  closed fence whose configured [cli].diff_formatter result is still
  pending also draws the rail, since that output supersedes the built-in
  rows anyway; a landed-but-failed run falls back to the built-in.

- Hold the transcript viewport still when the content shrinks: a settled
  diff collapsing its raw preamble to one formatted line frees rows at
  the bottom, and following them back up made the screen jump. The
  position now holds, the freed rows stay blank, and later rows fill
  them, so scrolling resumes only when the output actually grows past
  what is shown. The hold has only a lower bound, so a shrink larger
  than the viewport would hold the position past the content and blank
  the whole transcript — a collapsing multifile fence, or /clear
  restarting over a shorter one — so the view re-anchors to its tail
  instead. A manual scroll or resize releases the hold.

- Coalesce the stream frames the TUI hands the model: waitUpdate drains
  everything already queued into one updateMsg, so a burst of deltas
  costs one render instead of one per delta. Order is kept and a Gap
  still triggers a history reload.

- Fix the file-header misdetection: a removed SQL "-- x" line renders
  "--- x", which trimDiffPreamble/diffSectionStart mistook for a
  "--- "/"+++ " file header, dropping the "@@" hunk header (line numbers
  reset to 0) or the rows themselves when a removed "--- x" sat before
  an added "+++ y". A pair is a file header only when a "@@ " hunk
  follows it, and countDiff no longer skips "--- "/"+++ " rows.

- Keep a streaming multi-file diff's next file header on the plain rail:
  splitAtLastHunk settled everything before the last "@@ " header, which
  included the next file's "--- "/"+++ " pair (and its git preamble) once
  that file's first hunk header had arrived but no hunk of it had
  completed. isFileHeaderPair then saw no "@@ " after the pair, so the
  settled renderer read it as a removed/added row of the previous file's
  last hunk and counted it in that file's stat. A trailing dangling file
  header now moves from the settled part into the tail until its "@@ "
  arrives.

Tests: the multi-file cases TestSplitAtLastHunk was missing and an
end-to-end streamed test; a theme-switch test pinning the per-hunk memo's
theme key; and a benchmark replaying a 4-file x 15-hunk stream, redrawn
per streamed line.

fix(cli): highlight a streaming diff fence's hunk once

splitDiffSections appends a newline to every section, so splitHunks
returns a section's last hunk with a trailing newline and every earlier
hunk without. hunkRowsCached keyed the memo on the raw hunk, so one hunk
carried two keys over a stream: it was highlighted once while it was its
section's last hunk and again once a following hunk arrived. Every hunk
but a file's last was therefore highlighted twice, at ~1.4 ms a row.

Trim the trailing newline before keying. diffBody already trims, so the
rendered rows are unchanged; the streaming benchmark over a 4-file,
30-hunk, 1816-line diff drops from 7.8 s to 5.6 s.

TestStreamingDiffHunkMemoisedOnce pins one memo entry per hunk: it sees
three entries for two hunks before this change, two after.

Fold the writer-tool preview before highlighting it too. diffBody laid
out every row and then dropped all but maxLines, so a write_file card's
~24-row preview colourised the whole diff: ~772 ms per repaint for a
199-line file, paid on every message while the call runs. Counting the
fold from the source and laying out only the kept rows prices a
2000-line diff like a 24-line one.

BenchmarkDiffBlockPreview prices both shapes through a diffPreviewFold
seam, which TestDiffBodyFoldSeamSameRows proves draws the same rows: a
2000-line preview is 6.3 s and 86 MB with the whole body laid out, 71 ms
and 1.0 MB folded, and flat from 24 lines up.

docs(cli): correct diff-fence comment coherence

Four comments described behaviour the code does not have:

- diffRowCount's doc counted one row per non-blank line, but diffBody draws no
  row for a "@@ " header, so it over-counted by the hunk count.
- renderCodeRail's doc claimed the in-progress hunk and the pending-formatter
  placeholder, both drawn by renderRailText, not here.
- splitDiffSections' doc named only the fence path, omitting DiffText, and
  described the plain-rail fallback where DiffText drops a headerless section.
- the file header claimed md.go still sits at the 800-line ceiling.

Reword each to the constraint that holds now.
2026-10-01 11:01:45 +02:00
esengine f836dc6a3a fix(inbox): carry why a queue item stopped as a typed code the desktop words itself
A held queue item showed the kernel's English diagnostic (for example
"steer accepted but unapplied before turn exit") in a Chinese UI. Items now
also carry a stable BlockCode and the desktop words it in the UI language;
unknown codes fall back to the diagnostic.
2026-10-01 01:56:28 -07:00
YHH 69cb060066 Merge pull request #11548 from esengine/fix/storage-move-keeps-project-list
fix(storage): carry the remembered project list when the state root moves
2026-10-01 01:48:11 -07:00
esengine 6328dfa0b5 fix(control): flag a turn cancelled only when it ended on the cancel sentinel
A real failure that raced the stop button was reported as a cancellation and
hidden. TurnDone.Cancelled now requires Err to be nil or context.Canceled. The
desktop also reads "Cancelled" instead of "Interrupted" after the user's own
cancel.

Refs #11524
2026-10-01 01:43:02 -07:00
xiexian 9bb0aa28fd fix(studio): retry failed market vote lookups 2026-10-01 16:34:22 +08:00
YHH 3a754cebdb Merge pull request #11547 from KHG420/codex/studio-skill-author-invocation-comment
docs(skill): clarify authoring invocation policy
2026-10-01 01:34:04 -07:00
esengine 6694a1b81a fix(storage): carry the state root's loose entries when it moves
Settings > Storage moved sessions and projects but left other top-level state
behind while the code reads only the new root: the remembered project list
(serve-workspaces.json), user-level REASONIX/AGENTS/CLAUDE instructions,
memory/global, schedules, trusted seals, machine-id.key and the MCP migration
marker. All are now owned entries, so a move carries them.

Installs already moved get them back at boot: each entry is merged once without
overwriting and the old copy is renamed *.adopted. The project list is merged
under its lock and written atomically by a new leaf package shared with serve,
and file copies publish whole files. A guard test scans the tree for paths
joined under the state root so a future entry cannot be forgotten.
2026-10-01 01:33:31 -07:00
YHH 003000be21 Merge pull request #11532 from KHG420/codex/studio-publish-draft-lock
fix(studio): keep the publishing draft stable while submitting
2026-10-01 01:29:07 -07:00
Harbor404 fceb6b8ab9 Merge latest studio into UNC round-trip fix 2026-10-01 16:23:00 +08:00
YHH 92a1677fda Merge pull request #11551 from esengine/fix/11524-cancel-not-error
fix(desktop): do not draw a user-cancelled turn as an error card
2026-10-01 01:22:05 -07:00
Harbor404 0a5e45417d Merge latest studio into UNC round-trip fix 2026-10-01 16:21:05 +08:00
esengine e4f1f80a76 fix(notices): word the context-budget and unapplied-guidance notices from typed codes
Both host notices reached every frontend as kernel English, so a Chinese UI
showed an English card. The context-budget notice now carries a code and a JSON
figures payload, the unapplied-guidance notice carries the user's text as
Detail, and the desktop card and the TUI word them from those in the UI
language. Unknown codes and undecodable payloads still fall back to the
kernel's text.

Refs #11524
2026-10-01 01:20:40 -07:00
xiexian 552209fa46 docs(sdk): add an installable tool extension example 2026-10-01 16:16:56 +08:00
xiexian 8fdfb89127 docs(sdk): align fullsidecar demo guidance 2026-10-01 16:11:25 +08:00
YHH 9ced3d1577 Merge pull request #11542 from KHG420/codex/studio-fullsidecar-host-proof
docs(sdk): add fullsidecar installation and host checks
2026-10-01 01:04:54 -07:00
Harbor404 d6e0505dc6 chore(remote): narrow this PR to the UNC round trip 2026-10-01 16:02:51 +08:00
Harbor404 42f42d2d19 fix(acp): keep queued config order on no-op switches 2026-10-01 16:02:04 +08:00
Harbor404 e2b6e8fd31 Merge remote PR history into rebased branch 2026-10-01 15:57:36 +08:00
Harbor404 fe4b15b2f6 fix(remote): open absolute Windows workspaces from tree 2026-10-01 15:57:23 +08:00
esengine 5caa03eca1 fix(desktop): do not draw a user-cancelled turn as an error card
Skipping an ask (or stopping a turn) ends it with the kernel's Cancelled flag
set and context.Canceled as Err. The transcript appended Err as an error card
regardless of the flag, so the user's own choice read as "context canceled".
The flag is the identity; Err is not drawn when it is set. An effect test pins
the kernel side through boot.Build.

Refs #11524
2026-10-01 00:45:03 -07:00
xiexian 309ea9aa0f docs(skill): clarify authoring invocation policy 2026-10-01 15:42:19 +08:00
Harbor404 dd73c2159c perf(studio): skip rebuilds for unchanged model or effort 2026-10-01 15:37:50 +08:00
xiexian 15155c3a67 fix(studio): keep the publishing draft stable while submitting 2026-10-01 15:36:14 +08:00
esengine 7f7a063139 fix(studio): give the Windows tray icon a file at exactly the size Windows asks for 2026-10-01 00:36:08 -07:00
YHH 8c608c8bba Merge pull request #11523 from KHG420/codex/studio-skill-dependency-guide
docs(skills): explain and verify MCP dependency readiness
2026-10-01 00:33:50 -07:00
YHH e9a863c36d Merge pull request #11304 from Guan0923/codex/user-message-copy
feat(studio): 重设计用户消息操作栏并新增复制功能
2026-10-01 00:30:47 -07:00
YHH 0d30c68641 Merge pull request #11528 from KHG420/codex/studio-source-disabled-style
fix(studio): distinguish the busy plugin source without fading text
2026-10-01 00:27:32 -07:00
YHH dc8f746f5e Merge pull request #11537 from KHG420/codex/studio-skill-invocation-comment
docs(skills): distinguish manual listing from model invocation
2026-10-01 00:26:38 -07:00
Harbor404 8f5768e118 fix(remote): open absolute Windows workspaces from tree 2026-10-01 15:26:08 +08:00
Harbor404 277f5614a8 Merge remote-tracking branch 'origin/studio' into ci-reasonix-11504 2026-10-01 15:25:45 +08:00
YHH f6d948c60b Merge pull request #11522 from KHG420/codex/studio-export-result-lifecycle
fix(studio): keep package export feedback current and visible
2026-10-01 00:25:07 -07:00
esengine b2f36909eb fix(remote): a Windows absolute workspace is not joined after the remote home
resolveWorkspace only knew a leading slash as rooted, so a drive-letter path such as C:\Users\u\x (what the remote tree reports as a project root) was appended to home and the serve never started. Rootedness is now asked of the target machine.
2026-10-01 00:24:00 -07:00
Harbor404 12e6f9d3ae Merge fork/fix/powershell-shell-probe after studio rebase 2026-10-01 15:20:35 +08:00
xiexian b5f2f0c025 test(extensions): verify installed fullsidecar host effects 2026-10-01 15:14:53 +08:00
Harbor404 439e868ed3 style(boot): trim shell diagnostic comment 2026-10-01 15:12:11 +08:00
Harbor404 de5ba24bcc fix(boot): log the resolved shell at debug level
Headless `run` must leave stderr empty unless --debug is passed, so the
boot record of the resolved shell interpreter cannot be an Info log. It is a
diagnostic; the always-on counterpart is the Warn shellrun emits when a
launch actually fails.
2026-10-01 15:12:11 +08:00
Harbor404 ac2db59a33 fix(shell): keep the PowerShell probe within lint budgets
Trim repolint comment blocks and move launch-failure logging out of RunForeground so the function stays within the 120-line limit. No behavior change.

Signed-off-by: Harbor404 <[email protected]>
2026-10-01 15:12:11 +08:00
Harbor404 bac803e68c fix(shell): only trust a PowerShell that starts, and record which ran
A pinned [tools.shell] path was honoured on existence alone, so a pwsh.exe
that stats fine but will not start was held for the whole session while every
command failed at launch — the failure #10633 reported, still reachable
through the pinned-path branch after #10617 reworked auto-detection.

- Prove a pinned PowerShell the way the bash arm already proves its own, and
  fall back to detection with a warning when it will not start.
- VerifyShell proves a pinned PowerShell too, so the settings screen refuses
  one where it was typed rather than on every later command.
- powerShellLaunches answers false for a path that is not there: a missing
  executable is not launchable, so callers that gate on it alone now agree.
- resolveShell takes the launch probe as an argument, which puts the decision
  table ("pwsh will not start, 5.1 will") under test on any host.
- Detection that finds nothing says so instead of silently returning bare bash.
- Boot records the interpreter the session resolved to, and a launch-phase
  spawn failure records the shell, its path, the caller and the error. The
  desktop relays the sidecar's stderr into desktop-shell/logs/, which the
  report found empty.

Windows end-to-end is not verified locally; the fallback is covered by the
injected probe on Linux and macOS.
2026-10-01 15:12:11 +08:00
YHH c9daeddf3e Merge pull request #11530 from esengine/fix/editmenu-locale
fix(electron): label the edit menus in the interface language
2026-09-30 23:52:52 -07:00
xiexian 8dd9294495 docs(skills): distinguish manual listing from model invocation 2026-10-01 14:48:59 +08:00
esengine f2dc940a29 test(desktop): cover the host-only route, remote projects and old shells
Refs #11524
2026-09-30 23:47:02 -07:00
xiexian 9d930f67f4 docs(skills): explain and verify MCP dependency readiness 2026-10-01 14:45:56 +08:00
esengine c0766fbe59 fix(electron): keep every platform Edit menu item, test the rebuild rule
The hand-written Edit menu dropped Paste and Match Style, Delete,
Substitutions and Speech; restore them with the same roles (structure checked
against the role-based menu in a real Electron). The menu factory is now
Electron-free and unit-tested, the language follows the first preferred system
language like the page, and the redundant second menu install is gone.

Refs #11524
2026-09-30 23:45:39 -07:00
Guan09 73e2fb91af fix(studio): restore action hit areas and unify hover feedback 2026-10-01 14:43:36 +08:00
xiexian 4df81041b1 fix(studio): dim the busy plugin source input 2026-10-01 14:41:29 +08:00
YHH 4bcda5691d Merge pull request #11511 from KHG420/codex/studio-skill-catalog-guide
docs(skills): explain the catalog projection and cache boundary
2026-09-30 23:32:07 -07:00
esengine 383e6369ac refactor(desktop): read the shell's ability to reveal from the host, not the hub 2026-09-30 23:30:59 -07:00
esengine 6bb625fcf7 fix(desktop): extract the reveal menu item, add refusal wording 2026-09-30 23:22:30 -07:00
YHH 8d963086ea Merge pull request #11529 from esengine/feat/subagent-bg-rows
feat(studio): sub-agent transcript entry in the deck, show-all for clipped nested output
2026-09-30 23:21:57 -07:00
esengine 85df91d4ef feat(desktop): show a project's folder in the file manager from the sidebar menu
The project menu offered only sort and remove-from-list. Add a reveal item that
asks the hub where a listed project lives (GET /host/workspaces/locate, window
only, refuses any folder the window does not list) and has the shell select it,
so it needs no open pane. Remote hosts draw in their own rail and never reach
this menu; a project whose folder is gone shows the item disabled with the
reason.

Refs #11524
2026-09-30 23:21:09 -07:00
xiexian af2dbb252d fix(studio): keep package export feedback current and visible 2026-10-01 14:15:30 +08:00
esengine 8de4b4851c fix(electron): label the edit menus in the interface language
Role-only menu items take their text from Electron's built-in English
catalogue even when the app locale is zh-CN (verified on a real Electron:
getLocale() == zh-CN, labels Undo/Cut/Copy/Paste/Select All). Keep the roles
for the clipboard work and give each item an explicit zh/en label chosen by the
page's saved language (rx-lang), falling back to the machine locale exactly as
the page does. The macOS Edit menu is rebuilt when the language preference
changes.

Refs #11524
2026-09-30 23:15:27 -07:00
esengine fb3d9d7433 fix(studio): register the new actions, trap focus in the transcript overlay, cap its output
Registers tool.show-all, agent.open, agent.close and agent.focus-trap, moves
focus into the overlay and back to the deck chip, caps the overlay's rendered
output at 200000 characters with a note, keeps the sub-agent count stable, wraps
nested output, and drops a stray .agents rule that changed the sidebar spacing.
2026-09-30 23:13:00 -07:00
Harbor404 0a2a8b15ed fix(boot): log the resolved shell at debug level
Headless `run` must leave stderr empty unless --debug is passed, so the
boot record of the resolved shell interpreter cannot be an Info log. It is a
diagnostic; the always-on counterpart is the Warn shellrun emits when a
launch actually fails.
2026-10-01 14:11:01 +08:00
YHH 66029c7dec Merge pull request #11521 from KHG420/codex/studio-plugin-export-refusal
fix(studio): preserve plugin export failure reasons
2026-09-30 23:07:39 -07:00
esengine 43b210573b feat(studio): sub-agent rows open a full transcript, and clipped nested output offers show-all
The sub-agent list stays after delegates finish and each row opens that
sub-agent's whole transcript. A nested call whose output exceeds the 400
character preview now says so with a show-all control instead of cutting.
2026-09-30 23:02:34 -07:00
xiexian 422b47b3a0 docs(skills): describe the turn-projected catalog 2026-10-01 13:39:24 +08:00
YHH d80c290d89 Merge pull request #11519 from KHG420/codex/studio-plugin-source-confirmation
fix(studio): keep the confirmed plugin source stable during preview
2026-09-30 22:29:28 -07:00
YHH 594cbf664a Merge pull request #11520 from KHG420/codex/studio-package-action-errors
fix(studio): expose failed package operations alongside diagnostics
2026-09-30 22:28:38 -07:00
YHH 7bb8031e85 Merge pull request #11514 from KHG420/codex/studio-shared-project-skill
docs(skills): verify sharing a project playbook across collaborators
2026-09-30 22:10:17 -07:00
xiexian 188df0245b fix(studio): preserve plugin export refusal details 2026-10-01 13:04:18 +08:00
xiexian c2df8e3ed6 fix(studio): reveal package action failures with diagnostics 2026-10-01 12:54:56 +08:00
xiexian 104d01ba07 fix(studio): keep plugin source stable during preview 2026-10-01 12:43:45 +08:00
YHH e977923203 Merge pull request #11509 from KHG420/codex/studio-mcp-partial-retry
fix(studio): preserve partial MCP installation results on retry
2026-09-30 21:34:17 -07:00
YHH 5450ed38e9 Merge pull request #11510 from KHG420/codex/studio-backup-plugin-owner
fix(studio): isolate plugin selections during backup restore
2026-09-30 21:33:27 -07:00
xiexian 3fcce85ca7 docs(skills): cover repository sharing and personal activation 2026-10-01 12:26:06 +08:00
Harbor404 1650571850 fix(shell): keep the PowerShell probe within lint budgets
Trim repolint comment blocks and move launch-failure logging out of RunForeground so the function stays within the 120-line limit. No behavior change.

Signed-off-by: Harbor404 <[email protected]>
2026-10-01 12:07:06 +08:00
xiexian d6cc51fe76 fix(studio): keep restored plugin installers with their selection 2026-10-01 11:50:07 +08:00
xiexian abca1a62c5 fix(studio): retain partial MCP outcomes and retry remaining servers 2026-10-01 11:45:26 +08:00
YHH b70b86e7a7 Merge pull request #11494 from esengine/fix/headless-trust-silence
fix(run): say why a headless run in an untrusted folder did nothing
2026-09-30 20:38:05 -07:00
xiexian 86f5ea0c5a fix(sdk): make starter interceptor work on composed inputs 2026-10-01 11:36:41 +08:00
YHH 754166fc32 Merge pull request #11502 from KHG420/codex/studio-plugin-install-confirmation
fix(studio): preserve new plugin confirmation during install
2026-09-30 20:34:00 -07:00
YHH 0aeb204660 Merge pull request #11500 from KHG420/codex/studio-mcp-install-controls
fix(studio): keep MCP install confirmation stable while pending
2026-09-30 20:33:10 -07:00
YHH 1fa7030ced Merge pull request #11499 from KHG420/codex/studio-mcp-partial-install-notification
fix(studio): refresh inventory after partial MCP installation
2026-09-30 20:32:20 -07:00
YHH 43661cdec1 Merge pull request #11498 from KHG420/codex/studio-mcp-install-connection
fix(studio): reset MCP installation state on connection changes
2026-09-30 20:31:30 -07:00
YHH e07bc5faa8 Merge pull request #11496 from KHG420/codex/studio-mcp-operation-owner
fix(studio): scope MCP row confirmations and responses
2026-09-30 20:30:40 -07:00
YHH a968fd8881 Merge pull request #11497 from KHG420/codex/studio-inspection-shortcut-guard
fix(studio): guard installation inspection shortcuts
2026-09-30 20:29:50 -07:00
YHH 420a9231c5 Merge pull request #11458 from KHG420/codex/studio-project-order-11420
feat(studio): persist manual project order without changing launch project
2026-09-30 20:29:00 -07:00
YHH 11b84e5891 Merge pull request #11481 from KHG420/codex/studio-frontend-page-example
docs(studio): add a local frontend delivery example
2026-09-30 20:28:10 -07:00
YHH 0495d1f294 Merge pull request #11485 from KHG420/codex/studio-author-settled-notification
fix(studio): preserve author install notifications after sign-out
2026-09-30 20:27:20 -07:00
YHH 0040dec2a0 Merge pull request #11486 from KHG420/codex/studio-runtime-reload-connection
fix(studio): scope runtime reload state to its connection
2026-09-30 20:26:31 -07:00
YHH 4b94a02c27 Merge pull request #11489 from KHG420/codex/studio-package-operation-connection
fix(studio): reset package operation state on connection change
2026-09-30 20:25:41 -07:00
YHH 28a9010d0e Merge pull request #11475 from KHG420/codex/studio-url-elicitation-11348
feat(mcp): support consent-based URL elicitation
2026-09-30 20:24:40 -07:00
xiexian 2b4d79ceb5 test(boot): cover copied command lifecycle at provider boundary 2026-10-01 11:22:34 +08:00
xiexian 37924b57e4 test(studio): verify installed hook context lifecycle 2026-10-01 11:09:51 +08:00
Harbor404 e69108a843 fix(mcp): preserve disabled tool policy
Project configuration could replace a user server entry and drop disabled_tools, while a stale call to a filtered tool was reported as unknown. Merge user and project disables, reject empty names, and carry a typed refusal through boot and hot-added MCP registration.

Move an existing contextual-tool helper unchanged so execute_one.go does not grow past the file-size ratchet.
2026-10-01 11:09:15 +08:00
Harbor404 9b5ca5ff71 fix(shell): only trust a PowerShell that starts, and record which ran
A pinned [tools.shell] path was honoured on existence alone, so a pwsh.exe
that stats fine but will not start was held for the whole session while every
command failed at launch — the failure #10633 reported, still reachable
through the pinned-path branch after #10617 reworked auto-detection.

- Prove a pinned PowerShell the way the bash arm already proves its own, and
  fall back to detection with a warning when it will not start.
- VerifyShell proves a pinned PowerShell too, so the settings screen refuses
  one where it was typed rather than on every later command.
- powerShellLaunches answers false for a path that is not there: a missing
  executable is not launchable, so callers that gate on it alone now agree.
- resolveShell takes the launch probe as an argument, which puts the decision
  table ("pwsh will not start, 5.1 will") under test on any host.
- Detection that finds nothing says so instead of silently returning bare bash.
- Boot records the interpreter the session resolved to, and a launch-phase
  spawn failure records the shell, its path, the caller and the error. The
  desktop relays the sidecar's stderr into desktop-shell/logs/, which the
  report found empty.

Windows end-to-end is not verified locally; the fallback is covered by the
injected probe on Linux and macOS.
2026-10-01 11:07:32 +08:00
xiexian 71fb27901b fix(studio): preserve new plugin confirmation during install 2026-10-01 10:42:52 +08:00
xiexian 223c55b95f fix(studio): keep MCP install confirmation stable while pending 2026-10-01 10:34:49 +08:00
xiexian d84543d259 fix(studio): refresh inventory after partial MCP installation 2026-10-01 10:17:49 +08:00
xiexian 0247623274 fix(studio): reset MCP installation state on connection changes 2026-10-01 10:05:31 +08:00
YHH 13f238e6f4 Merge pull request #11492 from KHG420/codex/studio-mcp-operation-guard
fix(studio): guard conflicting MCP row operations
2026-09-30 19:00:24 -07:00
YHH 0209b345a0 Merge pull request #11495 from KHG420/codex/studio-skill-operation-owner
fix(studio): scope skill operation results and refresh current inventory
2026-09-30 18:57:14 -07:00
esengine 8fa67e544e fix(run): say why a headless run in an untrusted folder did nothing
A run in a folder with no trust decision refused every edit and command
under the asking default and exited 0 with an empty stderr (plain text
run) and only the generic 'no interactive approver' reason for the model.

The refusal now carries permission.untrusted_folder. The model reads the
cause and the remedy, the text run prints refused tools, code and remedy on
stderr, the structured result's denial carries a remedy, and
--fail-on-unverified exits 3 for it. Exit 0 and the trust gate itself are
unchanged.

Refs #11493
2026-09-30 18:55:48 -07:00
xiexian fe850bc160 fix(studio): guard installation inspection shortcuts 2026-10-01 09:50:50 +08:00
xiexian f527fa023d fix(studio): scope MCP row confirmations and responses 2026-10-01 09:41:52 +08:00
YHH d98ae85fb0 Merge pull request #11490 from esengine/fix/feedback-reply-test-race
test(feedback): read the reply stub's hits under its lock
2026-09-30 18:34:38 -07:00
xiexian 687186fcdd fix(studio): scope skill results and refresh current inventory 2026-10-01 09:31:31 +08:00
xiexian d30fd833d9 fix(studio): guard conflicting MCP row operations 2026-10-01 09:08:42 +08:00
esengine 459c2ae5c4 test(feedback): read the reply stub's hits under its lock 2026-09-30 18:00:24 -07:00
xiexian 33199377da fix(studio): reset package operation state on connection change 2026-10-01 08:57:14 +08:00
xiexian 1f1c469b16 fix(studio): scope runtime reload state to its connection 2026-10-01 08:51:01 +08:00
xiexian 7fe57fc25a fix(studio): preserve author install notifications after sign-out 2026-10-01 08:43:39 +08:00
YHH 6cc249f1d4 Merge pull request #11482 from esengine/release/studio-2.24.0-notes
docs(release): Studio 2.24.0 release notes
2026-09-30 17:36:02 -07:00
xiexian e5a801c603 feat(studio): persist manual project order without changing launch project 2026-10-01 08:35:14 +08:00
esengine fe5ea48568 docs(release): Studio 2.24.0 release notes 2026-09-30 17:29:36 -07:00
YHH 3aa259f2e2 Merge pull request #11480 from KHG420/codex/studio-publish-connection-state
fix(studio): reset publish form when its owner changes
2026-09-30 17:26:57 -07:00
YHH ed9c424991 Merge pull request #11474 from KHG420/codex/studio-api-notes-example
docs(studio): add a local API documentation scenario
2026-09-30 17:26:07 -07:00
YHH 9328ef003a Merge pull request #11478 from KHG420/codex/studio-market-connection-state
fix(studio): refresh market state across runtime connections
2026-09-30 17:24:07 -07:00
xiexian 824f95613a feat(mcp): support consent-based URL elicitation 2026-10-01 08:19:31 +08:00
xiexian 03d5346869 docs(studio): add a local frontend delivery example 2026-10-01 08:15:01 +08:00
YHH 490f3d0bfe Merge pull request #11479 from esengine/fix/11434-host-handshake
fix(studio): wait longer for a cold kernel start, retry an early exit once, and name the cause
2026-09-30 17:03:11 -07:00
esengine 64c8aa5170 fix(studio): wait longer for a cold kernel start, retry an early exit once, and name the cause
The shell gave the kernel 20 s to handshake and reported every failure as a
sentence. The wait is now 60 s (REASONIX_STUDIO_HANDSHAKE_TIMEOUT_MS), a small
starting window appears after 3 s, an exit or spawn failure within 10 s is
retried once, and launch failures carry a typed code (timeout, exited, spawn,
overflow) with a matching explanation in the error box.

Refs #11434
2026-09-30 16:54:16 -07:00
xiexian 00d9c2e490 docs(studio): add a source-linked API guide example 2026-10-01 07:48:20 +08:00
xiexian ff1163da75 fix(studio): refresh market state across runtime connections 2026-10-01 07:47:55 +08:00
xiexian 6cd9593404 fix(studio): reset publish form when its owner changes 2026-10-01 07:44:43 +08:00
YHH b3d7dd50fc Merge pull request #11476 from KHG420/codex/studio-compatible-mcp-ownership
fix(studio): preserve package ownership on explicit MCP connection
2026-09-30 16:37:16 -07:00
YHH f8e541a86b Merge pull request #11466 from KHG420/codex/studio-author-apply-generation
fix(studio): isolate author install responses by connection
2026-09-30 16:35:26 -07:00
YHH b5ea839c94 Merge pull request #11446 from esengine/feat/feedback-replies-ui
feat(studio): reply threads, needs-input and unread badge in the feedback dialog
2026-09-30 16:07:35 -07:00
xiexian 4c8eda7344 fix(studio): preserve package ownership on explicit MCP connection 2026-10-01 06:59:24 +08:00
xiexian dfd8aac901 fix(studio): isolate author installs across connection changes 2026-10-01 06:56:28 +08:00
YHH f69e2c5798 Merge pull request #11472 from KHG420/codex/studio-issue-fix-example
docs(plugins): add a runnable repository issue exercise
2026-09-30 15:39:20 -07:00
YHH 1918bb4f55 Merge pull request #11357 from BuGlessRB/open/ask-autosubmit
feat(cli): auto-submit a fully answered ask batch, gated on auto_submit / 功能(cli):多题问询答完最后一题即自动提交,由 auto_submit 控制
2026-09-30 15:38:35 -07:00
YHH 53d21288df Merge pull request #11465 from KHG420/codex/studio-market-retry-focus
fix(studio): preserve focus through market read recovery
2026-09-30 15:36:09 -07:00
YHH 3c0c265c22 Merge pull request #11467 from KHG420/codex/studio-compatible-mcp-effect
test(plugins): cover compatible MCP install lifecycle
2026-09-30 15:15:30 -07:00
YHH e179b51c52 Merge pull request #11469 from KHG420/codex/studio-update-feedback
fix(studio): preserve applying updates across settings controls
2026-09-30 15:10:44 -07:00
YHH 28b6680353 Merge pull request #11468 from KHG420/codex/studio-disabled-mcp-tools-5444
feat(studio): support disabling individual MCP tools
2026-09-30 15:02:44 -07:00
xiexian 788d2b8d41 fix(studio): preserve keyboard focus across market read recovery 2026-10-01 05:54:19 +08:00
YHH f5e0d83f0d Merge pull request #11454 from KHG420/codex/studio-hook-event-labels-11383
fix(studio): localize automation event labels
2026-09-30 14:48:18 -07:00
xiexian 3b21d25de8 docs(plugins): add a runnable repository issue exercise 2026-10-01 05:42:27 +08:00
xiexian c3b51178cb fix(studio): preserve applying updates across settings controls 2026-10-01 05:26:48 +08:00
YHH 18df489e23 Merge pull request #11449 from KHG420/codex/studio-marketplace-subdir-11378
fix(studio): install git-subdir marketplace plugins
2026-09-30 14:16:47 -07:00
YHH 5598442a99 Merge pull request #11459 from KHG420/codex/studio-bundled-skill-effect
test(studio): verify copied skill references reach the provider
2026-09-30 14:15:50 -07:00
YHH 3b8bb5492b Merge pull request #11460 from KHG420/codex/studio-update-panel-guard
fix(studio): isolate inline package updates through apply
2026-09-30 14:13:47 -07:00
xiexian 1718074787 test(plugins): cover compatible MCP install lifecycle 2026-10-01 05:11:41 +08:00
xiexian bb013c4bc9 feat(studio): filter disabled MCP tools before registration 2026-10-01 05:09:47 +08:00
YHH 1cbd447539 Merge pull request #11456 from KHG420/codex/studio-author-read-races
test(studio): cover stale author package reads
2026-09-30 14:09:11 -07:00
Stephen R. van den Berg 5b609815c5 feat(cli): auto-submit a fully answered ask batch, gated on auto_submit
Answering the last question of a multi-question ask with nothing left
unanswered commits the batch immediately, skipping the extra confirmation
Enter; an incomplete batch jumps to the first unanswered question instead. The
behaviour is gated on a new top-level `auto_submit` option (default false):
with it on the ask drops its Submit tab, and without it the Submit tab is shown
and still needs its explicit Enter.

`auto_submit` is a user preference: LoadForRoot holds it across the project
merge so a cloned repository cannot turn it on, and neither the annotated
project render nor the project delta carries it. Answering moves on to the next
unanswered question, so answering one the panel was thrown back to skips ahead
to the next gap rather than stopping on the already-answered question that
follows it; only the last question, with nothing left unanswered, commits. With
auto-submit on there is no Submit tab to page onto, so right stops at the last
question rather than landing on a hidden tab that commits.

- config: new top-level auto_submit, user/global only; rendered in the user's annotated TOML and omitted from a project render and the project delta.
- tui: Options.AutoSubmit; nextQuestion advances to the next unanswered question and commits on the last; right stops at the last question instead of a hidden Submit tab.
- program_test.go: the commit, next-unanswered-after-skip-back, skipped-question, hidden-tab, no-option and arrow-stops-at-last cases.
- docs: auto_submit in CONFIG_PATHS.md, reasonix.example.toml, and the Guide shortcut tables (en and zh-CN).
2026-09-30 22:42:33 +02:00
Stephen R. van den Berg 0102bf3a6a refactor(config): split provider-source scope and the [ui] section into files
Move the providerSourceScope type out of config.go and the [ui] table render
out of render.go into their own files, so both sit under their recorded
repolint budgets with room for the options that follow.
2026-09-30 22:42:33 +02:00
YHH 873990724d Merge pull request #11445 from esengine/feat/feedback-replies-kernel
feat(feedback): reply threads, triage statuses and unread state in the client
2026-09-30 13:38:50 -07:00
xiexian 9d950f34bc fix(studio): describe automation events in the UI language 2026-10-01 04:38:08 +08:00
xiexian e9882d667e fix(studio): keep package updates isolated until apply settles 2026-10-01 04:27:31 +08:00
YHH 0af416ce89 Merge pull request #11453 from KHG420/codex/studio-install-preview-i18n
fix(studio): localize installation trust disclosures
2026-09-30 13:24:20 -07:00
YHH ee625f5e41 Merge pull request #11443 from KHG420/codex/studio-own-installed-entry
fix(studio): locate capabilities after author installs
2026-09-30 13:23:25 -07:00
xiexian d5a5a6e70f test(studio): verify copied skill references reach the provider 2026-10-01 04:13:52 +08:00
YHH 53b82d1362 Merge pull request #11442 from KHG420/codex/studio-market-retry
fix(studio): retry failed market list and detail reads
2026-09-30 13:10:30 -07:00
xiexian 5c78af6b09 fix(studio): install git-subdir marketplace plugins 2026-10-01 04:09:53 +08:00
xiexian dfbe776afb fix(studio): localize installation trust disclosures 2026-10-01 04:06:09 +08:00
xiexian 0056f8386b fix(studio): locate capabilities after author installs 2026-10-01 03:53:40 +08:00
YHH bf8210aebf Merge pull request #11439 from esengine/fix/skill-invocation-flags
fix(skill): honour disable-model-invocation, user-invocable and argument-hint
2026-09-30 12:53:16 -07:00
xiexian 88966f614f test(studio): cover stale author package reads 2026-10-01 03:46:07 +08:00
YHH 46542dd558 Merge pull request #11450 from KHG420/codex/studio-package-operation-guard
fix(studio): guard concurrent package operations
2026-09-30 12:36:43 -07:00
YHH 4376074c23 Merge pull request #11447 from KHG420/codex/studio-plugin-update-effect
test(studio): verify plugin updates replace MCP contributions
2026-09-30 12:35:53 -07:00
YHH ff6f93280e Merge pull request #11444 from KHG420/codex/studio-author-read-retry
fix(studio): retry failed author market reads
2026-09-30 12:34:57 -07:00
xiexian 651d2bd4df fix(studio): retry failed market list and detail reads 2026-10-01 03:32:43 +08:00
YHH d6eed9aee4 Merge pull request #11422 from esengine/feat/model-overrides-11392
feat(studio): per-model context window and max output in the provider form
2026-09-30 12:18:34 -07:00
YHH 695802235e Merge pull request #11435 from esengine/feat/feedback-ui
feat(studio): feedback dialog with screenshots, environment block and my-feedback list
2026-09-30 12:17:36 -07:00
esengine 5d786d6de0 feat(studio): reply threads, needs-input and unread badge in the feedback dialog
My feedback shows the thread under each report as plain-text bubbles, a
needs-your-input chip with an inline reply box (byte counter, errors per code),
status chips for answered and closed, and a New mark that stays for the visit
in which the replies were read. The sidebar entry and the command palette item
carry an unread count that is read when the app opens and when the page opens,
never on a timer. The form copy says a report becomes public only if it is
filed as an issue, and keeps the warning about secrets and screenshots.

Refs #11424
2026-09-30 12:16:20 -07:00
esengine 01a52ce15e feat(feedback): reply threads, triage statuses and unread state in the client
The kernel reads the new statuses (needs_info, answered, closed), each report's
thread and needsInput, keeps the last-seen reply id per report locally and
exposes unread/hasNew for an entry badge. A reply goes through the controller
port, serve (POST /feedback/{receipt}/reply and /seen) and the TUI
(/feedback show, /feedback reply with --yes as the first token). The new
worker codes become sentinels: feedback.reply_limit, feedback.not_replyable
and feedback.challenge_required. A reply is never retried. Thread text is
cleaned of escape sequences before any frontend sees it. Submit copy no longer
says every report becomes public.

Refs #11424
2026-09-30 12:16:12 -07:00
YHH ad99a35714 Merge pull request #11452 from esengine/ci/feedback-sync-workersdev
ci(feedback-sync): reach the worker through workers.dev
2026-09-30 12:13:05 -07:00
esengine 74318f9d49 ci(feedback-sync): reach the worker through workers.dev 2026-09-30 11:58:16 -07:00
esengine a237c5843e fix(skill): refuse user-only skills as task profiles and in capability routing 2026-09-30 11:43:20 -07:00
YHH fa85a69b9d Merge pull request #11448 from esengine/fix/feedback-sync-403-diag
fix(feedback): show the response origin and body snippet when the worker refuses the sync token
2026-09-30 11:42:27 -07:00
xiexian 1320621cbe fix(studio): guard concurrent package operations 2026-10-01 02:37:10 +08:00
esengine c116fd76ec fix(feedback): show the response origin and body snippet when the worker refuses the sync token 2026-09-30 11:32:11 -07:00
xiexian e3ae0e78ab test(studio): verify plugin updates replace MCP contributions 2026-10-01 02:27:33 +08:00
YHH 6d9266fc8e Merge pull request #11440 from esengine/feat/feedback-sync-replies
feat(feedback): mirror reporter replies, publish only released items and images
2026-09-30 11:18:25 -07:00
xiexian d2a86112a4 fix(studio): retry failed author market reads 2026-10-01 02:14:44 +08:00
esengine d08999485a feat(feedback): mirror reporter replies to their issue, publish only released items and images
The worker now holds new submissions by default. The converter still
creates issues only for received items, links only images the worker marks
released, and posts one idempotent bot comment per reporter reply on items
that have an issue, then acks it. Contact is never read by the converter,
which a test pins.

Refs #11424
2026-09-30 10:52:34 -07:00
YHH 42ef9dd191 Merge pull request #11438 from esengine/fix/remote-error-diagnosis-studio
fix(remote): classify relay connection failures and show the specific cause
2026-09-30 10:00:04 -07:00
esengine cf2146408a fix(skill): honour disable-model-invocation, user-invocable and argument-hint 2026-09-30 09:53:07 -07:00
esengine 226984ba69 fix(remote): say why a relay connection failed instead of one generic sentence
A browser cannot read the status of a refused WebSocket upgrade, so a
rate limit, a refused origin, an expired ticket, a network loss and an
unreachable gateway all read as "relay temporarily unavailable". When the
socket errors before opening, one probe round (offline check, the gateway
health route, and an attachment preflight that only an allowed origin passes)
now classifies the failure by typed code; a 429 from the account service
carries its Retry-After. The notice names the cause, offers reconnect
(counting down a rate limit), and a reconnect that runs out of retries ends
with the last attempt's cause, or idle when the relay had reclaimed the
lease. The generic sentence remains only when the probe itself fails.
2026-09-30 09:51:57 -07:00
YHH f309fed993 Merge pull request #11432 from esengine/feat/feedback-client
feat(feedback): in-app feedback client, controller port, serve endpoints, TUI /feedback
2026-09-30 09:07:05 -07:00
esengine 6db27d94ae feat(studio): feedback dialog with screenshots, environment block and my-feedback list
Refs #11424
2026-09-30 08:52:25 -07:00
esengine f72102c8a6 feat(feedback): in-app feedback client, controller port, serve endpoints and TUI /feedback
Adds internal/platform/feedback: env collection, local redaction (the
crashreport implementation, now exported), image re-encode with a metadata
self-check, idempotent submit with retry, persisted install identity and
receipts, and GET /v1/feedback/mine with typed sentinel errors.

The controller exposes it as the Feedback port (SessionAPI), serve answers
GET /feedback/env, GET /feedback/mine, POST /feedback and POST /feedback/name
with dotted refusal codes, and /feedback [list|name|<category> --yes <text>]
is a controller slash command so the TUI inherits it. Nothing reaches a model
request or the cache-stable prefix.

Refs #11424
2026-09-30 08:45:43 -07:00
YHH 7733545274 Merge pull request #11427 from esengine/feat/feedback-sync
ci(feedback): converter and status sync for in-app feedback
2026-09-30 08:25:58 -07:00
YHH fa39e7db7f Merge pull request #11379 from chuiwenwei/feat/studio-index-corpus-v2
feat(bench): give the index corpus a measured efficiency substrate
2026-09-30 08:20:39 -07:00
YHH 21b3b35210 Merge pull request #11423 from esengine/fix/11421-git-bash-discovery
fix(sandbox): find Git Bash outside Program Files when a WSL bash shadows it on PATH
2026-09-30 08:04:41 -07:00
esengine 714c4dc935 ci(feedback): sync in-app feedback into issues and report status back
Adds a scheduled converter (scripts/feedback-sync.mjs) that turns pending
Studio feedback into issues, idempotently via a hidden marker, and reports
in_progress/fixed/wontfix/duplicate back to the worker. A fix recorded as
"next" is upgraded to its studio-v tag once the tag exists.
2026-09-30 08:04:34 -07:00
Guan09 a2a429e3aa fix(studio): tighten user message action spacing 2026-09-30 23:02:44 +08:00
esengine 85810c192d fix(sandbox): find Git Bash outside Program Files when a WSL bash shadows it on PATH
exec.LookPath returns only the first bash.exe on PATH, which is normally the
WSL launcher in System32, and the fallback list covered only the default
Program Files roots. A Git for Windows install elsewhere (with just its cmd
directory on PATH) was therefore never detected, so Studio offered PowerShell
only. Candidates now also come from every PATH directory and from the Git
tree the git.exe on PATH belongs to, and the WSL launcher is rejected in the
candidate loops too.

Fixes #11421
2026-09-30 07:51:57 -07:00
Will 5e2985ab6f feat(bench): fill the index substrate with six tasks that hold on both arms
Screened 87 candidates, staged: one pass over the pool, then two more only for
the candidates that were clean on both boundary arms. The six that held on both
arms in all three passes stay, the rest are dropped, so the efficiency substrate
only contains tasks that qualify:

  quarter  i62-epochlatch, i37-dispatchlag
  half     i106-budgetseal, i70-ingestfloor
  default  i47-tenantlease, i51-routedelay

All six are SnippetStop, SnippetThenRead or DefensiveRead in each of their
eighteen judged cells - nothing post-sufficient.

What the screening found: the answer kind decides whether a candidate holds.
Half-tier candidates whose answer was a bare number never survived (0 of 8);
the same tier with a codename answer survived 11 of 20. The dirty class in both
cases was PostSufficientRetrieval - the model re-checking a value it already had
- so the shape that needs no checking is the one that holds.

Also: i07 and i10 were re-measured after the review asked for it (their old
numbers came from the grep shape they no longer use), NeverSufficient is
reported separately rather than counted as clean, and the i11/i12 rows are gone
because those tasks are not in the corpus.
2026-09-30 22:49:27 +08:00
YHH 6f5c150339 Merge pull request #11425 from esengine/fix/11418-studio-launch-workspace
fix(studio): do not open the app's own install folder as the first workspace
2026-09-30 07:40:21 -07:00
esengine fb3a57133b fix(studio): do not open the app's own install folder as the first workspace
The host took its working directory as the first pane's project. A Start menu
launch inherits the install directory and a Finder launch the filesystem root,
so the app's folder was listed in the sidebar again after every removal. The
directory now counts only when it is outside the app's own directory and not a
filesystem root; otherwise the first remembered folder that still exists is
opened, then the home directory.

Fixes #11418
2026-09-30 07:22:40 -07:00
esengine b5b91b1c71 feat(studio): set context window and max output per model on a provider
The provider form only edited one window and one output cap for every model
on a connection. model_overrides already carried both per model and the
kernel resolves them for requests and compaction, so the edit endpoint and
the form now read and write them.

A model with no entry inherits the connection's value, shown as a grey
placeholder; a blank field is never sent as an override, and clearing one
removes the entry.
2026-09-30 07:19:36 -07:00
Guan09 e1f30114d2 fix(studio): show copy feedback and remove empty user header 2026-09-30 20:22:49 +08:00
Guan09 23ade961ba feat(studio): 调整用户消息操作栏并增加复制按钮 2026-09-30 18:20:53 +08:00
Will e47a2037f2 feat(bench): screen replacement candidates for the index substrate
The review settled the bar: a task qualifies only if it is clean on both of its
boundary arms, and none of the first six candidates is. This adds five more,
authored to the same shape but with a single value each and a call whose
arguments name the topic (./scripts/x {{cue}}), which is the shape the review
pointed at for i07 and i10.

- i15-ingest-lag and i16-commit-fence (quarter), i17-route-budget and
  i18-salvage-epoch (half), i19-tenant-quota (default).
- All five preflight clean at their own tier's generation.
- The corpus stays unqualified until six candidates hold on both arms across a
  three-pass batch; the ones that do not are dropped rather than promoted.
2026-09-30 17:52:28 +08:00
1375 changed files with 85827 additions and 5130 deletions

No files matched your search

+1
View File
@@ -10,6 +10,7 @@
# 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
+6
View File
@@ -6,3 +6,9 @@ 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.
+16
View File
@@ -8,6 +8,22 @@ only, on `main-v2`.
-
## Cause
-
## Blast radius
-
## Neighbouring behaviours tested
-
## Why this layer
-
## Issues
<!--
Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

+14 -10
View File
@@ -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/)'; then scripts=true; fi
if echo "$files" | grep -qE '^(scripts/|npm/|release-notes/studio/|\.github/workflows/release-studio\.yml$)'; then scripts=true; fi
fi
fi
{ echo "code=$code"; echo "desktop=$desktop"; echo "sdk=$sdk"; echo "scripts=$scripts"; } >> "$GITHUB_OUTPUT"
@@ -80,7 +80,11 @@ jobs:
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "22"
- run: node --test scripts/release-credits.test.mjs scripts/markdown-refs.test.mjs scripts/studio-release-notes.test.mjs scripts/publish-cli-pointer.test.mjs npm/publish.test.mjs
- 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
# 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
@@ -175,12 +179,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 8m, not 3m: across four consecutive runs of the same
# That timeout is 12m, 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. At 3m the
# step failed on slow runners without a code change; a hang still trips
# the 15 minute job budget above.
# 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.
#
# 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
@@ -191,11 +195,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: 15
timeout-minutes: 20
env:
REASONIX_RELEASE_CACHE_GUARD: "1"
WINDOWS_SANDBOX_WAIT_MS: "20000"
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/...
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/...
# The general Windows PR smoke list intentionally omits internal/contract/tool.
# Keep the session-temp portability contract covered without widening the
@@ -214,7 +218,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 8m package budget as Windows smoke:
# Full push suite still uses the same 12m 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
@@ -230,7 +234,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=8m ./...
run: go test -count=1 -p 4 -timeout=12m ./...
race:
needs: changes
+44
View File
@@ -0,0 +1,44 @@
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
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
with:
version: 10
run_install: false
+77
View File
@@ -0,0 +1,77 @@
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[@]}"
+22 -6
View File
@@ -757,14 +757,18 @@ 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" -o "reasonix$ext" ./cmd/reasonix
-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
if [ "$os" = windows ]; then
zip -qj "dist/reasonix-$os-$arch.zip" "reasonix$ext"
else
@@ -1211,13 +1215,13 @@ jobs:
# 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 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.
# 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.
cli-channels:
name: publish the CLI to npm and Homebrew
needs: [resolve, publish, cli-gate, cli-tag]
needs: [resolve, publish, cli-gate, cli-tag, cli-pointer]
if: vars.STUDIO_PUBLISHES_CLI == 'true'
runs-on: ubuntu-latest
# id-token lets npm publish --provenance attach a Sigstore attestation; the
@@ -1256,6 +1260,18 @@ jobs:
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
+1
View File
@@ -15,6 +15,7 @@
/runtime-resume
/extension-protocol-gen
/reasonix-plugin-example
/mcp-line-counter-kit
/reasonix-studio-host
/signpath-contract
/studio-delta
+10 -2
View File
@@ -18,6 +18,8 @@
<a href="https://esengine.github.io/DeepSeek-Reasonix/">Website</a>
&nbsp;·&nbsp;
<strong><a href="https://discord.gg/XF78rEME2D">Discord</a></strong>
&nbsp;·&nbsp;
<strong><a href="https://qm.qq.com/q/i59b0z2R8s">QQ</a></strong>
</p>
<p align="center">
@@ -31,6 +33,7 @@
<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">
@@ -41,12 +44,16 @@
<br/>
<p align="center"><strong>Open source · MIT · a single Go binary</strong></p>
<h3 align="center">A coding agent you can leave running.</h3>
<h3 align="center">A reliable coding agent for complex software engineering tasks.</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/>
@@ -265,8 +272,9 @@ 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>
+10 -2
View File
@@ -18,6 +18,8 @@
<a href="https://esengine.github.io/DeepSeek-Reasonix/">官方网站</a>
&nbsp;·&nbsp;
<strong><a href="https://discord.gg/XF78rEME2D">Discord</a></strong>
&nbsp;·&nbsp;
<strong><a href="https://qm.qq.com/q/i59b0z2R8s">QQ</a></strong>
</p>
<p align="center">
@@ -31,6 +33,7 @@
<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">
@@ -41,12 +44,16 @@
<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)
## 版本
@@ -245,9 +252,10 @@ 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>
+25
View File
@@ -320,6 +320,31 @@ 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
+82 -11
View File
@@ -3,12 +3,12 @@
Two questions about long-horizon memory, and the harness that makes their
answers trustworthy.
-mode=preflight build all 12 tasks under every arm and check the contract
-mode=preflight build all 18 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: 6 tasks across their own cue boundary
-mode=run-index Experiment I, full: 6 tasks x 4 index budgets
-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=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: see
"Index corpus v2" below.
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.
**Things that turned out not to be problems**, each after being measured rather
than argued about:
@@ -74,11 +74,83 @@ 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, if this is reopened
## Index corpus v2
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
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
`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`,
@@ -86,8 +158,7 @@ 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. Keep i03 and i04 — as a stopping corpus, not an
efficiency one.
nothing in the workspace.
`-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
+132 -7
View File
@@ -26,6 +26,12 @@ 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
@@ -45,6 +51,8 @@ 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.
@@ -121,6 +129,20 @@ 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{
{
@@ -192,10 +214,104 @@ func searchTasks() []contextTask {
}
}
func indexTasks() []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 {
return []contextTask{
{
ID: "i01-transport-fallback", Experiment: experimentIndex, TargetKind: "tool_input",
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,
Prompt: "transport fallback probe 当时报告的 selector 是什么?",
ProbeQuery: "transport fallback probe selector",
Vars: []varSpec{code("cue", "tfprobe"), code("selector", "fern")},
@@ -206,7 +322,7 @@ func indexTasks() []contextTask {
PlantBody: "fallback selector = {{selector}}",
},
{
ID: "i02-scheduler-handoff", Experiment: experimentIndex, TargetKind: "tool_input",
ID: "i02-scheduler-handoff", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleStopping,
Prompt: "scheduler handoff 当时记录的 grace 和 marker 是什么?",
ProbeQuery: "scheduler handoff grace marker",
Vars: []varSpec{
@@ -220,7 +336,7 @@ func indexTasks() []contextTask {
PlantBody: "handoff grace = {{grace}}\nmarker = {{marker}}",
},
{
ID: "i03-coalescing", Experiment: experimentIndex, TargetKind: "tool_input",
ID: "i03-coalescing", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleStopping,
Prompt: "job completion 的 coalescing probe 最后记录的 window 和 marker 是多少?",
ProbeQuery: "coalescing policy completion window",
Vars: []varSpec{
@@ -234,7 +350,7 @@ func indexTasks() []contextTask {
PlantBody: "completion coalesce window = {{window}}\nmarker = {{marker}}",
},
{
ID: "i04-recovery-fence", Experiment: experimentIndex, TargetKind: "tool_input",
ID: "i04-recovery-fence", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleStopping,
Prompt: "recovery fence 那次检查报告的 marker 和两个 epoch 是什么?",
ProbeQuery: "recovery fence epoch check",
Vars: []varSpec{
@@ -248,7 +364,7 @@ func indexTasks() []contextTask {
PlantBody: "{{marker}}\nexpected epoch {{expected}}\nobserved epoch {{observed}}",
},
{
ID: "i05-cache-probe", Experiment: experimentIndex, TargetKind: "tool_input",
ID: "i05-cache-probe", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleStopping,
Prompt: "cache probe 当时记录的 prefix salt 和 scope 是什么?",
ProbeQuery: "cache probe prefix salt scope",
Vars: []varSpec{code("cue", "cacheprobe"), code("salt", "comet"), code("scope", "lineage")},
@@ -259,7 +375,7 @@ func indexTasks() []contextTask {
PlantBody: "prefix salt = {{salt}}\nscope = {{scope}}",
},
{
ID: "i06-dispatch-handoff", Experiment: experimentIndex, TargetKind: "tool_input",
ID: "i06-dispatch-handoff", Experiment: experimentIndex, TargetKind: "tool_input", Role: roleStopping,
Prompt: "dispatch handoff probe 当时确认的 mode 和 marker 是什么?",
ProbeQuery: "dispatch handoff probe mode",
Vars: []varSpec{code("cue", "dispatchhandoff"), code("mode", "trigger"), code("marker", "iris")},
@@ -272,6 +388,12 @@ func indexTasks() []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()...)
}
@@ -332,6 +454,9 @@ 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)
}
+14 -3
View File
@@ -5,6 +5,7 @@ import (
"crypto/rand"
"fmt"
"hash/fnv"
"io"
mathrand "math/rand"
"strings"
)
@@ -90,15 +91,25 @@ 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 {
var seed [8]byte
if _, err := rand.Read(seed[:]); err != nil {
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
}
var n int64
for _, b := range seed {
n = n<<8 | int64(b)
}
return mathrand.New(mathrand.NewSource(n))
return mathrand.New(mathrand.NewSource(n)), nil
}
// instantiate substitutes {{name}} and refuses anything left over. The grammar
+8 -1
View File
@@ -62,7 +62,14 @@ func main() {
case "adversarial":
os.Exit(runAdversarial(root))
case "run-index":
os.Exit(runExperiment(experimentIndex, root, *dry, tasks))
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))
default:
fmt.Fprintf(os.Stderr, "unknown mode %q\n", *mode)
os.Exit(2)
+69 -17
View File
@@ -1,7 +1,10 @@
package main
import (
"bytes"
"fmt"
"maps"
mathrand "math/rand"
"os/exec"
"slices"
"strings"
@@ -19,16 +22,24 @@ 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 != 6 {
t.Errorf("index experiment has %d tasks, want 6", got)
if got := len(indexTasks()); got != 12 {
t.Errorf("index experiment has %d tasks, want 12", 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]++
}
// 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.
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.
for _, tier := range []string{tierQuarter, tierHalf, tierDefault} {
if tiers[tier] != 2 {
t.Errorf("tier %s has %d tasks, want 2", tier, tiers[tier])
@@ -36,6 +47,21 @@ 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) {
@@ -59,20 +85,46 @@ func TestNoAnswerLiteralExistsInTheRepository(t *testing.T) {
}
}
// Two runs of the same task must not share an answer, or one leaked run leaks
// every future one.
func TestInstancesDifferBetweenRuns(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)))
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, err := instantiateTask(task, liveRand())
if err != nil {
t.Fatal(err)
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)
}
if slices.Equal(a.AnswerMarkers, b.AnswerMarkers) {
t.Errorf("%s produced the same answers twice: %v", task.ID, a.AnswerMarkers)
c, _ := instantiateTask(task, other)
if maps.Equal(a.Vars, c.Vars) {
t.Errorf("%s: different entropy gave the same values", task.ID)
}
}
}
+1 -1
View File
@@ -232,7 +232,7 @@ func runBoundaries(root string) int {
var all []contextMetrics
var cueSide, noCueSide []contextMetrics
for _, t := range indexTasks() {
for _, t := range efficiencyTasks() {
cue, noCue := boundaryPair(t.CueTier)
for _, side := range []struct {
scale string
@@ -0,0 +1,8 @@
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)
@@ -0,0 +1,13 @@
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
@@ -0,0 +1,4 @@
EARTH_RADIUS_KM = 6371.0
def dist(left, right):
return abs(left - right) * EARTH_RADIUS_KM
@@ -0,0 +1,4 @@
import geo
def near_by(point, other):
return geo.dist(point, other) < 10
@@ -0,0 +1,16 @@
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))))
@@ -0,0 +1,7 @@
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}
@@ -0,0 +1,7 @@
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}
@@ -0,0 +1,11 @@
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
@@ -0,0 +1,9 @@
import core
_cache = None
def list_records_cached():
global _cache
if _cache is None:
_cache = core.list_records()
return _cache
@@ -0,0 +1,5 @@
import cache
def main():
for k, v in sorted(cache.list_records_cached().items()):
print(f"{k}={v}")
@@ -0,0 +1,4 @@
DATA = {"a": 1, "b": 2}
def list_records():
return dict(DATA)
@@ -0,0 +1 @@
DEFAULTS = {"host": "localhost", "port": 8080}
@@ -0,0 +1,6 @@
import json
import defaults
def load(text):
data = json.loads(text) if text.strip() else {}
return dict(defaults.DEFAULTS, **data)
@@ -0,0 +1,4 @@
import json
def dump(cfg):
return json.dumps(cfg, sort_keys=True)
@@ -0,0 +1,2 @@
STATES = ("new", "paid", "delivered", "cancelled")
TERMINAL = ("delivered", "cancelled")
@@ -0,0 +1,11 @@
import states
LEGAL = {
"new": ("paid", "cancelled"),
"paid": ("delivered", "cancelled"),
"delivered": (),
"cancelled": (),
}
def can_move(a, b):
return b in LEGAL[a]
@@ -0,0 +1,5 @@
import schema, serializer
def display_name(line):
rec = serializer.load(line)
return rec["handle"] or rec["email"]
@@ -0,0 +1,4 @@
FIELDS = ("handle", "email")
def blank():
return {f: "" for f in FIELDS}
@@ -0,0 +1,4 @@
import timings
def is_slow(entry):
return entry["elapsed_s"] > timings.SLOW_SECONDS
@@ -0,0 +1,4 @@
import timings
def within_budget(entries):
return sum(e["elapsed_s"] for e in entries) <= timings.BUDGET_SECONDS
@@ -0,0 +1,5 @@
SLOW_SECONDS = 2.0
BUDGET_SECONDS = 60.0
def record(seconds):
return {"elapsed_s": seconds}
+3 -3
View File
@@ -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 {
t.Fatal("no solution/: a generated task must prove its grader can pass")
if dir == trainCorpusDir || dir == verificationStressDir {
t.Fatal("no solution/: a corpus task must prove its grader can pass")
}
t.Skip("no solution/ to check this grader against")
t.Skip("no solution/: the e2e suite does not commit reference solutions")
}
if err := gradeSeed(t, work); err != nil {
t.Fatalf("the reference solution does not grade clean, so no attempt can: %v", err)
+28
View File
@@ -0,0 +1,28 @@
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)
}
}
}
+46
View File
@@ -0,0 +1,46 @@
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
}
@@ -0,0 +1,92 @@
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)
}
}
+35 -11
View File
@@ -19,6 +19,7 @@ import (
"os/signal"
"strings"
"syscall"
"time"
"github.com/gorilla/websocket"
@@ -34,6 +35,7 @@ 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"
@@ -147,6 +149,7 @@ 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 {
@@ -159,6 +162,7 @@ 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
@@ -167,7 +171,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)
hub, err := assemble(ctx, logs, handshakeTo, shell, served, phases)
if err != nil {
fmt.Fprintln(logs, "reasonix-studio-host:", err)
return 1
@@ -177,6 +181,7 @@ 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
@@ -184,7 +189,10 @@ 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())
if err := announce(handshakeTo, bound); err != nil {
phases.mark(startupSetup)
err = announce(handshakeTo, bound)
phases.mark(startupAnnounce)
if err != nil {
fmt.Fprintln(logs, "reasonix-studio-host:", err)
return 1
}
@@ -281,13 +289,15 @@ 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) (*serve.Hub, error) {
func assemble(ctx context.Context, logs, handshakeTo io.Writer, shell shellIdentity, page fs.FS, phases *startupPhases) (*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
}
@@ -311,15 +321,24 @@ func assemble(ctx context.Context, logs, handshakeTo io.Writer, shell shellIdent
paneSink = reporter.Wrap(paneSink)
}
}
root := boot.ResolveWorkspaceRoot("")
phases.mark(startupSinks)
root := launchWorkspace(shell.exe)
phases.mark(startupWorkspace)
built, err := boot.BuildRuntime(ctx, boot.Options{
Version: version,
WorkspaceRoot: root,
SessionDir: serve.SessionDirFor(root),
Sink: paneSink,
Stderr: logs,
StatsSource: surface.Desktop,
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,
})
phases.mark(startupRuntime)
if err != nil {
return nil, err
}
@@ -334,6 +353,7 @@ 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()
@@ -358,12 +378,16 @@ func assemble(ctx context.Context, logs, handshakeTo io.Writer, shell shellIdent
srv := serve.New(built.Controller, bc, hubCfg)
srv.SetPaneSink(paneSink)
srv.AdoptRuntime(built)
if _, err := hub.Adopt(srv, bc); err != nil {
phases.mark(startupHub)
_, err = hub.Adopt(srv, bc)
phases.mark(startupAdopt)
if err != nil {
hub.Shutdown()
return nil, err
}
hub.StartRecoveryGC(ctx)
startCloudRemote(ctx, cfg, logs, hub)
phases.mark(startupBackground)
return hub, nil
}
+1 -1
View File
@@ -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)
hub, err := assemble(t.Context(), io.Discard, io.Discard, shellIdentity{}, nil, newStartupPhases(io.Discard, time.Now))
if err != nil {
t.Fatalf("assemble: %v", err)
}
+50
View File
@@ -0,0 +1,50 @@
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)
}
+26
View File
@@ -0,0 +1,26 @@
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)
}
}
@@ -0,0 +1,100 @@
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)
}
})
}
}
+37
View File
@@ -71,3 +71,40 @@ 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")
}
}
@@ -86,6 +86,13 @@ func TestCLIGateBlocksEveryCLIChannel(t *testing.T) {
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"}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

+1 -1
View File
@@ -19,7 +19,7 @@
"computer-live": "electron test/computer_live.js"
},
"devDependencies": {
"electron": "44.4.5",
"electron": "44.5.1",
"electron-builder": "^26.15.3",
"playwright": "1.63.0"
},
+11 -4
View File
@@ -7,11 +7,17 @@ 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; 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 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 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],
@@ -73,8 +79,9 @@ 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 at(size)]);
for (const size of ICO_SIZES) icoPngs.push([size, await render(page, winSvg, 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)]);
+5 -5
View File
@@ -9,8 +9,8 @@ importers:
.:
devDependencies:
electron:
specifier: 44.4.5
version: 44.4.5([email protected])
specifier: 44.5.1
version: 44.5.1([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.4.5:
resolution: {integrity: sha512-SjgoaeYsSWZfJzubgQU7juvuXMTvn6/e1gAHdGFA/yuMbpF+I+skYqIJ6DdXBXHeWbkbpNpmwZCTpiivtlWZSw==}
electron@44.5.1:
resolution: {integrity: sha512-lx7AYoFIiyvEvgvpyWO9kOC60xtNPpiPZAth6Fmg9NutzpCAMLwpUlaBY2EXfZyIgUdzEmLMj9KvK0Vrokl1yQ==}
engines: {node: '>= 22.12.0'}
hasBin: true
@@ -1551,7 +1551,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
electron@44.4.5([email protected]):
electron@44.5.1([email protected]):
dependencies:
'@electron-internal/extract-zip': 1.0.5
'@electron/get': 5.1.0([email protected])
+2
View File
@@ -1,6 +1,7 @@
"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
@@ -96,6 +97,7 @@ 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;
+123 -13
View File
@@ -1,21 +1,131 @@
"use strict";
// 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: roles do the clipboard work, this only decides what to offer.
function contextTemplate(params) {
if (!params.isEditable && !params.selectionText) return [];
const can = params.editFlags ?? {};
// 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", enabled: !!can.canUndo },
{ role: "redo", enabled: !!can.canRedo },
{ role: "undo", label: w.undo, ...on("canUndo") },
{ role: "redo", label: w.redo, ...on("canRedo") },
{ type: "separator" },
{ role: "cut", enabled: !!can.canCut },
{ role: "copy", enabled: !!can.canCopy },
{ role: "paste", enabled: !!can.canPaste },
{ 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", enabled: !!can.canSelectAll },
{ role: "selectAll", label: w.selectAll, ...on("canSelectAll") },
];
}
module.exports = { contextTemplate };
// 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) {
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(
{ 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 };
}
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 };
+60 -13
View File
@@ -5,22 +5,42 @@ 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 = 20000;
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);
}
}
// 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 } = {}) {
const child = spawn(binary, args, { stdio: ["pipe", "pipe", "pipe"] });
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 });
child.stderr.setEncoding("utf8");
if (onStderr) child.stderr.on("data", onStderr);
if (onExit) child.on("exit", onExit);
const ready = firstLine(child).then(({ line, rest }) => {
const ready = firstLine(child, { onSlow, timeoutMs }).then(({ line, rest }) => {
// Only once the handshake parses: a child whose first line was not one has
// said nothing this process should act on.
const handshake = parse(line);
let handshake;
try {
handshake = parse(line);
} catch (err) {
throw new HostStartError("malformed", err.message);
}
if (onAct) readActs(child, rest, onAct);
return handshake;
});
@@ -60,19 +80,31 @@ function readActs(child, rest, onAct) {
drain();
}
function firstLine(child) {
function firstLine(child, { onSlow, timeoutMs }) {
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 Error("the kernel sent no handshake")),
HANDSHAKE_TIMEOUT_MS,
() =>
settle(
reject,
new HostStartError(
"timeout",
`the kernel was still running but sent no handshake within ${Math.round(timeoutMs / 1000)} s`,
{ elapsedMs: elapsed() },
),
),
timeoutMs,
);
const onData = (chunk) => {
buffered += chunk;
@@ -81,15 +113,22 @@ function firstLine(child) {
return settle(resolve, { line: buffered.slice(0, end), rest: buffered.slice(end + 1) });
}
if (buffered.length > HANDSHAKE_LIMIT) {
settle(reject, new Error("the handshake ran past its limit"));
settle(reject, new HostStartError("overflow", "the handshake ran past its limit"));
}
};
const onEarlyExit = (code) =>
settle(reject, new Error(`the kernel exited with ${code} before saying anything`));
const onEarlyExit = (code, signal) =>
settle(
reject,
new HostStartError("exited", `the kernel exited with ${code ?? `signal ${signal}`} before saying anything`, {
exitCode: code,
signal,
elapsedMs: elapsed(),
}),
);
// 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 Error(`the kernel could not be started: ${err.message}`));
const onSpawnFailure = (err) => settle(reject, new HostStartError("spawn", `the kernel could not be started: ${err.message}`, { elapsedMs: elapsed() }));
child.stdout.setEncoding("utf8");
child.stdout.on("data", onData);
child.on("exit", onEarlyExit);
@@ -128,4 +167,12 @@ function parse(line) {
return { origin: url.origin, token: body.token };
}
module.exports = { start, parse, readActs, PROTOCOL_VERSION };
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 };
+88 -25
View File
@@ -7,15 +7,17 @@ if (relaunchForOzonePlatform(app, process)) return;
const fs = require("node:fs/promises");
const { existsSync } = require("node:fs");
const path = require("node:path");
const { start } = require("./host");
const { start, shouldRetry, HANDSHAKE_TIMEOUT_MS } = require("./host");
const { showStarting } = require("./starting");
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 } = require("./reveal");
const { reveal, revealWorkspace } = require("./reveal");
const { appIcon } = require("./appicon");
const layout = require("./layout");
const { offerCleanup } = require("./legacy");
@@ -23,7 +25,7 @@ const { stripPackageGrants, unpaintedWindowCause } = require("./packagegrants");
const { BrowserProtocol } = require("./browserprotocol");
const { BrowserViews } = require("./browserviews");
const { startBrowserRelay } = require("./browserrelay");
const { prefsFile, registerPrefs } = require("./prefs");
const { loadPrefs, 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
@@ -45,6 +47,7 @@ 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,
@@ -70,6 +73,70 @@ 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.
@@ -86,23 +153,7 @@ async function boot() {
// inside the bundle rather than being it.
args.push("-studio-app", process.execPath, "-studio-app-pid", String(process.pid));
}
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;
const ready = await launchKernel(args);
logs.addSecret(ready.token);
handshaken = true;
logs.shell.line(`host: handshake from ${ready.origin}`);
@@ -110,13 +161,14 @@ 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);
installContextMenu(win.webContents, win, uiLang);
installFullScreenKey(win.webContents, win);
reload = installReload(win.webContents, win);
win.once("ready-to-show", () => win.show());
@@ -201,6 +253,7 @@ 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();
@@ -263,7 +316,10 @@ function fromWindow(event) {
return win && !win.isDestroyed() && event.sender === win.webContents ? win : null;
}
registerPrefs(ipcMain, () => prefsFile(app.getPath("userData")), fromWindow);
// 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));
ipcMain.handle("window:minimise", (event) => {
fromWindow(event)?.minimize();
@@ -300,8 +356,11 @@ 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) void shell.openExternal(target);
if (target) return 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" },
);
@@ -377,7 +436,7 @@ if (!primary) {
app.whenReady().then(() => {
if (!primary) return;
installApplicationMenu();
installApplicationMenu(uiLang);
boot().catch((err) => {
console.error("reasonix-studio:", err.message);
if (quitting) {
@@ -423,7 +482,11 @@ function handOver(act) {
if (act === "quit") app.quit();
}
app.on("window-all-closed", () => 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();
});
// 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.
+4 -16
View File
@@ -1,31 +1,19 @@
"use strict";
const { Menu } = require("electron");
const { contextTemplate } = require("./editmenu");
const { contextTemplate, menuInstaller } = 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.
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;
}
const installApplicationMenu = menuInstaller(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) {
function installContextMenu(contents, window, languageOf = () => "en") {
contents.on("context-menu", (_event, params) => {
const template = contextTemplate(params);
const template = contextTemplate(params, languageOf());
if (!template.length) return;
Menu.buildFromTemplate(template).popup({ window });
});
+2 -1
View File
@@ -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) {
function registerPrefs(ipcMain, fileOf, fromWindow, onSave = () => {}) {
ipcMain.on("prefs:load", (event) => {
event.returnValue = fromWindow(event) ? loadPrefs(fileOf()) : {};
});
@@ -52,6 +52,7 @@ function registerPrefs(ipcMain, fileOf, fromWindow) {
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.
+2
View File
@@ -28,6 +28,8 @@ 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.
+26 -10
View File
@@ -1,22 +1,38 @@
"use strict";
// 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.
// 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.
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 (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;
if (reloadAction(input, platform) !== "hard") return;
event.preventDefault();
contents.reload();
});
@@ -40,4 +56,4 @@ function installReload(contents, window, { platform = process.platform, now = Da
};
}
module.exports = { installReload, CRASH_RELOADS, CRASH_WINDOW_MS };
module.exports = { installReload, installPaneReload, reloadAction, CRASH_RELOADS, CRASH_WINDOW_MS };
+15 -2
View File
@@ -23,9 +23,13 @@ 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", `${base}/workspace/locate${query}`);
res = await client.request("GET", url);
} catch (err) {
return { refusal: refused(err.message) };
}
@@ -54,4 +58,13 @@ async function reveal(client, shell, base, rel, platform = process.platform) {
return null;
}
module.exports = { reveal };
// 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 };
+25 -12
View File
@@ -159,17 +159,29 @@ function openLogs(userData, options = {}) {
};
}
function startupFailure(locale, reason, logDir) {
if (String(locale).toLowerCase().startsWith("zh")) {
return {
title: "Reasonix Studio 无法启动",
detail: `${reason}\n\n日志位于:\n${logDir}`,
};
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}` };
}
return {
title: "Reasonix Studio could not start",
detail: `${reason}\n\nLogs are in:\n${logDir}`,
};
return { title: "Reasonix Studio could not start", detail: `${body}\n\nLogs are in:\n${logDir}` };
}
// failStartup is the one way a launch that cannot come up ends: recorded, told
@@ -177,8 +189,9 @@ function startupFailure(locale, reason, logDir) {
// 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 reason = [String(err?.message || err), hostOutput].filter(Boolean).join("\n\n");
const text = startupFailure(locale, logs.redact(reason), logs.dir);
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);
try {
dialog.showErrorBox(text.title, text.detail);
} catch (e) {
+41
View File
@@ -0,0 +1,41 @@
"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 };
+7 -4
View File
@@ -1,18 +1,17 @@
"use strict";
const path = require("node:path");
const { Menu, Tray, nativeImage } = require("electron");
const { Menu, Tray, nativeImage, screen } = require("electron");
const { trayImage } = require("./trayimage.js");
// 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 = nativeImage.createFromPath(ICON);
const image = trayImage(nativeImage, screen.getPrimaryDisplay().scaleFactor);
if (image.isEmpty()) return null;
const icon = new Tray(image);
const state = { fold: null, prefs: null };
@@ -47,6 +46,9 @@ 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);
@@ -68,6 +70,7 @@ function installTray(host, { onOpen, onQuit }) {
refresh,
close() {
clearInterval(timer);
screen.removeListener("display-metrics-changed", refit);
icon.destroy();
},
};
+30
View File
@@ -0,0 +1,30 @@
"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 };
+17
View File
@@ -0,0 +1,17 @@
"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 };
+3 -3
View File
@@ -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", "editmenu", "windowmenu"].every((r) => roles.includes(r)), roles);
check("it carries the three menus macOS routes through", ["appmenu", "windowmenu"].every((r) => roles.includes(r)) && menu.items.length === 3, roles);
const edit = (menu?.items ?? []).find((i) => String(i.role).toLowerCase() === "editmenu");
const edit = (menu?.items ?? [])[1];
const editRoles = (edit?.submenu?.items ?? []).map((i) => String(i.role).toLowerCase());
check("the edit menu carries every editing command", ["undo", "redo", "cut", "copy", "paste", "selectall"].every((r) => editRoles.includes(r)), editRoles);
check("the edit menu carries every editing command", ["undo", "redo", "cut", "copy", "paste", "pasteandmatchstyle", "delete", "selectall"].every((r) => editRoles.includes(r)), editRoles);
app.focus({ steal: true });
win.focus();
+256 -28
View File
@@ -6,8 +6,9 @@ import os from "node:os";
import fs from "node:fs";
const require = createRequire(import.meta.url);
const { parse, readActs } = require("../src/host.js");
const { contextTemplate } = require("../src/editmenu.js");
const { parse, readActs, start } = require("../src/host.js");
const { contextTemplate, editMenuTemplate, applicationMenuTemplate, menuInstaller } = require("../src/editmenu.js");
const { uiLanguage } = require("../src/uilang.js");
const { externalTarget } = require("../src/links.js");
const { offerCleanup, ownBundle } = require("../src/legacy.js");
const { stripPackageGrants, readReport, unpaintedWindowCause } = require("../src/packagegrants.js");
@@ -16,6 +17,23 @@ 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 });
@@ -63,6 +81,68 @@ 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/");
@@ -124,7 +204,7 @@ test("an unreachable kernel is an answer, not a crash", async () => {
assert.equal(await dead.trayState(), null);
});
const { reveal } = require("../src/reveal.js");
const { reveal, revealWorkspace } = 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.
@@ -143,7 +223,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), close: () => server.close() };
return { asked, opened, run: (base, rel) => reveal(client, shell, base, rel, platform), workspace: (root) => revealWorkspace(client, shell, root, platform), close: () => server.close() };
}
const ROOT = path.resolve(os.tmpdir(), "rx-workspace");
@@ -194,6 +274,22 @@ 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 () => {
@@ -314,6 +410,79 @@ 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.
@@ -849,30 +1018,67 @@ function reloadRig(platform = "darwin") {
return { state, send, gone, revive };
}
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 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) {
const { state, send } = reloadRig(platform);
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);
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`);
}
});
@@ -1104,6 +1310,7 @@ 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;
@@ -1163,8 +1370,29 @@ 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: Error: /);
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");
} 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);
});
+4 -4
View File
@@ -29,9 +29,9 @@
"@codemirror/search": "^6.7.2",
"@codemirror/state": "^6.7.6",
"@codemirror/view": "^6.43.13",
"@lezer/highlight": "^1.2.4",
"@lezer/highlight": "^1.2.5",
"codemirror": "^6.0.2",
"katex": "^0.18.9",
"katex": "^0.18.10",
"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.0",
"vitest": "^5.0.1"
"vite": "^8.3.2",
"vitest": "^5.0.3"
}
}
+1
View File
@@ -46,6 +46,7 @@ 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 # 跟随系统语言:中文各写法都归中文,其余归英文
+5 -2
View File
@@ -4,6 +4,7 @@ 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";
@@ -44,11 +45,12 @@ class BenchPort extends MockPort {
}
async providerSetup() {
return null;
return ONBOARDING ? { required: true, provider: "", model: "" } : null;
}
async appearance() {
const look = await super.appearance();
let look = await super.appearance();
if (query.has("zoom")) look = { ...look, zoom: Number(query.get("zoom")) };
return PREF === null ? look : { ...look, language: PREF };
}
@@ -148,6 +150,7 @@ 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);
+108
View File
@@ -0,0 +1,108 @@
// 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);
+125 -138
View File
@@ -60,14 +60,14 @@ importers:
specifier: ^6.43.13
version: 6.43.13
'@lezer/highlight':
specifier: ^1.2.4
version: 1.2.4
specifier: ^1.2.5
version: 1.2.5
codemirror:
specifier: ^6.0.2
version: 6.0.2
katex:
specifier: ^0.18.9
version: 0.18.9
specifier: ^0.18.10
version: 0.18.10
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].0([email protected]))
version: 6.1.1([email protected].2([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.0
version: 8.3.0([email protected])
specifier: ^8.3.2
version: 8.3.2([email protected])
vitest:
specifier: ^5.0.1
version: 5.0.1([email protected])([email protected].0([email protected]))
specifier: ^5.0.3
version: 5.0.3([email protected])([email protected].2([email protected]))
packages:
@@ -453,8 +453,8 @@ packages:
'@lezer/[email protected]':
resolution: {integrity: sha512-xToRsYxwsgJNHTgNdStpcvmbVuKxTapV0dM0wey1geMMRc9aggoVyKgzYp41D2/vVOx+Ii4hmE206kvxIXBVXQ==}
'@lezer/[email protected].4':
resolution: {integrity: sha512-4UeRVFO0lsBaM7GA32zOqbmqNPme9jedo8Ey+5c5656MU3MLitMLIA8aCZTYjUMEpEsuSeXEm78Uea6Iq3bvSg==}
'@lezer/[email protected].5':
resolution: {integrity: sha512-O1GMVKgtf5YspFaRzpmqkVgMtIk2HG9uMAbH6wdAIdNh3TVzCsch/+cRi+6l2//UGDKe6NB9BW3cvuR5ezMQ0w==}
'@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]47.0':
resolution: {integrity: sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==}
'@oxc-project/[email protected]52.0':
resolution: {integrity: sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==}
'@rolldown/[email protected].6':
resolution: {integrity: sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==}
'@rolldown/[email protected].12':
resolution: {integrity: sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [android]
'@rolldown/[email protected].6':
resolution: {integrity: sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==}
'@rolldown/[email protected].12':
resolution: {integrity: sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/[email protected].6':
resolution: {integrity: sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==}
'@rolldown/[email protected].12':
resolution: {integrity: sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/[email protected].6':
resolution: {integrity: sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==}
'@rolldown/[email protected].12':
resolution: {integrity: sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/[email protected].6':
resolution: {integrity: sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==}
'@rolldown/[email protected].12':
resolution: {integrity: sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/[email protected].6':
resolution: {integrity: sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==}
'@rolldown/[email protected].12':
resolution: {integrity: sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/[email protected].6':
resolution: {integrity: sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==}
'@rolldown/[email protected].12':
resolution: {integrity: sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/[email protected].6':
resolution: {integrity: sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==}
'@rolldown/[email protected].12':
resolution: {integrity: sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/[email protected].6':
resolution: {integrity: sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==}
'@rolldown/[email protected].12':
resolution: {integrity: sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/[email protected].6':
resolution: {integrity: sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==}
'@rolldown/[email protected].12':
resolution: {integrity: sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/[email protected].6':
resolution: {integrity: sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==}
'@rolldown/[email protected].12':
resolution: {integrity: sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/[email protected].6':
resolution: {integrity: sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==}
'@rolldown/[email protected].12':
resolution: {integrity: sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/[email protected].6':
resolution: {integrity: sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==}
'@rolldown/[email protected].12':
resolution: {integrity: sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/[email protected].6':
resolution: {integrity: sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==}
'@rolldown/[email protected].12':
resolution: {integrity: sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/[email protected].6':
resolution: {integrity: sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==}
'@rolldown/[email protected].12':
resolution: {integrity: sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==}
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].1':
resolution: {integrity: sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==}
'@vitest/[email protected].3':
resolution: {integrity: sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==}
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].1':
resolution: {integrity: sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==}
'@vitest/[email protected].3':
resolution: {integrity: sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==}
[email protected]:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
@@ -1051,8 +1051,8 @@ packages:
resolution: {integrity: sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==}
hasBin: true
[email protected].9:
resolution: {integrity: sha512-8ad9RyoKsb/g8/yLFE+KAlP+DhbCTRUNi/V9XGsxn0R+trJJltNwzcDNo0q/DEkOy5fQUQTAQyCXCYSE+OakTQ==}
[email protected].10:
resolution: {integrity: sha512-/B6p9eY9DX7aHBfpkHdpirDTZ5QH9xTwL9y837PWuzuv41O5jFdqNQwVQEQsimY0/iVNkwBY4+4hMfhQ7d4Dbw==}
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].6:
resolution: {integrity: sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==}
[email protected].12:
resolution: {integrity: sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
@@ -1408,9 +1408,6 @@ 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'}
@@ -1418,9 +1415,6 @@ packages:
[email protected]:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
[email protected]:
resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==}
[email protected]:
resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==}
@@ -1511,8 +1505,8 @@ packages:
[email protected]:
resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==}
[email protected].0:
resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==}
[email protected].2:
resolution: {integrity: sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
@@ -1554,20 +1548,20 @@ packages:
yaml:
optional: true
[email protected].1:
resolution: {integrity: sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==}
[email protected].3:
resolution: {integrity: sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==}
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.1
'@vitest/browser-preview': 5.0.1
'@vitest/browser-playwright': 5.0.3
'@vitest/browser-preview': 5.0.3
'@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0
'@vitest/coverage-istanbul': 5.0.1
'@vitest/coverage-v8': 5.0.1
'@vitest/ui': 5.0.1
'@vitest/coverage-istanbul': 5.0.3
'@vitest/coverage-v8': 5.0.3
'@vitest/ui': 5.0.3
happy-dom: '*'
jsdom: '*'
vite: ^6.4.0 || ^7.0.0 || ^8.0.0
@@ -1621,9 +1615,9 @@ packages:
resolution: {integrity: sha512-TEZA+Zqxin7Jjsm2cjRohCmen5awh+hT6Zi3VZdqZlNRk7zvOI/9WpBFg/DWlA56bWnzwm6DuB8NS0EsxQH9uQ==}
engines: {node: ^22.14.0 || >=24.0.0}
why-is-node-running@2.3.0:
resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==}
engines: {node: '>=8'}
why-is-node-running@3.2.1:
resolution: {integrity: sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==}
engines: {node: '>=20.11'}
hasBin: true
[email protected]:
@@ -1771,7 +1765,7 @@ snapshots:
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.6
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.4
'@lezer/highlight': 1.2.5
'@lezer/lr': 1.4.10
'@codemirror/[email protected]':
@@ -1789,7 +1783,7 @@ snapshots:
'@codemirror/language': 6.12.4
'@codemirror/state': 6.7.6
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.4
'@lezer/highlight': 1.2.5
'@lezer/lr': 1.4.10
'@lezer/yaml': 1.0.4
@@ -1798,7 +1792,7 @@ snapshots:
'@codemirror/state': 6.7.6
'@codemirror/view': 6.43.13
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.4
'@lezer/highlight': 1.2.5
'@lezer/lr': 1.4.10
style-mod: 4.1.4
@@ -1943,41 +1937,41 @@ snapshots:
'@lezer/[email protected]':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.4
'@lezer/highlight': 1.2.5
'@lezer/lr': 1.4.10
'@lezer/[email protected]':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.4
'@lezer/highlight': 1.2.5
'@lezer/lr': 1.4.10
'@lezer/[email protected].4':
'@lezer/[email protected].5':
dependencies:
'@lezer/common': 1.5.2
'@lezer/[email protected]':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.4
'@lezer/highlight': 1.2.5
'@lezer/lr': 1.4.10
'@lezer/[email protected]':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.4
'@lezer/highlight': 1.2.5
'@lezer/lr': 1.4.10
'@lezer/[email protected]':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.4
'@lezer/highlight': 1.2.5
'@lezer/lr': 1.4.10
'@lezer/[email protected]':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.4
'@lezer/highlight': 1.2.5
'@lezer/lr': 1.4.10
'@lezer/[email protected]':
@@ -1987,79 +1981,79 @@ snapshots:
'@lezer/[email protected]':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.4
'@lezer/highlight': 1.2.5
'@lezer/[email protected]':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.4
'@lezer/highlight': 1.2.5
'@lezer/lr': 1.4.10
'@lezer/[email protected]':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.4
'@lezer/highlight': 1.2.5
'@lezer/lr': 1.4.10
'@lezer/[email protected]':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.4
'@lezer/highlight': 1.2.5
'@lezer/lr': 1.4.10
'@lezer/[email protected]':
dependencies:
'@lezer/common': 1.5.2
'@lezer/highlight': 1.2.4
'@lezer/highlight': 1.2.5
'@lezer/lr': 1.4.10
'@marijn/[email protected]': {}
'@oxc-project/[email protected]47.0': {}
'@oxc-project/[email protected]52.0': {}
'@rolldown/[email protected].6':
'@rolldown/[email protected].12':
optional: true
'@rolldown/[email protected].6':
'@rolldown/[email protected].12':
optional: true
'@rolldown/[email protected].6':
'@rolldown/[email protected].12':
optional: true
'@rolldown/[email protected].6':
'@rolldown/[email protected].12':
optional: true
'@rolldown/[email protected].6':
'@rolldown/[email protected].12':
optional: true
'@rolldown/[email protected].6':
'@rolldown/[email protected].12':
optional: true
'@rolldown/[email protected].6':
'@rolldown/[email protected].12':
optional: true
'@rolldown/[email protected].6':
'@rolldown/[email protected].12':
optional: true
'@rolldown/[email protected].6':
'@rolldown/[email protected].12':
optional: true
'@rolldown/[email protected].6':
'@rolldown/[email protected].12':
optional: true
'@rolldown/[email protected].6':
'@rolldown/[email protected].12':
optional: true
'@rolldown/[email protected].6':
'@rolldown/[email protected].12':
optional: true
'@rolldown/[email protected].6':
'@rolldown/[email protected].12':
optional: true
'@rolldown/[email protected].6':
'@rolldown/[email protected].12':
optional: true
'@rolldown/[email protected].6':
'@rolldown/[email protected].12':
optional: true
'@rolldown/[email protected]': {}
@@ -2194,21 +2188,21 @@ snapshots:
'@ungap/[email protected]': {}
'@vitejs/[email protected]([email protected].0([email protected]))':
'@vitejs/[email protected]([email protected].2([email protected]))':
dependencies:
'@rolldown/pluginutils': 1.0.1
vite: 8.3.0([email protected])
vite: 8.3.2([email protected])
'@vitest/[email protected].1([email protected].0([email protected]))':
'@vitest/[email protected].3([email protected].2([email protected]))':
dependencies:
'@jridgewell/trace-mapping': 0.3.31
'@vitest/spy': 5.0.1
'@vitest/spy': 5.0.3
estree-walker: 3.0.3
magic-string: 1.4.2
optionalDependencies:
vite: 8.3.0([email protected])
vite: 8.3.2([email protected])
'@vitest/[email protected].1': {}
'@vitest/[email protected].3': {}
[email protected]: {}
@@ -2519,7 +2513,7 @@ snapshots:
dependencies:
commander: 8.3.0
[email protected].9:
[email protected].10:
dependencies:
commander: 15.0.0
@@ -3117,26 +3111,26 @@ snapshots:
[email protected]: {}
[email protected].6:
[email protected].12:
dependencies:
'@oxc-project/types': 0.147.0
'@oxc-project/types': 0.152.0
'@rolldown/pluginutils': 1.0.1
optionalDependencies:
'@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
'@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
[email protected]:
dependencies:
@@ -3144,14 +3138,10 @@ snapshots:
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]: {}
[email protected]:
@@ -3279,21 +3269,21 @@ snapshots:
'@types/unist': 3.0.3
vfile-message: 4.0.3
[email protected].0([email protected]):
[email protected].2([email protected]):
dependencies:
lightningcss: 1.33.0
picomatch: 4.0.7
postcss: 8.5.28
rolldown: 1.2.6
rolldown: 1.2.12
tinyglobby: 0.2.17
optionalDependencies:
esbuild: 0.28.2
fsevents: 2.3.3
[email protected].1([email protected])([email protected].0([email protected])):
[email protected].3([email protected])([email protected].2([email protected])):
dependencies:
'@types/chai': 5.2.3
'@vitest/mocker': 5.0.1([email protected].0([email protected]))
'@vitest/mocker': 5.0.3([email protected].2([email protected]))
chai: 6.2.2
es-module-lexer: 2.3.2
expect-type: 1.4.0
@@ -3304,8 +3294,8 @@ snapshots:
tinybench: 6.1.4
tinyexec: 1.3.0
tinyglobby: 0.2.17
vite: 8.3.0([email protected])
why-is-node-running: 2.3.0
vite: 8.3.2([email protected])
why-is-node-running: 3.2.1
optionalDependencies:
jsdom: 30.1.1
transitivePeerDependencies:
@@ -3339,10 +3329,7 @@ snapshots:
transitivePeerDependencies:
- '@noble/hashes'
why-is-node-running@2.3.0:
dependencies:
siginfo: 2.0.0
stackback: 0.0.2
why-is-node-running@3.2.1: {}
[email protected]: {}
+65
View File
@@ -81,6 +81,7 @@ 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" },
@@ -95,6 +96,10 @@ 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
@@ -109,6 +114,15 @@ 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
@@ -151,6 +165,37 @@ 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" },
@@ -177,6 +222,7 @@ 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" },
@@ -196,6 +242,8 @@ 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" },
@@ -211,12 +259,16 @@ 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" },
@@ -254,6 +306,7 @@ 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" },
@@ -268,6 +321,8 @@ 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
@@ -303,6 +358,7 @@ 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" },
@@ -314,6 +370,7 @@ 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" },
@@ -342,6 +399,11 @@ 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
@@ -420,6 +482,9 @@ 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" },
Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 1.6 KiB

@@ -0,0 +1,141 @@
[
"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"
]
@@ -0,0 +1,31 @@
// 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 = "没有值得折叠的内容";
+89 -1
View File
@@ -16,16 +16,20 @@ 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,
@@ -67,6 +71,7 @@ export const EN: Record<string, string> = {
"复制": "Copy",
"已复制": "Copied",
"复制不了": "Copy blocked",
"复制失败,请重试": "Copy failed. Try again.",
"复制这段回答": "Copy this answer",
"复制这段代码": "Copy this code",
"复制路径": "Copy path",
@@ -276,6 +281,10 @@ 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",
@@ -331,6 +340,11 @@ 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}",
@@ -532,7 +546,9 @@ export const EN: Record<string, string> = {
"编辑": "Edit",
"测试中…": "Testing…",
"测试连接": "Test it",
"没保存成功": "Not saved",
"保存失败": "Save failed",
"已保存,尚未生效": "Saved, not applied yet",
"刷新模型目录失败": "Could not refresh the model catalog",
"重新问一次有哪些模型": "Ask for the model list again",
"刷新模型目录": "Refresh model catalog",
"正在刷新…": "Refreshing…",
@@ -771,6 +787,7 @@ 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",
@@ -795,6 +812,7 @@ 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.",
@@ -819,6 +837,7 @@ 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",
@@ -1038,6 +1057,7 @@ 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",
@@ -1090,6 +1110,8 @@ 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",
@@ -1102,6 +1124,11 @@ 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",
@@ -1196,6 +1223,64 @@ 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 可以调用,每次调用照常经过权限确认":
@@ -1213,5 +1298,8 @@ 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,6 +21,9 @@ 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…",
@@ -0,0 +1,20 @@
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,6 +3,8 @@
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",
@@ -21,6 +23,7 @@ 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",
@@ -66,6 +69,7 @@ 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",
@@ -0,0 +1,133 @@
// 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.",
};
+51 -1
View File
@@ -3,6 +3,35 @@
// 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",
// 能力开关:名字、这台机器的存档、以及服务器自己
@@ -119,7 +148,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",
"名称只能包含字母、数字、点、连字符和下划线": "A name may use letters, digits, dot, dash and underscore only",
"名称只能用字母、数字、点、连字符和下划线,以字母或数字开头,最长 64 个字符": "Use only letters, digits, dots, hyphens and underscores, starting with a letter or digit, up to 64 characters",
"已经有名为「{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",
@@ -147,17 +176,34 @@ 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",
@@ -196,6 +242,7 @@ 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",
@@ -228,6 +275,7 @@ 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}",
@@ -285,6 +333,8 @@ 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,6 +131,25 @@ 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",
@@ -155,6 +174,8 @@ 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}",
+95 -2
View File
@@ -187,6 +187,20 @@ 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 属于另一条路径,本规则不覆盖":
@@ -268,6 +282,14 @@ 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.",
@@ -324,6 +346,7 @@ 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.",
@@ -409,12 +432,48 @@ 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",
@@ -451,6 +510,7 @@ export const EN_SETTINGS: Record<string, string> = {
" · 缺 key": " · no key",
"安装这个版本": "Install this version",
"保存中…": "Saving…",
"无法保存": "Could not save",
"不发送": "Do not send",
"当前版本": "Current version",
"读取中…": "Reading…",
@@ -595,8 +655,11 @@ 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",
"装好了,但这一轮还在跑:等它结束或新建会话后生效":
"Installed, but this turn is still running: it takes effect when the turn ends, or in a new session",
"装好了,但运行时未重载:{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.",
"没装上": "Not installed",
"这个地址上已经有「{name}」了。留空 key": "This address already holds “{name}”. Leave the key blank",
"已就位 · {n} 个工具,下一轮就能用": "In place · {n} tools, ready next turn",
@@ -627,6 +690,8 @@ 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.",
@@ -740,8 +805,30 @@ 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",
@@ -750,6 +837,8 @@ 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",
@@ -765,6 +854,8 @@ 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",
@@ -773,8 +864,10 @@ 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",
+5 -3
View File
@@ -9,7 +9,9 @@ export const EN_STORAGE: Record<string, string> = {
"选择旧版数据目录…": "Choose older data folder…",
"正在扫描…": "Scanning…",
"已找回 {n} 个会话。": "Recovered {n} sessions.",
"没有发现尚未导入的旧会话。": "No sessions remain to be imported.",
"没有发现尚未导入的旧会话。": "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).",
"有 {n} 项无法读取。": "{n} items could not be read.",
"未能扫描这个文件夹。请确认它是旧版 Reasonix 的数据目录。": "This folder could not be scanned. Make sure it is an older Reasonix data folder.",
存储: "Storage",
@@ -21,8 +23,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 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.",
"以下内容仍位于 {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.",
位置: "Locations",
迁移: "Migrate",
"迁移未能启动。": "The move could not be started.",
@@ -22,11 +22,30 @@ 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",
@@ -54,6 +73,7 @@ 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,6 +14,12 @@ 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,6 +24,17 @@ 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("该条已不在待送达队列中");
});
@@ -31,6 +42,14 @@ 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"));
+63 -2
View File
@@ -18,9 +18,19 @@ 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"];
export const SAVED_NOT_APPLIED: readonly string[] = ["provider.saved_while_running", "provider.saved_model_unlisted", "runtime.rebuild_failed", "runtime.saved_while_running"];
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": "任务正在运行,请先停止再切换模型",
@@ -42,7 +52,7 @@ const SAID: Record<string, string> = {
// ── 来源:填错了什么 ─────────────────────────────────────────────
"provider.name_required": "请为该来源填写名称",
"provider.name_invalid": "名称只能包含字母、数字、点、连字符和下划线",
"provider.name_invalid": "名称只能用字母、数字、点、连字符和下划线,以字母或数字开头,最长 64 个字符",
"provider.name_taken": "已经有名为「{name}」的连接了,换一个名称",
"provider.config_unreadable": "读不到配置文件,没法安全地选择密钥存放位置,请检查配置后重试",
"provider.endpoint_required": "请填写接口地址",
@@ -68,6 +78,7 @@ 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": "这台服务器未开放权限编辑",
@@ -104,6 +115,7 @@ 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} 个",
@@ -125,6 +137,7 @@ 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": "该协议不支持由端点自行搜索",
@@ -136,6 +149,8 @@ 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": "这个地址不是本机共享的地址,请重新扫码。",
@@ -148,15 +163,30 @@ 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": "该会话未启用记忆",
@@ -188,6 +218,21 @@ 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": "该会话正在清理,请稍后再打开",
@@ -263,6 +308,8 @@ 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}",
@@ -327,6 +374,20 @@ 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": "不存在该待回答的问题",
+11 -3
View File
@@ -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,6 +16,14 @@ 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: "模型卡在重复输出同一段文字,这一轮已停止;可以重试、补充引导,或换一个供应商/模型",
};
@@ -0,0 +1,9 @@
// 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: "队列记录损坏,这一条是从残留文件里找回的",
};
@@ -0,0 +1,25 @@
// @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("");
});
}
});
@@ -0,0 +1,8 @@
import type { WorkspaceLease } from "../port/wire";
import { t } from "./index";
export function workspaceLeaseDetail(claim: WorkspaceLease, showHolder = true): string {
return [showHolder && (claim.holder || claim.holderSessionId) ? t("会话 {holder}({session})持有写入范围:{paths}", {
holder: claim.holder ?? "", session: claim.holderSessionId ?? "", paths: JSON.stringify(claim.paths ?? []),
}) : "", claim.requestedPaths?.length ? t("所需写入范围:{paths}", { paths: JSON.stringify(claim.requestedPaths) }) : ""].filter(Boolean).join(" · ");
}
Loaded 100 of 1375 files, more files were not shown because too many files have changed in this diff. Show more