mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-14 18:39:33 +01:00
feat(settings): optional sync of case options and selected case across devices
This commit is contained in:
1 parent
ccb76c3325
commit
d62b11586b
7 files changed
+176
No files matched your search
@@ -3572,6 +3572,13 @@
|
||||
</div>
|
||||
<input type="text" id="appSettingsDefaultDir" class="set-input" placeholder="~/projects">
|
||||
</div>
|
||||
<div class="set-row" data-search="sync cases across devices selected case agent teams opus 1m">
|
||||
<div class="set-row-text">
|
||||
<span class="set-row-label">Sync cases across devices</span>
|
||||
<span class="set-row-desc">Keeps each case's Agent Teams and Opus 1M options on the server and switches this device to the case you last chose on another one when you return to the tab. Off by default: each browser keeps its own. The case names are stored in the shared settings file.</span>
|
||||
</div>
|
||||
<label class="switch switch-sm"><input type="checkbox" id="appSettingsSyncCaseState"><span class="slider"></span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -613,6 +613,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
this.updateCasePickerInput(select.value);
|
||||
});
|
||||
select.dataset.listenerAdded = 'true';
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') this._syncCaseStateFromServer();
|
||||
});
|
||||
}
|
||||
this.setupQuickStartCasePicker();
|
||||
// The phone overview labels rows with their case name, and a case rename or
|
||||
@@ -3670,6 +3673,60 @@ Object.assign(CodemanApp.prototype, {
|
||||
|
||||
saveCaseSettings(caseName, settings) {
|
||||
localStorage.setItem('caseSettings_' + caseName, JSON.stringify(settings));
|
||||
if (this._caseStateSyncOn()) this._pushCaseSettings(caseName, settings);
|
||||
},
|
||||
|
||||
// ── Opt-in case sync (`syncCaseState`): case options live on the server, the selected case follows other devices ──
|
||||
|
||||
/** Off unless the user turned it on; a non-admin in multi-user mode cannot write the shared settings.json. */
|
||||
_caseStateSyncOn() {
|
||||
return this.loadAppSettingsFromStorage().syncCaseState === true && !this._settingsReadOnly?.();
|
||||
},
|
||||
|
||||
async _pushCaseSettings(caseName, settings) {
|
||||
try {
|
||||
// PUT replaces the whole map, so merge into the server's current one rather than this device's copy.
|
||||
const current = await fetch('/api/settings').then((r) => r.json()).then((env) => env?.data ?? {});
|
||||
await fetch('/api/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
caseSettings: {
|
||||
...(current.caseSettings || {}),
|
||||
[caseName]: { agentTeams: !!settings.agentTeams, opusContext1m: !!settings.opusContext1m },
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to sync case settings:', err);
|
||||
}
|
||||
},
|
||||
|
||||
/** Server copy wins: write the synced per-case options into the local keys getCaseSettings() reads. */
|
||||
_applyServerCaseSettings(server) {
|
||||
if (server?.syncCaseState !== true || !server.caseSettings) return;
|
||||
try {
|
||||
for (const [name, s] of Object.entries(server.caseSettings)) {
|
||||
localStorage.setItem('caseSettings_' + name, JSON.stringify(s));
|
||||
}
|
||||
} catch { /* storage blocked: this device keeps what it has */ }
|
||||
},
|
||||
|
||||
/** On returning to the tab: take the other devices' case options and selected case. */
|
||||
async _syncCaseStateFromServer() {
|
||||
if (!this._caseStateSyncOn()) return;
|
||||
try {
|
||||
const server = await fetch('/api/settings').then((r) => r.json()).then((env) => env?.data ?? null);
|
||||
this._applyServerCaseSettings(server);
|
||||
const select = document.getElementById('quickStartCase');
|
||||
const last = server?.lastUsedCase;
|
||||
if (select && last && last !== select.value && this.cases?.some((c) => c.name === last)) {
|
||||
select.value = last;
|
||||
this.updateDirDisplayForCase(last);
|
||||
this.updateMobileCaseLabel(last);
|
||||
this.updateCasePickerInput(last);
|
||||
}
|
||||
} catch { /* offline: try again next time the tab is shown */ }
|
||||
},
|
||||
|
||||
onCaseSettingChanged() {
|
||||
|
||||
@@ -560,6 +560,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
// .checked fires no onchange, so the list's visibility (and lazy load)
|
||||
// needs an explicit sync on every open, not just a save.
|
||||
this.applyCliManagementVisibility();
|
||||
document.getElementById('appSettingsSyncCaseState').checked = settings.syncCaseState === true;
|
||||
// MCP server sync: synced, default OFF; same explicit-sync reasoning as above.
|
||||
// The routes read the SAVED setting, so remember what it was on open: switching it on
|
||||
// here does nothing server-side until Save (see mcpSync()).
|
||||
@@ -3139,6 +3140,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
customModelEndpointsEnabled: document.getElementById('appSettingsCustomModelEndpoints').checked,
|
||||
cliManagementEnabled: document.getElementById('appSettingsCliManagement').checked,
|
||||
mcpSyncEnabled: document.getElementById('appSettingsMcpSync').checked,
|
||||
syncCaseState: document.getElementById('appSettingsSyncCaseState').checked,
|
||||
configBackupEnabled: document.getElementById('appSettingsConfigBackup').checked,
|
||||
configBackupDir: document.getElementById('appSettingsConfigBackupDir').value.trim(),
|
||||
...this._configBackupRetentionPayload(_prev),
|
||||
@@ -5280,6 +5282,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
// installs that had saved `false` (agent-skill-default-migration.ts).
|
||||
if (!('agentSkillEnabled' in appSettings)) delete merged.agentSkillEnabled;
|
||||
this.saveAppSettingsToStorage(merged);
|
||||
this._applyServerCaseSettings(settings);
|
||||
|
||||
// Apply notification prefs from server if present (only if localStorage has none)
|
||||
if (notificationPreferences && this.notificationManager) {
|
||||
|
||||
@@ -1461,6 +1461,16 @@ export const SettingsUpdateSchema = z
|
||||
defaultClaudeMdPath: z.string().max(500).optional(),
|
||||
defaultWorkingDir: z.string().max(500).optional(),
|
||||
lastUsedCase: z.string().max(200).optional(),
|
||||
/**
|
||||
* Opt-in, SYNCED, default OFF: per-case run options (Agent Teams, Opus 1M) are kept here instead of
|
||||
* only in each browser, and a device follows the selected case another device chose.
|
||||
*/
|
||||
syncCaseState: z.boolean().optional(),
|
||||
/** Per-case run options, by case name; read and written only while `syncCaseState` is on. */
|
||||
caseSettings: z
|
||||
.record(z.string().max(200), z.object({ agentTeams: z.boolean(), opusContext1m: z.boolean() }).strict())
|
||||
.refine((m) => Object.keys(m).length <= 500, 'too many cases')
|
||||
.optional(),
|
||||
// Feature toggles
|
||||
ralphTrackerEnabled: z.boolean().optional(),
|
||||
subagentTrackingEnabled: z.boolean().optional(),
|
||||
|
||||
Reference in new issue
Block a user