Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/desktop/src/lib/settings-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,10 @@ export const SETTINGS_NAV: SettingsNavEntry[] = [
labelKey: "settings.nav.sync",
titleKey: "settings.configSync.title",
group: "system",
// Cloud sync (encrypted portable configuration backup) is not open to
// users yet: packaged builds hide the destination and its search hits,
// development builds keep it. Drop this flag to ship it again.
developmentOnly: true,
keywordKeys: [
"settings.configSync.connectionTitle",
"settings.configSync.endpoint",
Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/test/config-sync-settings.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ const syncError = await read(
test("cloud sync rendering follows the settings visibility gate", () => {
assert.match(settingsPage, /tab === "sync" && !tabHidden && <ConfigSyncPage \/>/);
assert.match(settingsIndex, /id: "sync"/);
// The cloud backup ships hidden from packaged builds: the destination stays
// a development-build surface until it opens.
assert.match(settingsIndex, /id: "sync"[\s\S]{0,400}developmentOnly: true/);
assert.doesNotMatch(settingsIndex, /experimentalBadgeKey: "settings\.configSync\.experimental"/);
assert.match(settingsIndex, /settings\.configSync\.connectionTitle/);
});
Expand Down
76 changes: 47 additions & 29 deletions apps/desktop/test/settings-developer-only-destinations.test.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
/**
* Developer-only settings destinations are retained in development builds
* but omitted from packaged builds. Cloud sync is a public Experimental
* destination: it is visible in every build without developer mode.
* Navigation, search, and stale-page handling must all honor the same
* visibility rules.
* but omitted from packaged builds. Cloud sync is not open to users yet and
* carries the same build gate: development builds keep it, packaged builds
* omit its rail row, page, and settings-search hits. Navigation, search, and
* stale-page handling must all honor the same visibility rules.
*/
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
Expand Down Expand Up @@ -74,27 +74,39 @@ test("Live Voice is reachable in every build without developer mode", () => {
);
});

test("Cloud sync is reachable in every build without developer mode", () => {
test("Cloud sync is a development-build-only destination", () => {
// Cloud backup (encrypted portable configuration sync) is not open to users
// yet: development builds keep the destination, packaged builds omit it.
for (const developerMode of [false, true]) {
for (const includeDevelopmentOnly of [false, true]) {
assert.ok(visibleSettingsNav(developerMode, includeDevelopmentOnly)
.some((entry) => entry.id === "sync"));
assert.equal(isSettingsDestinationHidden("sync", developerMode, includeDevelopmentOnly), false);
for (const query of [
"configSync.connectionTitle",
"configSync.endpoint",
"configSync.syncNow",
]) {
assert.ok(searchSettings(query, identity, { developerMode, includeDevelopmentOnly })
.some((hit) => hit.tab === "sync"));
}
assert.ok(visibleSettingsNav(developerMode, true)
.some((entry) => entry.id === "sync"));
assert.equal(isSettingsDestinationHidden("sync", developerMode, true), false);
for (const query of [
"configSync.connectionTitle",
"configSync.endpoint",
"configSync.syncNow",
]) {
assert.ok(searchSettings(query, identity, {
developerMode,
includeDevelopmentOnly: true,
}).some((hit) => hit.tab === "sync"));
}

assert.equal(visibleSettingsNav(developerMode, false)
.some((entry) => entry.id === "sync"), false);
assert.equal(isSettingsDestinationHidden("sync", developerMode, false), true);
assert.deepEqual(
searchSettings("configSync.connectionTitle", identity, {
developerMode,
includeDevelopmentOnly: false,
}),
[],
);
}
// Cloud sync is a regular destination now: no developer or build gate and no
// Experimental badge remain.
// The build gate is the only gate: no developer mode and no badge.
const sync = SETTINGS_NAV.find((entry) => entry.id === "sync");
assert.equal(sync?.developerOnly, undefined);
assert.equal(sync?.developmentOnly, undefined);
assert.equal(sync?.developmentOnly, true);
assert.equal(sync?.experimentalBadgeKey, undefined);
});

Expand All @@ -115,31 +127,37 @@ test("developer mode retains the developer-only destinations in development", ()
SETTINGS_NAV.filter((entry) => entry.developerOnly === true)
.every((entry) => entry.experimentalBadgeKey),
);
// Development builds keep the not-yet-open Cloud sync destination.
assert.equal(off.includes("sync"), true);
});

test("packaged builds still hide the developer-only remote hosts", () => {
test("packaged builds hide the developer-only destinations and Cloud sync", () => {
const packaged = visibleSettingsNav(true, false).map((entry) => entry.id);
for (const id of developerOnlyIds) {
assert.equal(packaged.includes(id), false);
assert.equal(isSettingsDestinationHidden(id, true, false), true);
}
assert.equal(packaged.includes("sync"), true);
assert.equal(isSettingsDestinationHidden("sync", true, false), false);
assert.equal(isSettingsDestinationHidden("sync", false, false), false);
assert.equal(packaged.includes("sync"), false);
assert.equal(isSettingsDestinationHidden("sync", true, false), true);
assert.equal(isSettingsDestinationHidden("sync", false, false), true);
assert.equal(isSettingsDestinationHidden("general", true, false), false);
});

test("settings search mirrors developer and packaged visibility", () => {
for (const options of [{ developerMode: false }, { developerMode: true }]) {
assert.ok(
searchSettings("configSync.connectionTitle", identity, options)
.some((hit) => hit.tab === "sync"),
);
}

for (const options of [
{ developerMode: false },
{ developerMode: true },
{ developerMode: false, includeDevelopmentOnly: false },
{ developerMode: true, includeDevelopmentOnly: false },
]) {
assert.ok(
searchSettings("configSync.connectionTitle", identity, options)
.some((hit) => hit.tab === "sync"),
assert.deepEqual(
searchSettings("configSync.connectionTitle", identity, options),
[],
);
}

Expand Down
4 changes: 3 additions & 1 deletion docs/spec/03-runtime/01-ipc-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -2406,7 +2406,9 @@ unchanged. See [provider configuration](12-provider-config-schema.md).
## 15. Cloud configuration sync

The Settings → Cloud sync page uses the following renderer-to-Main channels;
all are forwarded to the Host-owned `configSync.*` RPC methods:
all are forwarded to the Host-owned `configSync.*` RPC methods. The page is a
development-build-only surface for now; the channels and their Host contracts
are unchanged:

| IPC channel | Host method | contract |
|---|---|---|
Expand Down
4 changes: 4 additions & 0 deletions docs/spec/03-runtime/22-config-sync.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ unchanged.

## 5. Settings workflow

The destination is a development-build-only surface for now: a packaged build
omits the Settings → Cloud sync row, page, and settings-search hits, while
the Host-owned sync behavior described here is unchanged.

Settings → Cloud sync provides WebDAV endpoint credentials, vault password,
device label, server compatibility mode, category selection, a capability test,
sync-now, unlock, pause, folder mapping, approval/rejection, revision
Expand Down
30 changes: 17 additions & 13 deletions docs/spec/04-ux/06-settings-ia.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Settings is a **full-window page** that replaces the app sidebar + main chrome (
7. **MCP** — Lucide `Server` (agent connections)
8. **Subagents / 子智能体** — Lucide `Bot` (built-in and personal parallel agents)
9. **Projects / 项目** — Lucide `Archive` (durable project index)
10. **Cloud sync / 云同步** — Lucide `CloudDownload` (encrypted portable configuration backup and bidirectional sync)
10. **Cloud sync / 云同步** — Lucide `CloudDownload` (encrypted portable configuration backup and bidirectional sync; development builds only)
11. **Remote Hosts / 远程主机** — Lucide `Globe` (SSH bootstrap and pairing inventory; developer mode only)
12. **Info / 信息** — Lucide `Info` (versions, logs, updates, developer)
Icons are decorative (`aria-hidden` via the SVG default) and stay monochrome
Expand All @@ -63,8 +63,8 @@ Settings is a **full-window page** that replaces the app sidebar + main chrome (
scanability, the destinations are shown in four titled visual clusters:
`Preferences` / `偏好` (General, AI, Shortcuts), `Agent` / `智能体`
(Instructions, Models, Skills, MCP, Subagents), `Workspace` / `工作区`
(Projects), and `System` / `系统` (Cloud sync, Remote Hosts, Info;
Remote Hosts is developer-only). Headings are
(Projects), and `System` / `系统` (Cloud sync, Remote Hosts, Info; Cloud sync
is development-build-only, Remote Hosts is developer-only). Headings are
muted, non-interactive labels and use whitespace for separation; no divider
lines are rendered. These are visual landmarks only, not a second navigation
level.
Expand All @@ -74,12 +74,14 @@ Settings is a **full-window page** that replaces the app sidebar + main chrome (
requirement. Its rail row, page, search hits, and idle Composer entry are
available to all users.
It is the only place to enable Live Voice. See the Voice section below.
- **Cloud sync / 云同步** is a regular `System` / `系统` destination
available to every user in every build: its rail row, page, and
settings-search hits never depend on developer mode and never fall back to
General. It ships as a stable destination, so neither the rail row nor the
page title carries an Experimental badge, and nothing about the sync
behavior itself changes.
- **Cloud sync / 云同步** is not open to users yet: it is a
development-build-only `System` / `系统` destination. Its rail row, page,
and settings-search hits exist in development builds only; a packaged build
omits them, and a rail position left on it falls back to General. Developer
mode is not a gate either way, neither the rail row nor the page title
carries an Experimental badge, and nothing about the sync behavior itself
changes. Removing the destination's `developmentOnly` flag reopens it for
packaged builds.
- **Remote Hosts / 远程主机** is a developer-only, Experimental destination: its
rail row, its page, and its settings-search hits exist only while
`AppSettings.developerMode` is `true`. With developer mode off the row is
Expand Down Expand Up @@ -832,17 +834,19 @@ system while preserving their different data ownership:
- Developer-only destinations join and leave the rail, the page, and settings
search as one unit: while developer mode is off the rail omits the row,
settings search returns no hit for it, and an open Remote Hosts page returns
to General. Cloud sync is a regular destination and always stays reachable
to General. Cloud sync is development-build-only for now: packaged builds
omit its rail row, page, and settings-search hits and fall back to General,
while developer mode never gates it.

## 4. Acceptance

1. Opening Settings hides the coding app sidebar (full-page takeover)
2. Rail shows the search pill at the top, the back-to-app action pinned at the
foot on the main sidebar's footer icon line, and exactly General / 常规, AI,
Shortcuts / 快捷键, Instructions / 指令, Models / 模型, Skills / 技能, MCP,
Subagents / 子智能体, Projects / 项目, Cloud sync / 云同步,
Remote Hosts / 远程主机, and Info / 信息 in that order. Cloud sync / 云同步 is
available to every user; Remote Hosts appears only in developer mode. Voice
Subagents / 子智能体, Projects / 项目, Cloud sync / 云同步 (development
builds only), Remote Hosts / 远程主机 (developer mode only), and Info / 信息
in that order. A packaged build leaves Cloud sync out; Voice
appears between AI and Shortcuts only in development builds with developer
mode on. The rows are grouped under Preferences / 偏好,
Agent / 智能体, Workspace / 工作区, and System / 系统. There is no
Expand Down
36 changes: 21 additions & 15 deletions docs/spec/06-delivery/04-e2e-test-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -2913,6 +2913,8 @@ identify the platform validation still needed.
15. Archive one project session, open it from Projects, and return to Projects.
16. Return to the app shell and open Plugins.
- **Expected**: The rail contains exactly General, AI, Shortcuts, Instructions, Models, Skills, MCP, Subagents, Projects, Cloud sync, Remote Hosts, and Info in that order, each with its semantic Lucide icon (Sliders / Sparkles / Keyboard / FileText / Bot / BookOpen / Server / Bot / Archive / CloudDownload / Globe / Info). The flat directory is visually grouped under four muted, non-interactive headings — Preferences / 偏好 for General, AI, and Shortcuts; Agent / 智能体 for Instructions and Models; Workspace / 工作区 for Projects; About / 关于 for Info — with whitespace and no divider lines between groups; searching keeps the destination results flat and hides empty groups together with their headings. Appearance remains in General, while Permissions, Defaults, and the Command shell row live under 全局 AI; an available selected shell is represented by the selector without a duplicate Configured status, while default, fallback, and no-effective-shell states remain explicit; Context management has no settings card; Keyboard shortcuts and global instructions have their own destinations; Developer lives under Info; Projects shows active, closed, and archived durable rows without a visibility toggle, grouping them under the always-visible Pinned / All projects / Archived strips (D168/D267/D455) with per-section counts in a one-column workbench. The destination renders no hero block and no page-level counter run: the intro is one quiet description line, and each group strip's count agrees with its rendered rows; a click selects a row without leaving Settings; sorting by Name reorders rows inside every section without hiding any; search matches project fields and session titles and reports a match count, a session-title result selects its owning project, lists sessions in the inspector by latest activity with relative update times, and reveals history in batches of eight; clearing the search restores the complete index. The inspector menu closes on Escape and on an outside press. Bootstrap completion and background refreshes do not return Settings or Extensions to the chat home; the destination changes only after an explicit navigation action. Restore keeps the archive open and activation returns to chat with the restored project retained in the sidebar. Opening an archived session succeeds before clearing its archived state, returns to chat with that session selected, and makes it visible in the project sidebar; returning to Project archive no longer shows that session as archived. The home sidebar and global page results have no standalone Projects destination; Settings search finds Projects; Plugins remains an independent app-shell destination.
A packaged build omits Cloud sync from the rail and settings search; see
`04-ux/06-settings-ia.md`.
- **Specs linked**: `04-ux/06-settings-ia.md`, `04-ux/01-ui-ia.md`, `03-runtime/11-provider-model-system.md`
- **Acceptance**: B (model configuration), F (project persistence)
- **Milestone**: M4
Expand Down Expand Up @@ -9389,9 +9391,9 @@ must keep splitting are covered by `markdown-blocks.test.mjs`.
local WebDAV fixture that supports strong ETags and conditional PUT, plus a
fixture variant that ignores conditional headers but supports `PROPFIND`
directory listing. No real WebDAV account, provider, or production desktop.
Developer mode starts off so the public destination is exercised as shipped.
- **Steps:** 1) Open Settings with developer mode off; confirm Cloud sync is
present in the rail and returned by settings search, then open it and
Developer mode starts off so the destination is exercised as developed.
- **Steps:** 1) In a development build with developer mode off, confirm Cloud
sync is present in the rail and returned by settings search, then open it and
confirm neither the rail row nor the page title carries an Experimental
badge. 2) Toggle developer mode on and off and confirm the destination stays
reachable either way. 3) Enter the fixture URL,
Expand All @@ -9418,9 +9420,10 @@ must keep splitting are covered by `markdown-blocks.test.mjs`.
refreshes in the background. Confirm a configured endpoint reuses its stored
WebDAV app password, while password fields themselves remain blank and no
vault password is written to renderer storage.
- **Expected:** Cloud sync is reachable in every build without developer mode,
carries no Experimental badge on the rail row or page title, and neither its
availability nor its behavior changes when developer mode is toggled.
- **Expected:** Cloud sync is reachable in development builds without developer
mode, is absent from a packaged build's rail, page, and settings search,
carries no Experimental badge, and neither its availability nor its behavior
changes when developer mode is toggled.
Strict mode refuses
unreliable conditional writes. The explicit
compatibility mode accepts only a server that proves bounded directory
Expand All @@ -9445,8 +9448,9 @@ must keep splitting are covered by `markdown-blocks.test.mjs`.
- **Milestone:** M6+.
- **Status:** Draft; merge/crypto, in-process WebDAV conditional-write
coverage, and the two-device host/WebDAV path are automated by
`pnpm test:e2e:config-sync`. Public Cloud sync visibility without developer
mode is asserted by `settings-developer-only-destinations.test.mjs`; full
`pnpm test:e2e:config-sync`. Cloud sync's development-build-only visibility
and its packaged-build omission are asserted by
`settings-developer-only-destinations.test.mjs`; full
renderer-driven persistence and checkpoint recovery fault injection remain.

**E2E-CHAT-session-todo-checklist: TodoWrite to session-aware TodoDock**
Expand Down Expand Up @@ -9870,8 +9874,9 @@ This test plan spec is accepted when:
- Open Settings (footer profile → Settings).
- Expect **full-page** Codex settings (no app sidebar/nav). Left rail has Back
to app, search, and exactly General / AI / Shortcuts / Instructions / Models /
Skills / MCP / Subagents / Projects / Cloud sync / Remote Hosts / Info in that
order; content pane shows the selected destination.
Skills / MCP / Subagents / Projects, Cloud sync / Remote Hosts / Info in that
order (a packaged build leaves Cloud sync out); content pane shows the
selected destination.
- Return to the app shell and expect Plugins to remain an independent
sidebar-footer destination.
- Drag the empty 46px top band over either the rail or content pane; the native
Expand Down Expand Up @@ -10083,8 +10088,8 @@ This test plan spec is accepted when:
- Expect the working theme selector without inert toggle or open-target rows.
- Expect Appearance in General and Permissions + Defaults in AI. The rail
contains General, AI, Shortcuts, Instructions, Models, Skills, MCP,
Subagents, Projects, Cloud sync, and Info; Remote Hosts appears only in
developer mode. Voice may appear between AI and Shortcuts in development
Subagents, Projects, Cloud sync (development builds only), and Info; Remote
Hosts appears only in developer mode. Voice may appear between AI and
builds with developer mode on; plugin-contributed destinations follow the
core groups. There is no Import destination.
- Resize between 800px, 1200px, and 1600px widths; the content cards fill the
Expand Down Expand Up @@ -16540,9 +16545,10 @@ the latest destination. These assertions measure work counts, not device FPS.
- Automated coverage: `pnpm test:e2e:settings-scroll` mounts the production
SettingsPage, store, translations, and built CSS in isolated Electron. Only
preload data is stubbed; search navigation uses SearchDialog's public store
entry points. It also checks that Cloud sync has no developer-mode gate and
no Experimental badge, that Remote hosts keeps its badge, and the fallback
to General. This covers renderer interaction, not host persistence or the
entry points. It also checks that Cloud sync stays a development-build-only
destination with no developer-mode gate and no Experimental badge, that
Remote hosts keeps its badge, and the fallback to General. This covers
renderer interaction, not host persistence or the
full global-search dialog.

### E2E-SCHEDULED-dispatch
Expand Down
Loading
Loading