feat(desktop): the Connection and Account pages are the board's cards of single rows

Set-connect draws network, and other machines (remote, phone access) as one
row each behind a pill; Set-about draws account, version and diagnostics
(version, logs, trace export) and a short About note, with community and
sponsor behind pills. Landing on a setting that is its own page now targets the
page itself, and the section counts leave out blocks drawn inside another page.
This commit is contained in:
esengine committed 2026-11-05 05:02:44 +00:00
1 parent af75bc400f
commit bf6ff1be7f
17 files changed
+217 -99

No files matched your search

@@ -4,7 +4,6 @@ export const ABOUT: Messages = {
"about.a_newer_version": { zh: "更新的版本", en: "A newer version" },
"about.a_rollback_pins_the_version": { zh: "回退时会固定在所选版本,免得新版本提示把你带回刚离开的版本。", en: "A rollback pins the version you chose, so an update prompt does not take you back to the one you left." },
"about.a_session_written_by_a": { zh: "较新版本写入的会话在旧版本中暂时无法打开,升级回去后即可恢复。", en: " A session written by a newer version will not open on an older one until you upgrade again." },
"about.after_a_problem_or_a": { zh: "出现问题或闪退后,反馈时附上这个目录里的文件会更容易查清原因;这里只显示位置,不会上传任何内容。", en: "After a problem or a crash, attaching the files in this folder to a report makes the cause easier to find. This only shows the location; nothing is uploaded." },
"about.after_the_restart_current_is": { zh: "重启后运行的仍是 {current}:新版本没能替换已安装的文件,已恢复原版本。常见原因是装在 Program Files 或其他磁盘,或文件被安全软件占用。下载完整安装包安装一次即可,之后的更新会恢复正常。", en: "After the restart {current} is still running: the new version could not replace the installed files, so the previous one was restored. This usually means Studio is installed under Program Files or on another drive, or security software held a file. Install the full package once and updates work again after that." },
"about.an_earlier_version": { zh: "更早的版本", en: "An earlier version" },
"about.cancel_download": { zh: "取消下载", en: "Cancel download" },
@@ -71,7 +70,6 @@ export const ABOUT: Messages = {
"about.the_download_connection_delivered_nothing": { zh: "下载连接长时间没有数据,重连几次仍未恢复。当前版本未被改动,可以稍后重试,或下载完整安装包手动安装。", en: "The download connection delivered nothing for a long time and several reconnects did not help. The running version was not touched; try again later, or download the full installer." },
"about.the_downloaded_file_does_not": { zh: "下载到的文件与发布签名不符,已丢弃,没有安装。多为下载损坏或网络被篡改,请重试;反复出现请反馈。", en: "The downloaded file does not match the release signature, so it was discarded and nothing was installed. This is usually a corrupted download or a tampered network; try again, and report it if it keeps happening." },
"about.the_install_folder_cannot_have": { zh: "安装目录无法就地替换文件,本次改为下载完整安装包。", en: "The install folder cannot have its files replaced in place, so this update downloads the full package." },
"about.the_installed_version_available_updates": { zh: "当前安装的版本、可用更新,以及出现问题时如何回退。更新下载好后由你决定何时重启;回退后会固定在所选版本,不再提示新版本。", en: "The installed version, available updates, and how to go back if something breaks. A downloaded update waits for you to choose when to restart; after a rollback the chosen version is pinned and newer ones stop being offered." },
"about.the_last_update_to_v": { zh: "上次更新到 {v} 没有生效", en: "The last update to {v} did not take effect" },
"about.the_running_version_was_not": { zh: "当前版本未被改动,可以重试。", en: "The running version was not touched; you can try again." },
"about.the_system_or_security_software": { zh: "安装程序被系统或安全软件拦下了。当前版本未被改动,可以重试,或下载完整安装包手动安装。", en: "The system or security software stopped the installer. The running version was not touched; try again, or install the full package by hand." },
@@ -118,7 +118,6 @@ export const DATA: Messages = {
"data.same_as_this_machine": { zh: "与本机相同", en: "Same as this machine" },
"data.scanning": { zh: "正在扫描…", en: "Scanning…" },
"data.search_indexes_and_derived_data": { zh: "搜索索引与派生数据,删除后会自动重建", en: "Search indexes and derived data; deleting them rebuilds them" },
"data.send_each_turn_to_your": { zh: "把每轮对话以 OpenTelemetry 追踪的形式发给你自己的观测系统。默认关闭;只发送结构、耗时、数量和名称,不发送提示词、工具参数、结果或文件路径。", en: "Send each turn to your own observability stack as OpenTelemetry traces. Off by default; only structure, timing, counts and names are sent, never prompts, tool arguments, results or file paths." },
"data.service_name": { zh: "服务名", en: "Service name" },
"data.session_imported_with_its_original": { zh: "会话已导入,绑定版本保持不变。", en: "Session imported with its original composition version." },
"data.session_storage": { zh: "会话存储", en: "Session storage" },
+18 -3
View File
@@ -36,7 +36,6 @@ export const SETTINGS: Messages = {
"settings.common": { zh: "常用", en: "Common" },
"settings.completion_standard": { zh: "完成标准", en: "Completion standard" },
"settings.confirm_sign_out": { zh: "确认退出", en: "Confirm sign-out" },
"settings.connect_a_workspace_on_another": { zh: "接入另一台机器上的工作区:内核在远程运行,界面仍在本机。远程面板与本地面板并排显示,每处都会标明所在的机器。", en: "Connect a workspace on another machine: the kernel runs there, the window stays here. Remote panes sit alongside local ones, so each place states which machine it belongs to." },
"settings.context_maintenance": { zh: "上下文维护", en: "Context maintenance" },
"settings.controls_and_performance": { zh: "操作与性能", en: "Controls and performance" },
"settings.conversation_display": { zh: "对话显示", en: "Conversation display" },
@@ -87,7 +86,6 @@ export const SETTINGS: Messages = {
"settings.memory_usage_storage_backup": { zh: "记忆、用量、存储、备份", en: "Memory, usage, storage, backup" },
"settings.messages_sent_while_running": { zh: "运行中发送消息", en: "Messages sent while running" },
"settings.model_discovery_is_off_only": { zh: "模型自动发现已关闭:仅显式指定的技能会运行。开关立即生效,自下一条消息起,无需新建会话。", en: "Model discovery is off: only the skills you name by hand will run. The switch takes effect at once, from your next message — no new session needed." },
"settings.model_requests_remote_mcp_services": { zh: "模型请求、MCP 远程服务和网页抓取都经由此处。配置错误通常表现为聊天无响应,建议先测试连接,测试会指出中断的环节。", en: "Model requests, remote MCP services and web fetches all go through here. A wrong setting usually shows up as a chat that simply stops responding, so test the connection first — the test names the step that fails." },
"settings.models_by_use": { zh: "按用途选择模型", en: "Models by use" },
"settings.move_between_your_questions": { zh: "在提问之间移动", en: "Move between your questions" },
"settings.move_down_name": { zh: "下移:{name}", en: "Move down: {name}" },
@@ -136,7 +134,6 @@ export const SETTINGS: Messages = {
"settings.press_a_modifier_and_key": { zh: "请按修饰键与按键组合…", en: "Press a modifier and key combination…" },
"settings.previous_desktop_app": { zh: "旧版桌面端", en: "Previous desktop app" },
"settings.read_only_preview_of_word": { zh: "在工作台里只读预览 Word(.docx)和 Excel(.xlsx)。首次打开这类文件时才加载,在隔离的框中显示,不运行宏和脚本。关闭后只能用系统应用打开。仅保存在本机。", en: "Read-only preview of Word (.docx) and Excel (.xlsx) files in the workbench. It loads the first time such a file is opened and renders in an isolated frame without running macros or scripts. When off, these files open only in the system app. Saved on this device only." },
"settings.reasonix_itself_needs_no_account": { zh: "Reasonix 本身不需要账号,仅在需要联网的功能中使用:社区发帖、崩溃问题跟进,以及后续的技能发布。", en: "Reasonix itself needs no account. One is used only where the network is unavoidable: community posts, crash follow-ups, and later, publishing skills." },
"settings.rebuilds_the_runtime": { zh: "重建运行时", en: "Rebuilds the runtime" },
"settings.record_shortcut": { zh: "录制快捷键", en: "Record shortcut" },
"settings.registered_value": { zh: "已注册:{value}", en: "Registered: {value}" },
@@ -241,4 +238,22 @@ export const SETTINGS: Messages = {
"settings.config_in_one_file": { zh: "把模型、扩展、记忆等配置加密导出成一个文件", en: "Export models, extensions and memory to one encrypted file" },
"settings.back_up_to_the_cloud": { zh: "登录后把配置备份到云端", en: "Back up your configuration to the cloud once signed in" },
"settings.size_of_requests_sent": { zh: "发给模型服务商的请求有多大", en: "How large the requests sent to model providers are" },
"settings.other_machines": { zh: "其他机器", en: "Other machines" },
"settings.network_hint": { zh: "代理和网络诊断", en: "Proxy and network diagnostics" },
"settings.remote_hint": { zh: "连接其他机器上的 Reasonix", en: "Connect to Reasonix on other machines" },
"settings.remote_machines": { zh: "远程机器", en: "Remote machines" },
"settings.phone_hint": { zh: "用手机访问这台电脑上的会话,含通知设置", en: "Reach this computer's sessions from a phone, with notification settings" },
"settings.set_up_phone_access": { zh: "设置手机访问", en: "Set up phone access" },
"settings.account_hint": { zh: "社区发帖、崩溃跟进时用;与模型密钥无关", en: "Used for community posts and crash follow-up; unrelated to model keys" },
"settings.sign_in": { zh: "登录", en: "Sign in" },
"settings.account_info": { zh: "账号信息", en: "Account" },
"settings.version_and_diagnostics": { zh: "版本与诊断", en: "Version and diagnostics" },
"settings.versions_hint": { zh: "当前版本、可用更新、回退", en: "Current version, available updates, rolling back" },
"settings.versions_and_updates": { zh: "版本与更新", en: "Versions and updates" },
"settings.logs_hint": { zh: "出问题时附上的日志位置", en: "Where the logs to attach when something goes wrong are" },
"settings.trace_hint": { zh: "把执行轨迹导出给开发者排查", en: "Export execution traces for a developer to look into" },
"settings.about_note": { zh: "社区与贡献者、赞助在这里展示,不是开关。", en: "The community, contributors and sponsorship are shown here; they are not switches." },
"settings.look_at": { zh: "查看", en: "View" },
"settings.set_up_trace": { zh: "导出轨迹", en: "Export traces" },
"settings.about": { zh: "关于", en: "About" },
};
-37
View File
@@ -1,37 +0,0 @@
import { t } from "../i18n";
import type { AgentPort } from "../port/port";
import { AuthorLine } from "./AuthorLine";
import { Community } from "./Community";
import { Group } from "./Group";
import { Logs } from "./Logs";
import { Sponsor } from "./Sponsor";
import { Versions } from "./Versions";
export function About({ port }: { port: AgentPort }) {
return (
<>
<Group id="versions"
title={t("common.version")}
hint={t("about.the_installed_version_available_updates")}
>
<Versions port={port} />
</Group>
<Group id="logs"
title={t("about.logs_and_abnormal_exit_records")}
hint={t("about.after_a_problem_or_a")}
>
<Logs port={port} />
</Group>
<AuthorLine port={port} />
<Group id="community"
title={t("common.community_and_contributors")}
hint={t("about.join_the_community_for_help")}
>
<Community port={port} />
</Group>
<Group id="sponsor" title={t("about.support_the_project")}>
<Sponsor port={port} />
</Group>
</>
);
}
@@ -0,0 +1,42 @@
// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, expect, it, vi } from "vitest";
import "./testkit";
import { MockPort } from "../port/mock";
import type { AccountState, AgentPort } from "../port/port";
import { AccountSettings } from "./AccountSettings";
import { SettingDeepContext } from "./SettingDeep";
afterEach(cleanup);
function draw(state: AccountState | null) {
const go = vi.fn();
const port = new MockPort() as unknown as AgentPort;
render(
<SettingDeepContext.Provider value={{ pages: true, open: null, section: "账号与关于", go }}>
<AccountSettings port={port} homePort={port} state={state} unread="" reload={() => {}} />
</SettingDeepContext.Provider>,
);
return go;
}
it("is account, version and diagnostics, and about: one row per setting", () => {
draw({ signedIn: false });
expect([...document.querySelectorAll(".prefs-card > header h3")].map((h) => h.textContent)).toEqual(["账号", "版本与诊断", "关于"]);
for (const id of ["account", "versions", "logs", "community", "sponsor"]) expect(document.querySelector(`[data-setting="${id}"]`), id).toBeTruthy();
});
it("offers sign-in when signed out and the account when signed in", () => {
const go = draw({ signedIn: false });
fireEvent.click(screen.getByRole("button", { name: "登录" }));
expect(go).toHaveBeenLastCalledWith("account");
cleanup();
draw({ signedIn: true, user: { handle: "demo", email: "[email protected]", label: "demo" } });
expect(screen.getByRole("button", { name: "账号信息" })).toBeTruthy();
});
it("opens versions and logs from their pills", () => {
const go = draw({ signedIn: false });
fireEvent.click(screen.getByRole("button", { name: "版本与更新" }));
expect(go).toHaveBeenLastCalledWith("versions");
});
@@ -0,0 +1,50 @@
import { t } from "../i18n";
import type { AccountState, AgentPort } from "../port/port";
import { Account } from "./Account";
import { AuthorLine } from "./AuthorLine";
import { Community } from "./Community";
import { Group } from "./Group";
import { Logs } from "./Logs";
import { SectionCard } from "./SectionCard";
import { Sponsor } from "./Sponsor";
import { TraceExport } from "./TraceExport";
import { Versions } from "./Versions";
interface Props {
port: AgentPort;
homePort: AgentPort;
state: AccountState | null;
unread: string;
reload: () => void;
}
export function AccountSettings({ port, homePort, state, unread, reload }: Props) {
return (
<>
<SectionCard title={t("settings.account")}>
<Group id="account" pill={t(state?.signedIn ? "settings.account_info" : "settings.sign_in")} title={t("settings.account")} hint={t("settings.account_hint")}>
<Account port={port} state={state} unread={unread} reload={reload} />
</Group>
</SectionCard>
<SectionCard title={t("settings.version_and_diagnostics")}>
<Group id="versions" pill={t("settings.versions_and_updates")} title={t("common.version")} hint={t("settings.versions_hint")}>
<Versions port={homePort} />
</Group>
<Group id="logs" pill={t("safety.details")} title={t("about.logs_and_abnormal_exit_records")} hint={t("settings.logs_hint")}>
<Logs port={homePort} />
</Group>
<TraceExport port={port} />
</SectionCard>
<SectionCard title={t("settings.about")}>
<p className="srow-desc">{t("settings.about_note")}</p>
<AuthorLine port={homePort} />
<Group id="community" pill={t("settings.look_at")} title={t("common.community_and_contributors")} hint={t("about.join_the_community_for_help")}>
<Community port={homePort} />
</Group>
<Group id="sponsor" pill={t("settings.look_at")} title={t("about.support_the_project")}>
<Sponsor port={homePort} />
</Group>
</SectionCard>
</>
);
}
@@ -0,0 +1,41 @@
// @vitest-environment jsdom
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
import { afterEach, expect, it, vi } from "vitest";
import "./testkit";
import { MockHub } from "../port/mock_hub";
import { MockPort } from "../port/mock";
import type { AgentPort } from "../port/port";
import { ConnectSettings } from "./ConnectSettings";
import { SettingDeepContext } from "./SettingDeep";
afterEach(cleanup);
function draw(remote: boolean) {
const go = vi.fn();
const port = new MockPort() as unknown as AgentPort;
render(
<SettingDeepContext.Provider value={{ pages: true, open: null, section: "连接", go }}>
<ConnectSettings port={port} networkPort={port} host="" hub={new MockHub()} onError={() => {}} remote={remote} />
</SettingDeepContext.Provider>,
);
return go;
}
it("is a network card and an other-machines card of one row per setting", () => {
draw(true);
expect([...document.querySelectorAll(".prefs-card > header h3")].map((h) => h.textContent)).toEqual(["网络", "其他机器"]);
for (const id of ["network", "remote", "phone"]) expect(document.querySelector(`[data-setting="${id}"]`), id).toBeTruthy();
});
it("opens each page from its pill", () => {
const go = draw(true);
for (const [name, id] of [["详细设置", "network"], ["远程机器", "remote"], ["设置手机访问", "phone"]] as const) {
fireEvent.click(screen.getByRole("button", { name }));
expect(go).toHaveBeenLastCalledWith(id);
}
});
it("draws no machines card where remote access is not offered", () => {
draw(false);
expect(document.querySelector('[data-setting="remote"]')).toBeNull();
});
@@ -0,0 +1,44 @@
import { t } from "../i18n";
import type { HubPort } from "../port/hub";
import type { AgentPort } from "../port/port";
import { remoteDeviceId } from "../port/cloud_remote";
import { Group } from "./Group";
import { Network } from "./Network";
import { PhoneAccess } from "./PhoneAccess";
import { PhoneNotifications } from "./PhoneNotifications";
import { PhoneReminders } from "./PhoneReminders";
import { Remotes } from "./Remotes";
import { SectionCard } from "./SectionCard";
interface Props {
port: AgentPort;
networkPort: AgentPort;
host: string;
hub: HubPort;
onError: (e: unknown) => void;
remote: boolean;
}
export function ConnectSettings({ port, networkPort, host, hub, onError, remote }: Props) {
return (
<>
<SectionCard title={t("common.network")}>
<Group id="network" pill={t("safety.details")} title={t("common.network")} hint={t("settings.network_hint")}>
<Network port={networkPort} host={host} />
</Group>
</SectionCard>
{remote && (
<SectionCard title={t("settings.other_machines")}>
<Group id="remote" pill={t("settings.remote_machines")} title={t("settings.remote")} hint={t("settings.remote_hint")}>
<Remotes hub={hub} onError={onError} />
</Group>
<Group id="phone" pill={t("settings.set_up_phone_access")} title={t("remote.phone_access")} hint={t("settings.phone_hint")}>
<PhoneAccess hub={hub} onError={onError} />
<PhoneNotifications hub={hub} port={port} onError={onError} />
{remoteDeviceId() && <PhoneReminders host={remoteDeviceId()!} />}
</Group>
</SectionCard>
)}
</>
);
}
+2 -4
View File
@@ -3,7 +3,6 @@ import { t } from "../i18n";
import type { HubPort } from "../port/hub";
import type { CloudShareOffer, ShareOffer, ShareStatus } from "../port/share";
import { copyText } from "./CopyButton";
import { ApplyNote } from "./Group";
import { Switch } from "./Switch";
interface Props {
@@ -301,14 +300,13 @@ export function PhoneAccess({ hub, onError }: Props) {
// A settings page must not grow a heading for a door this kernel cannot open.
if (!share.st) return null;
return (
<section className="grp" id="set-phone" data-setting="phone">
<div className="phone-door">
<div className="grp-hd">
<h3>{t("remote.phone_access")}</h3>
</div>
<div className="grp-items share">
<ShareBody share={share} />
</div>
<ApplyNote id="phone" />
</section>
</div>
);
}
+4 -37
View File
@@ -19,14 +19,11 @@ import { firstPage, pageOf } from "./settingPages";
import { Group, SCOPE_SAID } from "./Group";
import { CommonSettings } from "./CommonSettings";
import { AgentSettings } from "./AgentSettings";
import { AccountSettings } from "./AccountSettings";
import { ConnectSettings } from "./ConnectSettings";
import { SafetySettings } from "./SafetySettings";
import type { Section } from "./prefsnav";
import { AddServer } from "./AddServer";
import { PhoneAccess } from "./PhoneAccess";
import { PhoneReminders } from "./PhoneReminders";
import { remoteDeviceId } from "../port/cloud_remote";
import { PhoneNotifications } from "./PhoneNotifications";
import { Remotes } from "./Remotes";
import type { RemoteHost } from "../port/remote";
import { AddPlugin } from "./AddPlugin";
import { Packages } from "./Packages";
@@ -40,22 +37,18 @@ import { ServerRow } from "./ServerRow";
import { SkillGroups } from "./SkillRow";
import { Hooks } from "./Hooks";
import { ScheduledRuns } from "./ScheduledRuns";
import { Network } from "./Network";
import { ConfigTrouble } from "./ConfigTrouble";
import { PresentFiles } from "./PresentFiles";
import { TraceExport } from "./TraceExport";
import { ModHost } from "./ModHost";
import { DisplaySettings } from "./DisplaySettings";
import { DataSettings } from "./DataSettings";
import { SettingDeepContext } from "./SettingDeep";
import { Account } from "./Account";
import { activeKind, groupVendors } from "./Models";
import { useModelCatalog } from "./useModelCatalog";
import { useRoles } from "./useRoles";
import { KIND_LABEL } from "./vendors";
import { planProtocolSwitch } from "./protocolswitch";
import { Boundary } from "./Boundary";
import { About } from "./About";
import { DEFAULT_DAYS } from "./Usage";
import { Appearance, SCHEMES } from "./Appearance";
import { ScopeBar } from "./CapabilityScope";
@@ -592,36 +585,10 @@ export function Settings({ hub, onError, port, networkPort, localWorkspacePort,
</>
)}
{at === "network" && (
<Group id="network"
title={t("common.network")}
hint={t("settings.model_requests_remote_mcp_services")}
>
<Network port={networkPort ?? port} host={networkHost ?? ""} />
</Group>
<ConnectSettings port={port} networkPort={networkPort ?? port} host={networkHost ?? ""} hub={hub} onError={onError} remote={shown("remote")} />
)}
{at === "account" && <TraceExport port={port} />}
{at === "network" && shown("remote") && (
<Group id="remote"
title={t("settings.remote")}
hint={t("settings.connect_a_workspace_on_another")}
>
<Remotes hub={hub} onError={onError} />
</Group>
)}
{at === "network" && shown("remote") && <PhoneAccess hub={hub} onError={onError} />}
{at === "network" && shown("remote") && <PhoneNotifications hub={hub} port={port} onError={onError} />}
{at === "network" && shown("remote") && remoteDeviceId() && <PhoneReminders host={remoteDeviceId()!} />}
{at === "account" && (
<Group id="account"
title={t("settings.account")}
hint={t("settings.reasonix_itself_needs_no_account")}
>
<Account port={port} state={acct} unread={accountUnread} reload={reloadAccount} />
</Group>
)}
{at === "account" && <About port={homePort} />}
{at === "account" && <AccountSettings port={port} homePort={homePort} state={acct} unread={accountUnread} reload={reloadAccount} />}
{at === "storage" && (
<DataSettings port={port} homePort={homePort} localPort={localPort} hub={hub} workspace={workspaceRoot ?? (homePort === port ? status?.workspaceRoot ?? "" : "")} cwd={status?.cwd} busy={busy}
signedIn={!!acct?.signedIn && !!acct.user} memories={memCount} onIsolate={() => run("isolate", () => port.isolateWorkspace())} onChanged={onChanged} onRecovered={() => { onChanged(); onSessionsRecovered?.(); }} />
+2 -2
View File
@@ -16,7 +16,7 @@ interface Props {
onPage?: (section: Section, page: string) => void;
}
const countOf = (id: Section) => SETTINGS.filter((e) => !e.hidden && pageHome(e.section) === id).length;
const countOf = (id: Section) => SETTINGS.filter((e) => !e.hidden && !e.within && pageHome(e.section) === id).length;
export function SettingsNav({ at, values, shown, danger, disabled, onSelect, page, onPage }: Props) {
return (
@@ -60,7 +60,7 @@ export function SettingsNav({ at, values, shown, danger, disabled, onSelect, pag
export function SettingsTitle({ at }: { at: Section }) {
const group = NAV.find(([, items]) => items.some(([id]) => id === at));
const name = group?.[1].find(([id]) => id === at)?.[1] ?? at;
const count = SETTINGS.filter((e) => !e.hidden && pageHome(e.section) === at).length;
const count = SETTINGS.filter((e) => !e.hidden && !e.within && pageHome(e.section) === at).length;
const desc = SECTION_DESC[at];
return (
<div className="prefs-pagetitle">
+1 -1
View File
@@ -52,7 +52,7 @@ export function TraceExport({ port }: { port: AgentPort }) {
};
return (
<Group id="trace-export" title={t("data.trace_export")} hint={t("data.send_each_turn_to_your")}>
<Group id="trace-export" pill={t("settings.set_up_trace")} title={t("data.trace_export")} hint={t("settings.trace_hint")}>
<div className="box">
<div className="lrow">
<span className="tx">
+9 -9
View File
@@ -232,7 +232,7 @@ export const SETTINGS: SettingEntry[] = [
{ section: "peers", anchor: "peer-projects", within: "peers", title: "remote.projects_allowed_through", scope: "machine", apply: "next-turn", keywords: ["放行", "项目", "不再询问", "长期", "撤销", "peer", "release", "会话间"] },
{ section: "tools", anchor: "write-lease", page: true, title: "session.several_sessions_writing_one_project", scope: "machine", apply: "on-open", keywords: ["lease", "写锁", "并行", "多会话", "乐观", "optimistic", "write_lease", "等待"] },
{ section: "session", anchor: "subagent-claims", title: "session.sub_agent_write_scope", scope: "machine", apply: "runtime-rebuild", keywords: ["subagent", "子代理", "并发", "write_paths", "乐观", "optimistic", "写锁"] },
{ section: "account", anchor: "trace-export", title: "data.trace_export", scope: "machine", apply: "on-open", keywords: ["追踪", "opentelemetry", "otlp", "观测", "遥测", "trace", "collector", "收集器"] },
{ section: "account", anchor: "trace-export", page: true, title: "data.trace_export", scope: "machine", apply: "on-open", keywords: ["追踪", "opentelemetry", "otlp", "观测", "遥测", "trace", "collector", "收集器"] },
{ section: "tools", anchor: "web-access", page: true, title: "models.web_search_and_fetch", scope: "machine", apply: "on-open", keywords: ["搜索", "searxng", "deepseek", "抓取", "计费", "付费", "web search", "fetch"] },
{ section: "tools", anchor: "browser-tools", title: "common.built_in_browser", scope: "machine", apply: "runtime-rebuild", keywords: ["browser", "网页", "chrome", "浏览器工具"] },
// The four blocks below carry a scope control of their own, and the kernel
@@ -249,22 +249,22 @@ export const SETTINGS: SettingEntry[] = [
{ section: "ext", anchor: "skills", title: "settings.skills", scope: "chosen", apply: "immediate", keywords: ["skill", "技能包"] },
{ section: "network", anchor: "network", page: true, title: "common.network", scope: "machine", apply: "immediate", keywords: ["代理", "proxy", "抓取", "超时"] },
{ section: "remote", anchor: "remote", title: "settings.remote", scope: "machine", apply: "immediate", keywords: ["ssh", "机器", "远端工作区"] },
{ section: "remote", anchor: "remote", page: true, title: "settings.remote", scope: "machine", apply: "immediate", keywords: ["ssh", "机器", "远端工作区"] },
// Machine rather than session: the door is this window's, and a phone that
// walks through it reaches every pane. Nothing is written, so nothing outlives
// the window either.
{ section: "remote", anchor: "phone", title: "remote.phone_access", scope: "machine", apply: "immediate", keywords: ["手机", "扫码", "二维码", "局域网", "配对", "phone", "mobile", "qr"] },
{ section: "remote", anchor: "phone-notify", title: "remote.phone_notifications", scope: "machine", apply: "immediate", keywords: ["推送", "提醒", "手机", "批准", "push", "notification", "phone"] },
{ section: "remote", anchor: "phone-device", title: "remote.this_phones_reminders", scope: "machine", apply: "immediate", keywords: ["推送", "提醒", "手机", "push", "notification"] },
{ section: "account", anchor: "account", title: "settings.account", scope: "account", apply: "immediate", keywords: ["登录", "社区"] },
{ section: "remote", anchor: "phone", page: true, title: "remote.phone_access", scope: "machine", apply: "immediate", keywords: ["手机", "扫码", "二维码", "局域网", "配对", "phone", "mobile", "qr"] },
{ section: "remote", anchor: "phone-notify", within: "phone", title: "remote.phone_notifications", scope: "machine", apply: "immediate", keywords: ["推送", "提醒", "手机", "批准", "push", "notification", "phone"] },
{ section: "remote", anchor: "phone-device", within: "phone", title: "remote.this_phones_reminders", scope: "machine", apply: "immediate", keywords: ["推送", "提醒", "手机", "push", "notification"] },
{ section: "account", anchor: "account", page: true, title: "settings.account", scope: "account", apply: "immediate", keywords: ["登录", "社区"] },
// Shown only while signed in; restoring writes through the ordinary config paths and reloads the runtime.
{ section: "storage", anchor: "backup-file", page: true, title: "data.export_and_import_config", scope: "machine", apply: "immediate", keywords: ["备份", "导出", "导入", "换机", "迁移", "文件"] },
{ section: "storage", anchor: "backup", page: true, title: "data.cloud_backup", scope: "account", apply: "immediate", keywords: ["备份", "恢复", "换机", "同步", "导出"] },
{ section: "versions", anchor: "versions", page: true, title: "common.version", scope: "machine", apply: "immediate", keywords: ["更新", "升级"] },
{ section: "common", anchor: "update-mode", title: "about.update_mode", scope: "machine", apply: "immediate", keywords: ["自动更新", "检查更新", "更新提醒", "后台", "update", "auto update", "notify", "自动下载", "download"] },
{ section: "versions", anchor: "logs", title: "about.logs_and_abnormal_exit_records", scope: "machine", apply: "none", keywords: ["日志", "崩溃", "闪退", "异常退出", "log", "logs", "crash", "路径"] },
{ section: "versions", anchor: "community", title: "common.community_and_contributors", scope: "machine", apply: "none", keywords: ["社区", "交流群", "QQ", "加群", "二维码", "discord", "贡献者", "contributors", "community"] },
{ section: "versions", anchor: "sponsor", title: "about.support_the_project", scope: "machine", apply: "none", keywords: ["捐助", "支付宝", "微信支付", "收款码", "paypal", "donate", "sponsor", "support", "alipay", "wechat"] },
{ section: "versions", anchor: "logs", page: true, title: "about.logs_and_abnormal_exit_records", scope: "machine", apply: "none", keywords: ["日志", "崩溃", "闪退", "异常退出", "log", "logs", "crash", "路径"] },
{ section: "versions", anchor: "community", page: true, title: "common.community_and_contributors", scope: "machine", apply: "none", keywords: ["社区", "交流群", "QQ", "加群", "二维码", "discord", "贡献者", "contributors", "community"] },
{ section: "versions", anchor: "sponsor", page: true, title: "about.support_the_project", scope: "machine", apply: "none", keywords: ["捐助", "支付宝", "微信支付", "收款码", "paypal", "donate", "sponsor", "support", "alipay", "wechat"] },
{ section: "memory", anchor: "memory", page: true, title: "data.memory", scope: "chosen", apply: "immediate", keywords: ["记住", "忘记", "事实"] },
{ section: "usage", anchor: "currency", title: "usage.cost_display_currency", scope: "machine", apply: "immediate", keywords: ["币种", "货币", "人民币", "美元", "currency", "CNY", "USD", "汇率"] },
{ section: "usage", anchor: "upload-usage", page: true, title: "usage.upload_volume", scope: "machine", apply: "none", keywords: ["流量", "上传", "带宽", "请求", "upload", "traffic", "bandwidth"] },
@@ -37,6 +37,7 @@ it("keeps machine reads separate from active-pane account operations", async ()
expect(localAccount).not.toHaveBeenCalled();
expect(remoteAccount).toHaveBeenCalled();
await userEvent.click(screen.getByRole("tab", { name: /账号/ }));
await userEvent.click(await screen.findByRole("button", { name: "账号信息" }));
await screen.findAllByText("Remote account");
await userEvent.click(screen.getByRole("button", { name: "退出登录" }));
await userEvent.click(screen.getByRole("button", { name: /确认退出/ }));
@@ -27,7 +27,7 @@ function AccountSettings({ port, onChanged }: { port: AgentPort; onChanged: () =
status={{ preset: "balanced", toolApprovalMode: "ask" } as SessionStatus}
theme="light" onTheme={() => {}} contrast="" onContrast={() => {}} weight="" onWeight={() => {}}
look={{} as never} onLook={() => {}} reloadThemes={() => {}}
onClose={() => {}} onChanged={onChanged} onError={() => {}} at="account"
onClose={() => {}} onChanged={onChanged} onError={() => {}} at="account:account"
account={account} accountUnread="" reloadAccount={() => { void port.account().then(setAccount); }}
/>;
}
@@ -11,7 +11,7 @@ afterEach(() => { cleanup(); vi.restoreAllMocks(); history.replaceState(null, ""
const drawn = () => render(<Settings hub={new MockHub()} onError={() => {}} port={new MockPort()} networkPort={new MockPort()}
status={null} theme="" reloadThemes={() => {}} onTheme={() => {}} contrast="" onContrast={() => {}}
weight="" onWeight={() => {}} look={{}} onLook={() => {}} onClose={() => {}} onChanged={() => {}}
at="remote" account={null} accountUnread="" reloadAccount={() => {}} />);
at="remote:phone" account={null} accountUnread="" reloadAccount={() => {}} />);
it("offers a phone its own reminders only on a page that controls a computer", async () => {
history.replaceState(null, "", "/?device=" + "ab".repeat(32));
@@ -9,7 +9,7 @@ export function useSettingLanding(root: RefObject<HTMLDivElement | null>, anchor
let done: ReturnType<typeof setTimeout> | undefined;
const behavior = matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth";
const aim = () => {
const found = col.querySelector<HTMLElement>(`#set-${CSS.escape(anchor)}`);
const found = col.querySelector<HTMLElement>(`.deep-page[data-setting="${CSS.escape(anchor)}"], #set-${CSS.escape(anchor)}`);
if (!found) return;
for (let parent = found.closest("details"); parent; parent = parent.parentElement?.closest("details") ?? null)
parent.open = true;