feat: gate remote projects as a technical preview

Remote Host and managed SSH project entry points were always exposed while the signed artifact workflow remains preview-stage. Remote Projects now defaults off behind its own Platform Features tab, preserves saved Hosts, projects, and credentials, and returns active remote work to a local project when disabled.

Main IPC enforces the same setting while still allowing in-flight cancellation. Application settings migrate to version 9 with the preview disabled.

Release note: 新增“远程项目(技术预览)”独立页签和开关,默认关闭;启用后显示 SSH Host 与远程项目功能,关闭不会删除已有配置。
This commit is contained in:
mesalogo
2026-08-25 00:16:02 +08:00
parent 607bb33249
commit 6c74f057eb
23 changed files with 1267 additions and 115 deletions
+90 -4
View File
@@ -65,6 +65,7 @@ describe('ApplicationSettingsStore', () => {
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
remoteProjectsEnabled: false,
magicNotesEnabled: false,
magicNotesShowIncompleteTodoCount: false,
magicNoteCommentMode: 'immediate',
@@ -74,16 +75,18 @@ describe('ApplicationSettingsStore', () => {
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
remoteProjectsEnabled: false,
magicNotesEnabled: false,
magicNotesShowIncompleteTodoCount: false,
magicNoteCommentMode: 'immediate',
magicNoteCommentFormat: 'combined'
})
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
version: 8,
version: 9,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
remoteProjectsEnabled: false,
magicNotesEnabled: false,
magicNotesShowIncompleteTodoCount: false,
magicNoteCommentMode: 'immediate',
@@ -95,6 +98,29 @@ describe('ApplicationSettingsStore', () => {
).toEqual([])
})
it('persists the Remote Projects preference', async () => {
const { filePath, store } = await createStore()
await expect(
store.update({ remoteProjectsEnabled: true })
).resolves.toEqual({
...defaultApplicationSettings,
remoteProjectsEnabled: true
})
await expect(
new ApplicationSettingsStore(filePath).get()
).resolves.toEqual({
...defaultApplicationSettings,
remoteProjectsEnabled: true
})
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
version: 9,
lastSeenReleaseNotesVersion: null,
...defaultApplicationSettings,
remoteProjectsEnabled: true
})
})
it('creates the parent directory and can reload persisted settings', async () => {
const { directory } = await createStore()
const filePath = join(directory, 'nested', 'application-settings.json')
@@ -110,6 +136,7 @@ describe('ApplicationSettingsStore', () => {
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
remoteProjectsEnabled: false,
magicNotesEnabled: false,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
@@ -134,6 +161,7 @@ describe('ApplicationSettingsStore', () => {
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
remoteProjectsEnabled: false,
magicNotesEnabled: false,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
@@ -158,6 +186,7 @@ describe('ApplicationSettingsStore', () => {
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
@@ -182,6 +211,7 @@ describe('ApplicationSettingsStore', () => {
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'after-save-manual',
@@ -209,10 +239,11 @@ describe('ApplicationSettingsStore', () => {
new ApplicationSettingsStore(filePath).getLastSeenReleaseNotesVersion()
).resolves.toBe('0.8.18')
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
version: 8,
version: 9,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'after-save-manual',
@@ -240,6 +271,7 @@ describe('ApplicationSettingsStore', () => {
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'after-save-auto',
@@ -267,6 +299,7 @@ describe('ApplicationSettingsStore', () => {
checkUpdatesOnStartup: false,
updateSource: 'mirror',
modelDownloadSource: 'modelscope',
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'after-save-auto',
@@ -279,8 +312,9 @@ describe('ApplicationSettingsStore', () => {
await store.update({ modelDownloadSource: 'hugging-face' })
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
...versionSix,
version: 8,
version: 9,
modelDownloadSource: 'hugging-face',
remoteProjectsEnabled: false,
magicNotesShowIncompleteTodoCount: true
})
})
@@ -303,6 +337,7 @@ describe('ApplicationSettingsStore', () => {
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
@@ -313,7 +348,53 @@ describe('ApplicationSettingsStore', () => {
)
})
it('lazily migrates version 8 with Remote Projects disabled', async () => {
const { filePath, store } = await createStore()
const versionEight = {
version: 8,
checkUpdatesOnStartup: false,
updateSource: 'mirror',
modelDownloadSource: 'hugging-face',
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: false,
magicNoteCommentMode: 'after-save-manual',
magicNoteCommentFormat: 'narrative',
warnings: [{ code: 'application-settings-recovered' }],
lastSeenReleaseNotesVersion: '0.8.18'
}
await writeFile(filePath, JSON.stringify(versionEight), 'utf8')
await expect(store.get()).resolves.toEqual({
checkUpdatesOnStartup: false,
updateSource: 'mirror',
modelDownloadSource: 'hugging-face',
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: false,
magicNoteCommentMode: 'after-save-manual',
magicNoteCommentFormat: 'narrative'
})
await expect(store.getLastSeenReleaseNotesVersion()).resolves.toBe(
'0.8.18'
)
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual(
versionEight
)
await store.update({ checkUpdatesOnStartup: true })
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
...versionEight,
version: 9,
checkUpdatesOnStartup: true,
remoteProjectsEnabled: false
})
})
it('strictly rejects incomplete full settings', () => {
expect(
applicationSettingsSchema.safeParse(defaultApplicationSettings)
.success
).toBe(true)
for (const input of [
{},
{ checkUpdatesOnStartup: 'true' },
@@ -337,6 +418,7 @@ describe('ApplicationSettingsStore', () => {
{},
{ checkUpdatesOnStartup: 'true' },
{ modelDownloadSource: 'automatic' },
{ remoteProjectsEnabled: 'false' },
{ anotherSetting: true },
null
]) {
@@ -358,6 +440,7 @@ describe('ApplicationSettingsStore', () => {
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
@@ -473,16 +556,18 @@ describe('ApplicationSettingsStore', () => {
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
remoteProjectsEnabled: false,
magicNotesEnabled: false,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
magicNoteCommentFormat: 'combined'
})
expect(JSON.parse(await readFile(filePath, 'utf8'))).toEqual({
version: 8,
version: 9,
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
remoteProjectsEnabled: false,
magicNotesEnabled: false,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',
@@ -509,6 +594,7 @@ describe('ApplicationSettingsStore', () => {
checkUpdatesOnStartup: false,
updateSource: 'github',
modelDownloadSource: 'modelscope',
remoteProjectsEnabled: false,
magicNotesEnabled: true,
magicNotesShowIncompleteTodoCount: true,
magicNoteCommentMode: 'immediate',