Skip to content
Open
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
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.
10 changes: 7 additions & 3 deletions apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"test": "vitest run --passWithNoTests"
},
"dependencies": {
"@monaco-editor/react": "^4.7.0",
"@open-codesign/artifacts": "workspace:*",
"@open-codesign/core": "workspace:*",
"@open-codesign/exporters": "workspace:*",
Expand All @@ -27,31 +28,34 @@
"@open-codesign/shared": "workspace:*",
"@open-codesign/templates": "workspace:*",
"@open-codesign/ui": "workspace:*",
"@shikijs/monaco": "^4.0.2",
"better-sqlite3": "^12.9.0",
"electron-log": "^5",
"electron-updater": "^6.3.9",
"lucide-react": "^1.8.0",
"monaco-editor": "^0.55.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"shiki": "^4.0.2",
"smol-toml": "^1.6.1",
"zip-lib": "^1.0.4",
"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