feat: refactor frontend to Svelte 5 + TS + Vite & macOS glassmorphism UI#259
feat: refactor frontend to Svelte 5 + TS + Vite & macOS glassmorphism UI#259Cmochance wants to merge 6 commits into
Conversation
| "guide.step2": "Apply Once", | ||
| "guide.step2Text": "Click Generate Codex CLI config. This app generates the environment variable commands.", | ||
| "guide.step3": "Set environment variables", | ||
| "guide.step3Text": "After restart, ask as usual. This app forwards messages to the active provider.", | ||
| "guide.step4": "Start using Codex CLI", | ||
| "guide.step4Text": "Select 3P mode after restart.", | ||
| "guide.start": "Start", |
There was a problem hiding this comment.
🔴 Duplicate i18n keys in English dictionary cause Guide page to show wrong text
In the English (en) dictionary, several guide.* keys are duplicated with completely different values. In JavaScript, duplicate object literal keys silently resolve to the last value. This overwrites the correct, detailed translations (lines 926–962) with terse/incorrect stubs (lines 963–969). For example, guide.step2 changes from "Set Default" to "Apply Once", guide.step4 from "Run codex in a Terminal" to "Start using Codex CLI", and guide.start from "Add Your First Provider" to just "Start". The entire English Guide page Quick Start section displays wrong step titles, truncated descriptions, and a meaningless CTA label.
Affected duplicate keys and their overwritten values
guide.step2: "Set Default" → "Apply Once"guide.step2Text: full paragraph → "Click Generate Codex CLI config..."guide.step3: "Apply (Automatic)" → "Set environment variables"guide.step3Text: full paragraph → "After restart, ask as usual..."guide.step4: "Run codex in a Terminal" → "Start using Codex CLI"guide.step4Text: full paragraph → "Select 3P mode after restart."guide.start: "Add Your First Provider" → "Start"
| "guide.step2": "Apply Once", | |
| "guide.step2Text": "Click Generate Codex CLI config. This app generates the environment variable commands.", | |
| "guide.step3": "Set environment variables", | |
| "guide.step3Text": "After restart, ask as usual. This app forwards messages to the active provider.", | |
| "guide.step4": "Start using Codex CLI", | |
| "guide.step4Text": "Select 3P mode after restart.", | |
| "guide.start": "Start", |
Was this helpful? React with 👍 or 👎 to provide feedback.
| function handleHashChange() { | ||
| const hash = window.location.hash || '#dashboard'; | ||
| const cleanHash = hash.replace(/^#/, ''); | ||
|
|
||
| // Parse routes | ||
| if (cleanHash.startsWith('providers/edit/')) { | ||
| activeTab.set('providers/edit'); | ||
| routeParams = { id: cleanHash.substring('providers/edit/'.length) }; | ||
| } else { | ||
| activeTab.set(cleanHash); | ||
| routeParams = {}; | ||
| } | ||
| } |
There was a problem hiding this comment.
🔴 Hash router doesn't strip query parameters, causing 404 when adding preset providers
In App.svelte:19-30, handleHashChange() sets activeTab to the raw hash string without stripping query parameters. When Dashboard.svelte:200 navigates via window.location.hash = 'providers/add?preset=${presetId}', the hashchange handler sets activeTab to 'providers/add?preset=deepseek'. This string doesn't match the $activeTab === 'providers/add' condition in frontend/src/App.svelte:66, so the router falls through to the 404 catch-all. Users clicking a preset from the Dashboard to add a new provider see a "404 Not Found" page instead of the ProvidersAdd form.
| function handleHashChange() { | |
| const hash = window.location.hash || '#dashboard'; | |
| const cleanHash = hash.replace(/^#/, ''); | |
| // Parse routes | |
| if (cleanHash.startsWith('providers/edit/')) { | |
| activeTab.set('providers/edit'); | |
| routeParams = { id: cleanHash.substring('providers/edit/'.length) }; | |
| } else { | |
| activeTab.set(cleanHash); | |
| routeParams = {}; | |
| } | |
| } | |
| function handleHashChange() { | |
| const hash = window.location.hash || '#dashboard'; | |
| const cleanHash = hash.replace(/^#/, '').split('?')[0]; | |
| // Parse routes | |
| if (cleanHash.startsWith('providers/edit/')) { | |
| activeTab.set('providers/edit'); | |
| routeParams = { id: cleanHash.substring('providers/edit/'.length) }; | |
| } else { | |
| activeTab.set(cleanHash); | |
| routeParams = {}; | |
| } | |
| } |
Was this helpful? React with 👍 or 👎 to provide feedback.
| "fullscreen": false, | ||
| "transparent": true, |
There was a problem hiding this comment.
🔴 Transparent window on Windows/Linux without vibrancy makes app content unreadable
This PR adds "transparent": true to tauri.conf.json:22 for all platforms, but window_vibrancy::apply_vibrancy is only applied on macOS (src-tauri/src/main.rs:64). The CSS in frontend/src/app.css:141 sets body { background-color: transparent } and #app-root uses --mac-bg-window: rgba(255, 255, 255, 0.4) (40% opaque white). On Windows and Linux, the window will be truly transparent with no native blur behind it, rendering the app nearly unreadable as desktop content shows through the semi-transparent UI. Before this PR, transparent was not set (defaults to false), so this is a regression for Windows and Linux users.
Prompt for agents
The problem is that transparent: true and titleBarStyle: Transparent in tauri.conf.json are macOS-specific features that create a broken experience on Windows and Linux. window_vibrancy::apply_vibrancy is only called under #[cfg(target_os = "macos")] in src-tauri/src/main.rs:64-71, but the CSS (frontend/src/app.css) assumes a vibrancy backdrop with rgba backgrounds everywhere. On Windows/Linux, users see through the window to the desktop.
Possible fixes:
1. Make transparent/titleBarStyle conditional per-platform in the Tauri config (Tauri 2 supports platform-specific window config).
2. Add a Windows vibrancy equivalent using window_vibrancy::apply_acrylic or apply_mica for Windows in main.rs.
3. Add a CSS fallback that uses solid background colors when not on macOS (e.g., detect via a data attribute set from Rust on setup).
4. At minimum, set body/app-root background to a solid color and only use transparent backgrounds when vibrancy is confirmed active.
Was this helpful? React with 👍 or 👎 to provide feedback.
| payload.grokWeb = { | ||
| cookies, | ||
| statsigId: grokStatsigId.trim(), | ||
| userAgent: grokUserAgent.trim(), | ||
| }; |
There was a problem hiding this comment.
🔴 Grok Web payload always sends empty statsigId/userAgent strings instead of omitting them
In ProvidersAdd.svelte:210-214, statsigId and userAgent are always included in the grokWeb payload even when empty (grokStatsigId.trim() returns ""). The old code in frontend-old/js/app.js:1430-1432 only included these fields conditionally (if (statsigId) payload.statsigId = statsigId). The backend Grok Web auth (crates/adapters/src/grok_web/auth.rs) auto-generates a Statsig blob when the field is absent but likely uses an empty string as-is when present, which would break Grok Web requests by sending an empty x-statsig-id header instead of the dynamically generated one.
| payload.grokWeb = { | |
| cookies, | |
| statsigId: grokStatsigId.trim(), | |
| userAgent: grokUserAgent.trim(), | |
| }; | |
| payload.grokWeb = { | |
| cookies, | |
| ...(grokStatsigId.trim() ? { statsigId: grokStatsigId.trim() } : {}), | |
| ...(grokUserAgent.trim() ? { userAgent: grokUserAgent.trim() } : {}), | |
| }; |
Was this helpful? React with 👍 or 👎 to provide feedback.
| grokWeb?: { | ||
| sso?: string; | ||
| cookieString?: string; | ||
| cfClearance?: string; | ||
| ssoRw?: string; | ||
| statsigId?: string; | ||
| userAgent?: string; | ||
| }; |
There was a problem hiding this comment.
🟡 Provider.grokWeb TypeScript interface doesn't match actual runtime payload structure
The Provider.grokWeb TypeScript interface at api.ts:40-47 defines flat fields (sso, cookieString, cfClearance, ssoRw, statsigId, userAgent), but the actual runtime payload built in ProvidersAdd.svelte:210-214 uses a nested cookies object: { cookies: { sso, 'sso-rw', cf_clearance, cookieString }, statsigId, userAgent }. The nested structure is correct — it matches both the old frontend's collectGrokWebPayload() (frontend-old/js/app.js:1426-1432) and the backend's GrokCookies::from_provider() which reads provider.extra["grokWeb"]["cookies"] (crates/adapters/src/grok_web/auth.rs:108-111). However, the TypeScript interface is misleading: any code that reads provider.grokWeb.sso (trusting the interface) would get undefined at runtime because the actual value lives at provider.grokWeb.cookies.sso.
| grokWeb?: { | |
| sso?: string; | |
| cookieString?: string; | |
| cfClearance?: string; | |
| ssoRw?: string; | |
| statsigId?: string; | |
| userAgent?: string; | |
| }; | |
| grokWeb?: { | |
| cookies?: Record<string, string>; | |
| statsigId?: string; | |
| userAgent?: string; | |
| }; |
Was this helpful? React with 👍 or 👎 to provide feedback.
| "identifier": "store.alyse.codex-app-transfer", | ||
| "build": { | ||
| "frontendDist": "../frontend" | ||
| "frontendDist": "../frontend/dist" |
There was a problem hiding this comment.
🔴 Missing beforeBuildCommand/beforeDevCommand in tauri.conf.json makes the app unbuildable
The PR switches the frontend from static HTML/CSS/JS (served directly from frontend/) to a Svelte 5 + Vite app that requires a build step (npm run build in frontend/) to produce frontend/dist/. Both tauri.conf.json:7 (frontendDist: "../frontend/dist") and src-tauri/src/admin/static_files.rs:13 (include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist")) now point to frontend/dist/, but this directory doesn't exist at build time — it's a Vite build output and is gitignored (frontend/.gitignore line 11). Without beforeBuildCommand (e.g., "cd frontend && npm install && npm run build") and beforeDevCommand (e.g., "cd frontend && npm run dev") in the build section of tauri.conf.json, neither cargo tauri build, cargo tauri dev, nor make mac-app will produce a working application. The CI workflow (rust-tauri-check) also has no npm/node step before cargo check -p codex-app-transfer, so the include_dir! macro will fail to resolve the missing directory.
Prompt for agents
The build section of tauri.conf.json needs beforeBuildCommand and beforeDevCommand to handle the Svelte/Vite frontend build. In src-tauri/tauri.conf.json, add to the "build" object:
"beforeBuildCommand": "cd frontend && npm install && npm run build"
"beforeDevCommand": "cd frontend && npm run dev"
Additionally, the CI workflow (.github/workflows/ci.yml) in the rust-tauri-check job needs a step to install Node.js and build the frontend before running cargo check. Something like:
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Build Svelte frontend
run: cd frontend && npm ci && npm run build
This must come before the cargo check step. Without these changes, the include_dir! macro in static_files.rs will fail because frontend/dist does not exist.
Was this helpful? React with 👍 or 👎 to provide feedback.
| {$t('nav.' + $activeTab, { defaultValue: 'Codex App Transfer' })} | ||
| </div> | ||
|
|
||
| <!-- Actions on the right --> |
There was a problem hiding this comment.
🟡 Titlebar displays raw i18n key for provider add/edit routes because defaultValue is not implemented
In Titlebar.svelte:41, the title is computed as $t('nav.' + $activeTab, { defaultValue: 'Codex App Transfer' }). When $activeTab is 'providers/add' or 'providers/edit', this constructs keys like nav.providers/add that don't exist in the i18n dictionaries (frontend/src/lib/i18n.ts). The t() function at frontend/src/lib/i18n.ts:1044 treats the vars parameter purely as string interpolation variables (replacing {key} patterns), not as an options object with defaultValue support. When a key is missing, it falls through to returning the raw key string — so the titlebar will display the literal text nav.providers/add or nav.providers/edit instead of a friendly title like "Codex App Transfer".
Prompt for agents
There are two options to fix this:
1. Add defaultValue support to the t() function in frontend/src/lib/i18n.ts. In the derived store callback, check if vars has a special 'defaultValue' key, and if the dictionary lookup returns the raw key (meaning no translation was found), return the defaultValue instead. Make sure to delete 'defaultValue' from vars before doing the {key} pattern replacements.
2. Alternatively, fix the Titlebar.svelte to handle the case where activeTab contains sub-routes. For example, extract the base tab name (e.g. 'providers' from 'providers/add') and use that for the lookup, or maintain a separate mapping of route-to-title. A simpler fix would be:
const baseTab = $activeTab.split('/')[0];
$t('nav.' + baseTab, { defaultValue: 'Codex App Transfer' })
Combined with implementing defaultValue support in the t() function.
Was this helpful? React with 👍 or 👎 to provide feedback.
| if (isEditMode && id) { | ||
| const providers = await CCApi.getProviders(); | ||
| const editing = providers.find(p => p.id === id); | ||
| if (editing) { | ||
| name = editing.name; | ||
| baseUrl = editing.baseUrl; | ||
| apiKey = ''; // Always clear API key input for editing | ||
| hasSavedKey = !!editing.hasApiKey; | ||
| authScheme = editing.authScheme; | ||
| apiFormat = editing.apiFormat; | ||
| mappings = { | ||
| default: editing.mappings?.default || '', | ||
| gpt_5_5: editing.mappings?.gpt_5_5 || '', | ||
| gpt_5_4: editing.mappings?.gpt_5_4 || '', | ||
| gpt_5_4_mini: editing.mappings?.gpt_5_4_mini || '', | ||
| gpt_5_3_codex: editing.mappings?.gpt_5_3_codex || '', | ||
| gpt_5_2: editing.mappings?.gpt_5_2 || '', | ||
| }; |
There was a problem hiding this comment.
🔴 Grok Web cookie fields not populated when editing an existing grok_web provider, causing data loss on save
In ProvidersAdd.svelte:66-83, when loading an existing provider for editing, the grok-specific form fields (grokSso, grokSsoRw, grokCfClearance, grokCookieString, grokStatsigId, grokUserAgent) are never populated from the provider's saved data. These fields stay at their initialized empty-string values. When the user saves the edited provider, getPayload() at line 204-216 builds grokWeb.cookies.sso from the empty grokSso variable, producing { cookies: { sso: "" } }. The backend validation at src-tauri/src/admin/handlers/providers/crud.rs:69-78 requires grokWeb.cookies.sso to be a non-empty string and will reject the save with a validation error. Even if validation were relaxed, the empty values would overwrite the previously saved cookie data, effectively wiping the user's Grok authentication credentials.
Prompt for agents
In ProvidersAdd.svelte's loadInitialData function, after loading the existing provider for editing (around line 83), add code to populate the grok web form fields from the provider's saved grokWeb data. The provider data from the API may have grokWeb in the shape { cookies: { sso, sso-rw, cf_clearance, cookieString }, statsigId, userAgent }. You need to:
1. First, check if the backend returns grokWeb in the provider list response (it may be stripped for security). If so, you may need a separate API call like CCApi.getProviderSecret(id) to retrieve the sensitive cookie data.
2. Once retrieved, populate the form fields:
grokSso = editing.grokWeb?.cookies?.sso || '';
grokSsoRw = editing.grokWeb?.cookies?.['sso-rw'] || '';
grokCfClearance = editing.grokWeb?.cookies?.cf_clearance || '';
grokCookieString = editing.grokWeb?.cookies?.cookieString || '';
grokStatsigId = editing.grokWeb?.statsigId || '';
grokUserAgent = editing.grokWeb?.userAgent || '';
3. Also consider setting a hasSavedGrokWeb flag (similar to hasSavedKey for API keys) so the form can show placeholder text indicating values are saved, and only send new values if the user actually changes them.
Was this helpful? React with 👍 or 👎 to provide feedback.
Refactor frontend to Svelte 5 + TS + Vite with macOS classic style frosted glass UI, transparency window configuration, and updated documentation.