@openchambery/web 1.18.2-beta.2 → 1.18.2-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AssistantView-DvoyOGkx.js → AssistantView-zQDlmCA-.js} +1 -1
- package/dist/assets/{ChatContainer-D6yfuigV.js → ChatContainer-BmLxb00F.js} +4 -4
- package/dist/assets/{DiagramView-MGGo9iME.js → DiagramView-GH3vG8Z5.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-jVOkZkyX.js → MarkdownRendererImpl-tgkSfBku.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-Q2IzHqGA.js → MobileDetailNavigation-CyDXHPw9.js} +1 -1
- package/dist/assets/{MobileShareBridge-apXM34Lb.js → MobileShareBridge-DT2ySSn1.js} +1 -1
- package/dist/assets/{MobileSurface-htGkf7Ua.js → MobileSurface-Bhur0fxY.js} +1 -1
- package/dist/assets/{MultiRunWindow-B-l_O-bd.js → MultiRunWindow-CJQ8RU1e.js} +1 -1
- package/dist/assets/{OnboardingScreen-CUsgwt4y.js → OnboardingScreen-DiPnIwza.js} +1 -1
- package/dist/assets/{OverlayScrollbar-D6NySAxi.js → OverlayScrollbar-DC12_DLb.js} +1 -1
- package/dist/assets/SettingsView-BywZ5HTE.js +25 -0
- package/dist/assets/{SettingsWindow-DfVgTQ4I.js → SettingsWindow-zVXnRr6V.js} +1 -1
- package/dist/assets/{TerminalView-DNyjfkvT.js → TerminalView-CE3CCTsi.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DLG1iThm.js → ToolOutputDialog-C3_vf7HM.js} +1 -1
- package/dist/assets/{appThemeRegistry-CgfOR_0L.js → appThemeRegistry-Y2U1kIMh.js} +2 -2
- package/dist/assets/{clipboard-BW7nk_Cx.js → clipboard-CbqhDr1g.js} +1 -1
- package/dist/assets/{context-menu-BaxmodD8.js → context-menu-wdR3gf8a.js} +1 -1
- package/dist/assets/{dialog-DjYfj1Sn.js → dialog--cf4dygO.js} +1 -1
- package/dist/assets/{es-5Lhudh0n.js → es-DCoGZljP.js} +1 -1
- package/dist/assets/{fr-Q_UhFmKT.js → fr-dkEoZpPz.js} +2 -2
- package/dist/assets/{gitApi-BYxg_qGJ.js → gitApi-BL_etF0B.js} +1 -1
- package/dist/assets/{insertionBoundaries-DzXMTleb.js → insertionBoundaries-CjCybFIP.js} +1 -1
- package/dist/assets/{ja-CzMPz7bT.js → ja-bQwSsvRr.js} +1 -1
- package/dist/assets/{ko-B-EslUsR.js → ko-Kq5C3hsv.js} +1 -1
- package/dist/assets/{main-CO6z5yRB.js → main-Bpwq1Xh1.js} +2 -2
- package/dist/assets/{main-LDphyc-v.js → main-DIVJIea3.js} +7 -7
- package/dist/assets/{miniChat-BZQwTZpx.js → miniChat-BrYw8M4I.js} +2 -2
- package/dist/assets/{mobile-BktO3_4R.js → mobile-i4Pu_Tak.js} +2 -2
- package/dist/assets/{multirun-ZlwlkIy5.js → multirun-OJ8angjt.js} +1 -1
- package/dist/assets/{pl-Dnea6Ppc.js → pl-CYpMAgVn.js} +1 -1
- package/dist/assets/projectMeta-Dt0loPh9.js +3 -0
- package/dist/assets/{pt-BR-Ca6QTem7.js → pt-BR-MSPTLfTm.js} +1 -1
- package/dist/assets/{radio-CJuhbQ5a.js → radio-SXFBBh2O.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-Db8uO-6q.js → renderElectronMiniChatApp-BBb29zbe.js} +2 -2
- package/dist/assets/{renderMobileApp-BCV81Ryl.js → renderMobileApp-Ctj7sqAE.js} +5 -5
- package/dist/assets/{runtimeEndpointReset-Dg3DMy9v.js → runtimeEndpointReset-By8IWO59.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-D8JHUI0z.js → sortable-tabs-strip-8qgYIxgY.js} +1 -1
- package/dist/assets/src-DOs1ZW0T.css +2 -0
- package/dist/assets/{store-wa1ersRf.js → store-OURVnLSN.js} +2 -2
- package/dist/assets/{uk-xGc9Pzge.js → uk-cSkRAYFY.js} +1 -1
- package/dist/assets/{useAppFontEffects-CM0VKsdo.js → useAppFontEffects-D81CYdad.js} +3 -3
- package/dist/assets/{useDesktopWindowControlsLayout-Cq-r4Hcb.js → useDesktopWindowControlsLayout-BWOeSiA5.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-aUC3zzl_.js → useEffectiveDirectory-BNDb_82A.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-DnzT4jO2.js → useMobileNavigationStore-DBYUoTQ0.js} +4 -4
- package/dist/assets/useSessionAutoCleanup-Cc_AwEoh.js +1 -0
- package/dist/assets/{useWorkerHighlightedLines-CiYQjBe6.js → useWorkerHighlightedLines-DEhp2GTh.js} +1 -1
- package/dist/assets/{zh-CN-sL4ktGB3.js → zh-CN-BKqeT_uz.js} +1 -1
- package/dist/assets/{zh-TW-Cfxps4gq.js → zh-TW-BaNjJ8Y4.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/mini-chat.html +2 -2
- package/dist/mobile.html +2 -2
- package/package.json +1 -1
- package/server/index.d.ts +6 -0
- package/server/index.js +6 -0
- package/server/lib/config-sync/DOCUMENTATION.md +70 -0
- package/server/lib/config-sync/config-sync.test.js +197 -0
- package/server/lib/config-sync/constants.js +39 -0
- package/server/lib/config-sync/contract.js +130 -0
- package/server/lib/config-sync/credential-auth.js +39 -0
- package/server/lib/config-sync/engine.js +108 -0
- package/server/lib/config-sync/index.js +67 -0
- package/server/lib/config-sync/local-backup.js +171 -0
- package/server/lib/config-sync/plan.js +318 -0
- package/server/lib/config-sync/receiver.js +330 -0
- package/server/lib/config-sync/routes.js +141 -0
- package/server/lib/config-sync/routes.test.js +154 -0
- package/server/lib/config-sync/scripts.js +264 -0
- package/server/lib/config-sync/selections.js +112 -0
- package/server/lib/config-sync/tar.js +35 -0
- package/server/lib/config-sync/target-id.js +24 -0
- package/server/lib/opencode/DOCUMENTATION.md +3 -0
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/core-routes.js +1 -0
- package/server/lib/opencode/openchamber-routes.js +7 -0
- package/server/lib/opencode/settings-runtime.js +82 -51
- package/server/lib/opencode/settings-runtime.test.js +42 -0
- package/server/lib/relay/DOCUMENTATION.md +1 -1
- package/dist/assets/SettingsView-DxMpVLlt.js +0 -25
- package/dist/assets/projectMeta-v-LP2mzz.js +0 -3
- package/dist/assets/src-oHrYIqHF.css +0 -2
- package/dist/assets/useSessionAutoCleanup-DxZeceKO.js +0 -1
|
@@ -39,9 +39,41 @@ export const createSettingsRuntime = (deps) => {
|
|
|
39
39
|
normalizeStringArray,
|
|
40
40
|
formatSettingsResponse,
|
|
41
41
|
resolveDirectoryCandidate,
|
|
42
|
+
/**
|
|
43
|
+
* Optional late-bound exclusive runner (Electron injects after startWebUiServer).
|
|
44
|
+
* May be a function returning the runner, or the runner itself.
|
|
45
|
+
*/
|
|
46
|
+
getRunExclusivePersist,
|
|
47
|
+
runExclusivePersist: initialRunExclusivePersist,
|
|
42
48
|
} = deps;
|
|
43
49
|
|
|
44
50
|
let persistSettingsLock = Promise.resolve();
|
|
51
|
+
let runExclusivePersist = typeof initialRunExclusivePersist === 'function'
|
|
52
|
+
? initialRunExclusivePersist
|
|
53
|
+
: null;
|
|
54
|
+
|
|
55
|
+
const resolveRunExclusivePersist = () => {
|
|
56
|
+
if (typeof getRunExclusivePersist === 'function') {
|
|
57
|
+
const resolved = getRunExclusivePersist();
|
|
58
|
+
if (typeof resolved === 'function') return resolved;
|
|
59
|
+
}
|
|
60
|
+
return typeof runExclusivePersist === 'function' ? runExclusivePersist : null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const runPersistedWrite = (work) => {
|
|
64
|
+
const external = resolveRunExclusivePersist();
|
|
65
|
+
if (external) {
|
|
66
|
+
return external(work);
|
|
67
|
+
}
|
|
68
|
+
const next = persistSettingsLock.then(async () => work());
|
|
69
|
+
// Keep the chain alive even if one writer throws.
|
|
70
|
+
persistSettingsLock = next.catch(() => {});
|
|
71
|
+
return next;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const setRunExclusivePersist = (fn) => {
|
|
75
|
+
runExclusivePersist = typeof fn === 'function' ? fn : null;
|
|
76
|
+
};
|
|
45
77
|
|
|
46
78
|
// Orphan recovery is a one-shot best-effort scan: when orphans can't be
|
|
47
79
|
// matched on first pass they stay on disk and every subsequent settings
|
|
@@ -495,7 +527,7 @@ export const createSettingsRuntime = (deps) => {
|
|
|
495
527
|
await fsPromises.rm(tmp, { force: true });
|
|
496
528
|
};
|
|
497
529
|
|
|
498
|
-
const
|
|
530
|
+
const writeSettingsToDiskUnlocked = async (settings) => {
|
|
499
531
|
try {
|
|
500
532
|
await fsPromises.mkdir(path.dirname(SETTINGS_FILE_PATH), { recursive: true });
|
|
501
533
|
// Atomic write: Electron main and ssh-manager read this file via plain
|
|
@@ -511,6 +543,8 @@ export const createSettingsRuntime = (deps) => {
|
|
|
511
543
|
}
|
|
512
544
|
};
|
|
513
545
|
|
|
546
|
+
const writeSettingsToDisk = async (settings) => runPersistedWrite(() => writeSettingsToDiskUnlocked(settings));
|
|
547
|
+
|
|
514
548
|
const validateProjectEntries = async (projects) => {
|
|
515
549
|
if (!Array.isArray(projects)) {
|
|
516
550
|
return [];
|
|
@@ -778,67 +812,63 @@ export const createSettingsRuntime = (deps) => {
|
|
|
778
812
|
const migration7 = migrateSettingsRemoveApprovedDirectories(migration6.settings);
|
|
779
813
|
const migration8 = migrateSettingsCompactChatDefaults(migration7.settings);
|
|
780
814
|
if (migration1.changed || migration2.changed || migration3.changed || migration4.changed || migration5.changed || migration6.changed || migration7.changed || migration8.changed) {
|
|
781
|
-
await
|
|
815
|
+
await runPersistedWrite(() => writeSettingsToDiskUnlocked(migration8.settings));
|
|
782
816
|
}
|
|
783
817
|
return migration8.settings;
|
|
784
818
|
};
|
|
785
819
|
|
|
786
|
-
const persistSettings = async (changes) => {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
let next = mergePersistedSettings(current, sanitized);
|
|
794
|
-
|
|
795
|
-
const normalizedState = normalizeSettingsPaths(next);
|
|
796
|
-
if (normalizedState.changed) {
|
|
797
|
-
next = normalizedState.settings;
|
|
798
|
-
}
|
|
820
|
+
const persistSettings = async (changes) => runPersistedWrite(async () => {
|
|
821
|
+
// Log field names only — changes can carry credentials (UI password,
|
|
822
|
+
// client tokens) that must never reach the log file.
|
|
823
|
+
console.log('[persistSettings] Updating fields:', Object.keys(changes || {}).join(', ') || '(none)');
|
|
824
|
+
const current = await readSettingsFromDisk();
|
|
825
|
+
const sanitized = sanitizeSettingsUpdate(changes);
|
|
826
|
+
let next = mergePersistedSettings(current, sanitized);
|
|
799
827
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
828
|
+
const normalizedState = normalizeSettingsPaths(next);
|
|
829
|
+
if (normalizedState.changed) {
|
|
830
|
+
next = normalizedState.settings;
|
|
831
|
+
}
|
|
804
832
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
833
|
+
const deterministicProjectIdMigration = await migrateSettingsToDeterministicProjectIds(next);
|
|
834
|
+
if (deterministicProjectIdMigration.changed) {
|
|
835
|
+
next = deterministicProjectIdMigration.settings;
|
|
836
|
+
}
|
|
809
837
|
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
}
|
|
838
|
+
const approvedDirectoriesMigration = migrateSettingsRemoveApprovedDirectories(next);
|
|
839
|
+
if (approvedDirectoriesMigration.changed) {
|
|
840
|
+
next = approvedDirectoriesMigration.settings;
|
|
841
|
+
}
|
|
815
842
|
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
next = { ...next, projects: validated };
|
|
822
|
-
}
|
|
843
|
+
// Ensure marker + new defaults exist before the first settings write lands.
|
|
844
|
+
const compactChatDefaultsMigration = migrateSettingsCompactChatDefaults(next);
|
|
845
|
+
if (compactChatDefaultsMigration.changed) {
|
|
846
|
+
next = compactChatDefaultsMigration.settings;
|
|
847
|
+
}
|
|
823
848
|
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
} else if (next.activeProjectId) {
|
|
832
|
-
console.log(`[persistSettings] No projects found, clearing activeProjectId ${next.activeProjectId}`);
|
|
833
|
-
next = { ...next, activeProjectId: undefined };
|
|
834
|
-
}
|
|
849
|
+
// Validating project paths hits the filesystem for every entry, so only
|
|
850
|
+
// do it when the incoming update actually touches the projects list —
|
|
851
|
+
// not on every theme/window-state/etc. save.
|
|
852
|
+
if (Object.prototype.hasOwnProperty.call(sanitized, 'projects') && Array.isArray(next.projects)) {
|
|
853
|
+
const validated = await validateProjectEntries(next.projects);
|
|
854
|
+
next = { ...next, projects: validated };
|
|
855
|
+
}
|
|
835
856
|
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
857
|
+
if (Array.isArray(next.projects) && next.projects.length > 0) {
|
|
858
|
+
const activeId = typeof next.activeProjectId === 'string' ? next.activeProjectId : '';
|
|
859
|
+
const active = next.projects.find((project) => project.id === activeId) || null;
|
|
860
|
+
if (!active) {
|
|
861
|
+
console.log(`[persistSettings] Active project ID ${activeId} not found, switching to ${next.projects[0].id}`);
|
|
862
|
+
next = { ...next, activeProjectId: next.projects[0].id };
|
|
863
|
+
}
|
|
864
|
+
} else if (next.activeProjectId) {
|
|
865
|
+
console.log(`[persistSettings] No projects found, clearing activeProjectId ${next.activeProjectId}`);
|
|
866
|
+
next = { ...next, activeProjectId: undefined };
|
|
867
|
+
}
|
|
839
868
|
|
|
840
|
-
|
|
841
|
-
|
|
869
|
+
await writeSettingsToDiskUnlocked(next);
|
|
870
|
+
return formatSettingsResponse(next);
|
|
871
|
+
});
|
|
842
872
|
|
|
843
873
|
return {
|
|
844
874
|
readSettingsFromDisk,
|
|
@@ -846,5 +876,6 @@ export const createSettingsRuntime = (deps) => {
|
|
|
846
876
|
readSettingsFromDiskMigrated,
|
|
847
877
|
writeSettingsToDisk,
|
|
848
878
|
persistSettings,
|
|
879
|
+
setRunExclusivePersist,
|
|
849
880
|
};
|
|
850
881
|
};
|
|
@@ -190,6 +190,48 @@ describe('settings runtime', () => {
|
|
|
190
190
|
}
|
|
191
191
|
});
|
|
192
192
|
|
|
193
|
+
it('joins an external exclusive persist runner when provided', async () => {
|
|
194
|
+
const { runtime, settingsFilePath, cleanup } = await createRuntime();
|
|
195
|
+
try {
|
|
196
|
+
const order = [];
|
|
197
|
+
let releaseOuter;
|
|
198
|
+
const outerGate = new Promise((resolve) => {
|
|
199
|
+
releaseOuter = resolve;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
runtime.setRunExclusivePersist(async (work) => {
|
|
203
|
+
order.push('lock-enter');
|
|
204
|
+
const result = await work();
|
|
205
|
+
order.push('lock-exit');
|
|
206
|
+
return result;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const pending = (async () => {
|
|
210
|
+
order.push('outer-start');
|
|
211
|
+
await outerGate;
|
|
212
|
+
order.push('outer-before-write');
|
|
213
|
+
await runtime.writeSettingsToDisk({ theme: 'external' });
|
|
214
|
+
order.push('outer-after-write');
|
|
215
|
+
})();
|
|
216
|
+
|
|
217
|
+
// Without awaiting the outer gate, ensure the lock wrapper is the one used.
|
|
218
|
+
releaseOuter();
|
|
219
|
+
await pending;
|
|
220
|
+
expect(order).toEqual([
|
|
221
|
+
'outer-start',
|
|
222
|
+
'outer-before-write',
|
|
223
|
+
'lock-enter',
|
|
224
|
+
'lock-exit',
|
|
225
|
+
'outer-after-write',
|
|
226
|
+
]);
|
|
227
|
+
await expect(fsPromises.readFile(settingsFilePath, 'utf8')).resolves.toBe(
|
|
228
|
+
JSON.stringify({ theme: 'external' }, null, 2),
|
|
229
|
+
);
|
|
230
|
+
} finally {
|
|
231
|
+
await cleanup();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
193
235
|
it.skipIf(process.platform !== 'win32')('falls back when Windows blocks atomic settings replacement', async () => {
|
|
194
236
|
const tempRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-settings-runtime-'));
|
|
195
237
|
const settingsFilePath = path.join(tempRoot, 'settings.json');
|
|
@@ -50,7 +50,7 @@ The host dispatcher restricts tunneled traffic to explicit path allowlists (one
|
|
|
50
50
|
Private Relay is transparent for allowlisted HTTP paths, so most “works on LAN / Desktop, empty on mobile Relay” bugs are Host routing or client transport-identity mistakes rather than Relay framing bugs:
|
|
51
51
|
|
|
52
52
|
- **Same path, real Host route required.** Clients call the ordinary `/api/...` path through the tunnel. If that route is missing on the Host process currently holding the relay claim (wrong git worktree, stale packaged Desktop build, or a backend that never registered the OpenChamber route before the OpenCode proxy/SPA fallback), the tunnel still returns HTTP 200 with SPA HTML or proxied OpenCode content. Symptom: chat and status work, Provider/model catalog does not.
|
|
53
|
-
- **HTTP allowlist is prefix-based for `/api/`.** New REST/SSE APIs under `/api/` do not need a tunnel-host allowlist edit. New **WebSocket** paths still need both `ALLOWED_WS_PATHS` and `isUrlAuthWebSocketPath` (see the `relay-transport` skill).
|
|
53
|
+
- **HTTP allowlist is prefix-based for `/api/`.** New REST/SSE APIs under `/api/` do not need a tunnel-host allowlist edit. New **WebSocket** paths still need both `ALLOWED_WS_PATHS` and `isUrlAuthWebSocketPath` (see the `relay-transport` skill). Config sync (`/api/openchamber/config-sync/*`, ticket 05/06) therefore works over Relay without a tunnel-host allowlist change; identity and credential grants remain endpoint concerns (`relay:<serverId>`, pairing-settings / inbound host grant).
|
|
54
54
|
- **Transport identity ≠ runtime key.** After LAN⇄relay swaps, UI catalog loaders commit only when `useConfigStore.catalogTransportIdentity` matches `getRuntimeTransportIdentity()`. `runtimeEndpointReset.ts` must write that transport fingerprint on both full endpoint reset and in-place transport reconnect. Writing `runtimeKey` instead leaves Providers empty because the stable device/instance id is shared across LAN and relay.
|
|
55
55
|
- **Safe catalog projection stays Host-owned.** Provider credentials never cross the browser; Relay only carries the already-allowlisted `GET /api/config/catalog/providers` JSON. Keep Host projection, client parser bounds, and `partial` rules in sync (empty `release_date` is absent, not partial).
|
|
56
56
|
|