Skip to content

feat: refactor frontend to Svelte 5 + TS + Vite & macOS glassmorphism UI#259

Open
Cmochance wants to merge 6 commits into
mainfrom
refactor-mac-glass-ui
Open

feat: refactor frontend to Svelte 5 + TS + Vite & macOS glassmorphism UI#259
Cmochance wants to merge 6 commits into
mainfrom
refactor-mac-glass-ui

Conversation

@Cmochance

@Cmochance Cmochance commented May 25, 2026

Copy link
Copy Markdown
Owner

Refactor frontend to Svelte 5 + TS + Vite with macOS classic style frosted glass UI, transparency window configuration, and updated documentation.


Open in Devin Review

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 new potential issues.

View 9 additional findings in Devin Review.

Open in Devin Review

Comment thread frontend/src/lib/i18n.ts
Comment on lines +963 to +969
"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",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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"
Suggested change
"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",
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread frontend/src/App.svelte
Comment on lines +19 to +31
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 = {};
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Suggested change
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 = {};
}
}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src-tauri/tauri.conf.json Outdated
Comment on lines +22 to +23
"fullscreen": false,
"transparent": true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +210 to +214
payload.grokWeb = {
cookies,
statsigId: grokStatsigId.trim(),
userAgent: grokUserAgent.trim(),
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Suggested change
payload.grokWeb = {
cookies,
statsigId: grokStatsigId.trim(),
userAgent: grokUserAgent.trim(),
};
payload.grokWeb = {
cookies,
...(grokStatsigId.trim() ? { statsigId: grokStatsigId.trim() } : {}),
...(grokUserAgent.trim() ? { userAgent: grokUserAgent.trim() } : {}),
};
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 4 new potential issues.

View 14 additional findings in Devin Review.

Open in Devin Review

Comment thread frontend/src/lib/api.ts
Comment on lines +40 to +47
grokWeb?: {
sso?: string;
cookieString?: string;
cfClearance?: string;
ssoRw?: string;
statsigId?: string;
userAgent?: string;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Suggested change
grokWeb?: {
sso?: string;
cookieString?: string;
cfClearance?: string;
ssoRw?: string;
statsigId?: string;
userAgent?: string;
};
grokWeb?: {
cookies?: Record<string, string>;
statsigId?: string;
userAgent?: string;
};
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread src-tauri/tauri.conf.json
"identifier": "store.alyse.codex-app-transfer",
"build": {
"frontendDist": "../frontend"
"frontendDist": "../frontend/dist"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

{$t('nav.' + $activeTab, { defaultValue: 'Codex App Transfer' })}
</div>

<!-- Actions on the right -->

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +66 to +83
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 || '',
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant