Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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
6 changes: 6 additions & 0 deletions .changeset/persist-fast-model-v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@open-codesign/shared": patch
"@open-codesign/desktop": patch
---

Preserve `modelFast`, `imageGeneration`, and `designSystem` when other settings writes rebuild the on-disk v3 config, so the fast model and related optionals are not cleared after the next provider or import save.
6 changes: 3 additions & 3 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,20 @@
"zustand": "^5.0.2"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.4",
"@tailwindcss/postcss": "^4.0.0",
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^22.10.2",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"electron": "^39.8.9",
"electron": "^39.8.8",
"electron-builder": "^26.8.1",
"electron-builder-squirrel-windows": "26.8.1",
"electron-vite": "^2.3.0",
"react-markdown": "^10.1.0",
"remark-gfm": "^4.0.1",
"tailwindcss": "^4.2.4",
"tailwindcss": "^4.0.0",
"typescript": "^5.7.2",
"vite": "^6.0.5",
"vitest": "^2.1.8"
Expand Down
5 changes: 3 additions & 2 deletions apps/desktop/src/main/codex-oauth-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
ERROR_CODES,
type ProviderEntry,
hydrateConfig,
preservedV3OptionalsForWrite,
} from '@open-codesign/shared';
import { configDir, writeConfig } from './config';
import { ipcMain, shell } from './electron-runtime';
Expand Down Expand Up @@ -133,7 +134,7 @@ async function persistProviderMutation(
activeModel: cfg?.activeModel ?? '',
secrets: cfg?.secrets ?? {},
providers: nextProviders,
...(cfg?.designSystem !== undefined ? { designSystem: cfg.designSystem } : {}),
...preservedV3OptionalsForWrite(cfg),
});
await writeConfig(next);
setCachedConfig(next);
Expand All @@ -155,7 +156,7 @@ async function claimActiveProviderIfUnset(): Promise<void> {
activeModel: CHATGPT_CODEX_PROVIDER.defaultModel,
secrets: cfg.secrets,
providers: cfg.providers,
...(cfg.designSystem !== undefined ? { designSystem: cfg.designSystem } : {}),
...preservedV3OptionalsForWrite(cfg),
});
await writeConfig(next);
setCachedConfig(next);
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/main/image-generation-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
type ImageGenerationSize,
ImageGenerationSizeSchema,
hydrateConfig,
preservedV3OptionalsForWrite,
} from '@open-codesign/shared';
import { writeConfig } from './config';
import { ipcMain } from './electron-runtime';
Expand Down Expand Up @@ -281,7 +282,7 @@ async function updateImageGenerationSettings(
activeModel: cfg.activeModel,
secrets: cfg.secrets,
providers: cfg.providers,
...(cfg.designSystem !== undefined ? { designSystem: cfg.designSystem } : {}),
...preservedV3OptionalsForWrite(cfg, { skipImageGeneration: true }),
imageGeneration: parsed,
});
await writeConfig(config);
Expand Down
32 changes: 28 additions & 4 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,27 @@ const USE_AGENT_RUNTIME = (() => {

const IS_VITEST = process.env['VITEST'] === 'true';

/** No OS title bar; renderer draws TopBar with -webkit-app-region. Register once (not in createWindow — macOS can call createWindow again on activate). */
function registerWindowChromeIpc(): void {
ipcMain.on('window:minimize', () => mainWindow?.minimize());
ipcMain.on('window:maximize', () => {
if (!mainWindow) return;
if (mainWindow.isMaximized()) mainWindow.unmaximize();
else mainWindow.maximize();
});
ipcMain.on('window:close', () => mainWindow?.close());
}

function createWindow(): void {
// frame: false removes the native title bar and window controls; we provide
// a custom top bar in the renderer (see TopBar, WindowControls).
mainWindow = new BrowserWindow({
width: 1280,
height: 820,
minWidth: 960,
minHeight: 640,
autoHideMenuBar: process.platform !== 'darwin',
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
frame: false,
autoHideMenuBar: true,
backgroundColor: BRAND.backgroundColor,
icon: join(__dirname, '../../resources/icon.png'),
show: false,
Expand All @@ -148,7 +161,12 @@ function createWindow(): void {
},
});

mainWindow.on('ready-to-show', () => mainWindow?.show());
mainWindow.on('ready-to-show', () => {
if (process.platform === 'win32' || process.platform === 'linux') {
mainWindow?.setMenuBarVisibility(false);
}
mainWindow?.show();
});
// Null the reference on close so stale IPC sends from async emitters
// (autoUpdater, long-running generate runs) become clean no-ops rather
// than throwing "Object has been destroyed" on a discarded webContents.
Expand Down Expand Up @@ -1084,7 +1102,12 @@ function registerIpcHandlers(db: Database | null): void {
// Inline-comment edits don't need to be tied to whatever provider was
// pinned in the original generate; resolve fresh against the canonical
// active provider so a switch in Settings takes effect immediately.
const hint = payload.model ?? { provider: cfg.provider, modelId: cfg.modelPrimary };
// Prefer modelFast when configured — it's cheaper for quick edits.
const fastModelId = cfg.modelFast ?? null;
const hint = payload.model ?? {
provider: cfg.provider,
modelId: fastModelId ?? cfg.modelPrimary,
};
const active = resolveActiveModel(cfg, hint);
const allowKeyless = active.allowKeyless;
const apiKey = await resolveApiKeyForActive(active.model.provider, allowKeyless);
Expand Down Expand Up @@ -1359,6 +1382,7 @@ if (!IS_VITEST) {
registerDiagnosticsIpc(diagnosticsDb);
setupAutoUpdater();
registerAppMenu();
registerWindowChromeIpc();
createWindow();
void scheduleStartupUpdateCheck();

Expand Down
60 changes: 37 additions & 23 deletions apps/desktop/src/main/onboarding-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
hydrateConfig,
isSupportedOnboardingProvider,
modelsEndpointUrl,
preservedV3OptionalsForWrite,
toPersistedV3,
} from '@open-codesign/shared';
import { buildAuthHeadersForWire } from './auth-headers';
import { defaultConfigDir, readConfig, writeConfig } from './config';
Expand Down Expand Up @@ -180,6 +182,7 @@ function toState(cfg: Config | null): OnboardingState {
hasKey: false,
provider: null,
modelPrimary: null,
modelFast: null,
baseUrl: null,
designSystem: null,
};
Expand All @@ -191,6 +194,7 @@ function toState(cfg: Config | null): OnboardingState {
hasKey: false,
provider: active,
modelPrimary: null,
modelFast: null,
baseUrl: null,
designSystem: cfg.designSystem ?? null,
};
Expand All @@ -199,6 +203,7 @@ function toState(cfg: Config | null): OnboardingState {
hasKey: true,
provider: active,
modelPrimary: cfg.activeModel,
modelFast: cfg.modelFast ?? null,
baseUrl: cfg.providers[active]?.baseUrl ?? null,
designSystem: cfg.designSystem ?? null,
};
Expand All @@ -224,6 +229,7 @@ export async function setDesignSystem(
activeModel: cfg.activeModel,
secrets: cfg.secrets,
providers: cfg.providers,
...preservedV3OptionalsForWrite(cfg, { clearDesignSystem: designSystem === null }),
...(designSystem !== null ? { designSystem: StoredDesignSystem.parse(designSystem) } : {}),
});
await writeConfig(next);
Expand Down Expand Up @@ -376,9 +382,7 @@ async function runSetProviderAndModels(input: SetProviderAndModelsInput): Promis
activeModel: nextActiveModel,
secrets: nextSecrets,
providers: nextProviders,
...(cachedConfig?.designSystem !== undefined
? { designSystem: cachedConfig.designSystem }
: {}),
...preservedV3OptionalsForWrite(cachedConfig),
});
await writeConfig(next);
cachedConfig = next;
Expand Down Expand Up @@ -428,7 +432,7 @@ async function runDeleteProvider(raw: unknown): Promise<ProviderRow[]> {
activeModel: '',
secrets: {},
providers: nextProviders,
...(cfg.designSystem !== undefined ? { designSystem: cfg.designSystem } : {}),
...preservedV3OptionalsForWrite(cfg),
});
await writeConfig(emptyNext);
cachedConfig = emptyNext;
Expand All @@ -441,7 +445,7 @@ async function runDeleteProvider(raw: unknown): Promise<ProviderRow[]> {
activeModel: modelPrimary,
secrets: nextSecrets,
providers: nextProviders,
...(cfg.designSystem !== undefined ? { designSystem: cfg.designSystem } : {}),
...preservedV3OptionalsForWrite(cfg),
});
await writeConfig(next);
cachedConfig = next;
Expand Down Expand Up @@ -472,7 +476,7 @@ async function runSetActiveProvider(raw: unknown): Promise<OnboardingState> {
activeModel: modelPrimary,
secrets: cfg.secrets,
providers: cfg.providers,
...(cfg.designSystem !== undefined ? { designSystem: cfg.designSystem } : {}),
...preservedV3OptionalsForWrite(cfg),
});
await writeConfig(next);
cachedConfig = next;
Expand Down Expand Up @@ -540,7 +544,7 @@ async function runResetOnboarding(): Promise<void> {
activeModel: cfg.activeModel,
secrets: {},
providers: cfg.providers,
...(cfg.designSystem !== undefined ? { designSystem: cfg.designSystem } : {}),
...preservedV3OptionalsForWrite(cfg),
});
await writeConfig(next);
cachedConfig = next;
Expand Down Expand Up @@ -652,9 +656,7 @@ async function runAddCustomProvider(input: AddCustomProviderInput): Promise<Onbo
: (cachedConfig?.activeModel ?? input.defaultModel),
secrets: nextSecrets,
providers: nextProviders,
...(cachedConfig?.designSystem !== undefined
? { designSystem: cachedConfig.designSystem }
: {}),
...preservedV3OptionalsForWrite(cachedConfig),
});
await writeConfig(next);
cachedConfig = next;
Expand Down Expand Up @@ -781,7 +783,7 @@ async function runUpdateProvider(input: UpdateProviderInput): Promise<Onboarding
activeModel: cfg.activeModel,
secrets: nextSecrets,
providers: { ...cfg.providers, [input.id]: updated },
...(cfg.designSystem !== undefined ? { designSystem: cfg.designSystem } : {}),
...preservedV3OptionalsForWrite(cfg),
});
await writeConfig(next);
cachedConfig = next;
Expand Down Expand Up @@ -876,9 +878,7 @@ async function runImportCodex(imported: CodexImport): Promise<OnboardingState> {
activeModel,
secrets: nextSecrets,
providers: nextProviders,
...(cachedConfig?.designSystem !== undefined
? { designSystem: cachedConfig.designSystem }
: {}),
...preservedV3OptionalsForWrite(cachedConfig),
});
await writeConfig(next);
cachedConfig = next;
Expand Down Expand Up @@ -933,9 +933,7 @@ async function runImportClaudeCode(imported: ClaudeCodeImport): Promise<Onboardi
activeModel: nextActiveModel,
secrets: nextSecrets,
providers: nextProviders,
...(cachedConfig?.designSystem !== undefined
? { designSystem: cachedConfig.designSystem }
: {}),
...preservedV3OptionalsForWrite(cachedConfig),
});
await writeConfig(next);
cachedConfig = next;
Expand Down Expand Up @@ -981,9 +979,7 @@ async function runImportGemini(imported: GeminiImport): Promise<OnboardingState>
activeModel: nextActiveModel,
secrets: nextSecrets,
providers: nextProviders,
...(cachedConfig?.designSystem !== undefined
? { designSystem: cachedConfig.designSystem }
: {}),
...preservedV3OptionalsForWrite(cachedConfig),
});
await writeConfig(next);
cachedConfig = next;
Expand Down Expand Up @@ -1027,9 +1023,7 @@ async function runImportOpencode(imported: OpencodeImport): Promise<OnboardingSt
activeModel,
secrets: nextSecrets,
providers: nextProviders,
...(cachedConfig?.designSystem !== undefined
? { designSystem: cachedConfig.designSystem }
: {}),
...preservedV3OptionalsForWrite(cachedConfig),
});
await writeConfig(next);
cachedConfig = next;
Expand Down Expand Up @@ -1153,6 +1147,26 @@ export function registerOnboardingIpc(): void {
},
);

ipcMain.handle('config:v1:set-fast-model', async (_e, raw: unknown): Promise<OnboardingState> => {
const input = raw as { modelFast: string | null };
if (typeof input !== 'object' || input === null || !('modelFast' in input)) {
throw new CodesignError(
'config:v1:set-fast-model expects { modelFast: string | null }',
ERROR_CODES.IPC_BAD_INPUT,
);
}
const modelFast = input.modelFast === '' ? null : input.modelFast;
if (cachedConfig === null) {
throw new CodesignError('No configuration found', ERROR_CODES.CONFIG_MISSING);
}
cachedConfig = hydrateConfig({
...toPersistedV3(cachedConfig),
modelFast: modelFast ?? undefined,
});
await writeConfig(cachedConfig);
return toState(cachedConfig);
});

ipcMain.handle(
'config:v1:detect-external-configs',
async (): Promise<ExternalConfigsDetection> => {
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,8 @@ const api = {
'config:v1:set-active-provider-and-model',
input,
) as Promise<OnboardingState>,
setFastModel: (modelFast: string | null) =>
ipcRenderer.invoke('config:v1:set-fast-model', { modelFast }) as Promise<OnboardingState>,
testEndpoint: (input: {
wire: WireApi;
baseUrl: string;
Expand Down Expand Up @@ -572,6 +574,9 @@ const api = {
},
openExternal: (url: string) =>
ipcRenderer.invoke('codesign:v1:open-external', url) as Promise<void>,
minimize: () => ipcRenderer.send('window:minimize'),
maximize: () => ipcRenderer.send('window:maximize'),
close: () => ipcRenderer.send('window:close'),
};

contextBridge.exposeInMainWorld('codesign', api);
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function App() {
const switchDesign = useCodesignStore((s) => s.switchDesign);
const sendPrompt = useCodesignStore((s) => s.sendPrompt);
const isGenerating = useCodesignStore(
(s) => s.isGenerating && s.generatingDesignId === s.currentDesignId,
(s) => s.currentDesignId !== null && s.activeGenerations.has(s.currentDesignId),
);
const setView = useCodesignStore((s) => s.setView);
const view = useCodesignStore((s) => s.view);
Expand Down
Loading
Loading