diff --git a/src/backend/controllers/homepage/HomepageController.test.ts b/src/backend/controllers/homepage/HomepageController.test.ts
index d1bd13965..d26356719 100644
--- a/src/backend/controllers/homepage/HomepageController.test.ts
+++ b/src/backend/controllers/homepage/HomepageController.test.ts
@@ -173,12 +173,14 @@ describe('HomepageController shell routes', () => {
expect(String(captured.body)).toMatch(//i);
});
- it('serves the shell on /settings, /dashboard, /action, /@:username', async () => {
+ it('serves the shell on /settings, /dashboard, /desktop, /action, /@:username', async () => {
for (const path of [
'/settings',
'/settings/*splat',
'/dashboard',
'/dashboard/',
+ '/desktop',
+ '/desktop/',
'/action/*splat',
'/@:username',
]) {
diff --git a/src/backend/controllers/homepage/HomepageController.ts b/src/backend/controllers/homepage/HomepageController.ts
index 53aca9338..c0e3970b0 100644
--- a/src/backend/controllers/homepage/HomepageController.ts
+++ b/src/backend/controllers/homepage/HomepageController.ts
@@ -82,6 +82,9 @@ export class HomepageController extends PuterController {
router.get('/dashboard', {}, (req, res) => sendShell(req, res));
router.get('/dashboard/', {}, (req, res) => sendShell(req, res));
+ router.get('/desktop', {}, (req, res) => sendShell(req, res));
+ router.get('/desktop/', {}, (req, res) => sendShell(req, res));
+
router.get('/action/*splat', {}, (req, res) => sendShell(req, res));
router.get('/@:username', {}, (req, res) => sendShell(req, res));
diff --git a/src/backend/controllers/oidc/OIDCController.ts b/src/backend/controllers/oidc/OIDCController.ts
index 794a15277..e2606aefd 100644
--- a/src/backend/controllers/oidc/OIDCController.ts
+++ b/src/backend/controllers/oidc/OIDCController.ts
@@ -139,6 +139,19 @@ export class OIDCController extends PuterController {
let appRedirectUri = flowRedirects[flow] ?? (origin || '/');
+ // Optional GUI return path so login started from /desktop or
+ // /dashboard lands back there. Strict whitelist — never a
+ // client-supplied URL (no open redirect).
+ const rawReturnTo = Array.isArray(req.query.return_to)
+ ? req.query.return_to[0]
+ : req.query.return_to;
+ if (
+ (flow === 'login' || flow === 'signup') &&
+ (rawReturnTo === '/desktop' || rawReturnTo === '/dashboard')
+ ) {
+ appRedirectUri = `${origin}${rawReturnTo}`;
+ }
+
// Popup support
const rawPopup = Array.isArray(req.query.embedded_in_popup)
? req.query.embedded_in_popup[0]
diff --git a/src/gui/dev-server.js b/src/gui/dev-server.js
index 0cbeb689b..747e69ebb 100644
--- a/src/gui/dev-server.js
+++ b/src/gui/dev-server.js
@@ -52,7 +52,7 @@ startServer(1);
// build the GUI
build();
-app.get(['/', '/app/*', '/action/*'], (req, res) => {
+app.get(['/', '/app/*', '/action/*', '/desktop', '/dashboard'], (req, res) => {
res.send(generateDevHtml({
env: env,
api_origin: 'https://api.puter.com',
diff --git a/src/gui/src/UI/Dashboard/TabHome.js b/src/gui/src/UI/Dashboard/TabHome.js
index 09eeb7623..0fd33de80 100644
--- a/src/gui/src/UI/Dashboard/TabHome.js
+++ b/src/gui/src/UI/Dashboard/TabHome.js
@@ -28,10 +28,12 @@ function buildRecentAppsHTML() {
// Show up to 6 recent apps (2 columns x 3 rows)
const recentApps = window.launch_apps.recent.slice(0, 6);
for (const app_info of recentApps) {
- // if title, name and uuid are the same and index_url is set, then show the hostname of index_url
+ // if title, name and uuid are the same and start with 'app-' and index_url
+ // is set, then show the hostname of index_url instead of the opaque app id
if (
app_info.name === app_info.title &&
app_info.name === app_info.uuid &&
+ app_info.name?.startsWith('app-') &&
app_info.index_url
) {
app_info.title = new URL(app_info.index_url).hostname;
@@ -178,14 +180,6 @@ const TabHome = {
h += '';
h += '';
- // Desktop switch card (spans full width)
- // h += '
';
- // h += '
';
- // h += 'Looking for Puter\'s desktop interface?';
- // h += '';
- // h += '
';
- // h += '
';
-
// Usage card (spans full width on second row)
h += '';
h +=
@@ -208,6 +202,23 @@ const TabHome = {
h += '
';
h += '';
+ // Open Desktop card (spans full width, links to the desktop interface)
+ h += '';
+
h += '';
return h;
},
@@ -271,11 +282,6 @@ const TabHome = {
},
);
- // Handle desktop switch button
- $el_window.on('click', '.bento-desktop-switch-btn', function () {
- window.location.href = '/';
- });
-
// Handle "Save Account" warning click
$el_window.on('click', '.bento-save-account-warning', function (e) {
e.preventDefault();
diff --git a/src/gui/src/UI/Dashboard/UIDashboard.js b/src/gui/src/UI/Dashboard/UIDashboard.js
index 74de9882a..8fdedf1b9 100644
--- a/src/gui/src/UI/Dashboard/UIDashboard.js
+++ b/src/gui/src/UI/Dashboard/UIDashboard.js
@@ -149,6 +149,11 @@ async function UIDashboard (options) {
const $el_window = $(el_window);
+ // Remove `?ref=...` from the URL, keeping the path and tab hash (mirrors UIDesktop)
+ if ( window.url_query_params?.has('ref') ) {
+ window.history.pushState(null, document.title, window.location.pathname + window.location.hash);
+ }
+
// Restore sidebar collapsed state
if ( localStorage.getItem('dashboard-sidebar-collapsed') === '1' ) {
$el_window.find('.dashboard-sidebar').addClass('collapsed');
@@ -285,8 +290,9 @@ async function UIDashboard (options) {
window.addEventListener('hashchange', handleRouteChange);
window.addEventListener('popstate', handleRouteChange);
- // Sidebar item click handler
- $el_window.on('click', '.dashboard-sidebar-item', function (e) {
+ // Sidebar item click handler — only tab items ([data-section]); plain links
+ // like the "Open Desktop" button navigate natively
+ $el_window.on('click', '.dashboard-sidebar-item[data-section]', function (e) {
e.preventDefault();
const $this = $(this);
const section = $this.attr('data-section');
diff --git a/src/gui/src/UI/UIDesktop.js b/src/gui/src/UI/UIDesktop.js
index d4631643e..cbbf61566 100644
--- a/src/gui/src/UI/UIDesktop.js
+++ b/src/gui/src/UI/UIDesktop.js
@@ -709,9 +709,9 @@ async function UIDesktop (options) {
window.sidebar_items = val;
});
- // Remove `?ref=...` from navbar URL
+ // Remove `?ref=...` from navbar URL, keeping the current path
if ( window.url_query_params.has('ref') ) {
- window.history.pushState(null, document.title, '/');
+ window.history.pushState(null, document.title, window.location.pathname);
}
//show_hidden_files
@@ -1732,7 +1732,7 @@ async function UIDesktop (options) {
try {
stat = await puter.fs.stat({ path: item_path, consistency: 'eventual' });
} catch ( e ) {
- window.history.replaceState(null, document.title, '/');
+ window.history.replaceState(null, document.title, '/desktop');
UIAlert({
message: i18n('error_user_or_path_not_found'),
type: 'error',
@@ -1816,9 +1816,11 @@ async function UIDesktop (options) {
//--------------------------------------------------------------------------------------
// Direct download link
- // i.e. https://puter.com/?download=
+ // i.e. https://puter.com/?download= or https://puter.com/desktop?download=
//--------------------------------------------------------------------------------------
- if ( window.url_paths.length === 0 && window.url_query_params.has('download') ) {
+ const is_bare_desktop_path = window.url_paths.length === 0
+ || (window.url_paths.length === 1 && window.url_paths[0]?.toLocaleLowerCase() === 'desktop');
+ if ( is_bare_desktop_path && window.url_query_params.has('download') ) {
const url = window.url_query_params.get('download');
let file_name = url.split('/').pop().split('?')[0];
@@ -2051,7 +2053,7 @@ $(document).on('contextmenu taphold', '.toolbar', function (event) {
$(document).on('click', '.qr-btn', async function (e) {
UIWindowQR({
message_i18n_key: 'scan_qr_c2a',
- text: `${window.gui_origin }?auth_token=${ window.auth_token}`,
+ text: `${window.gui_origin }/desktop?auth_token=${ window.auth_token}`,
});
});
diff --git a/src/gui/src/UI/UIWindow.js b/src/gui/src/UI/UIWindow.js
index 7a2b21862..2b12bc3c2 100644
--- a/src/gui/src/UI/UIWindow.js
+++ b/src/gui/src/UI/UIWindow.js
@@ -3561,7 +3561,7 @@ $.fn.close = async function (options) {
// only reset the URL if this window was the one that owns it (mirrors the open-side check in focusWindow)
const update_window_url = $(this).attr('data-update_window_url');
if ( ! window.is_dashboard_mode && (update_window_url === 'true' || update_window_url === null) ) {
- window.history.replaceState(null, document.title, '/');
+ window.history.replaceState(null, document.title, '/desktop');
}
// bring focus to the last window in the window-stack (only if not minimized)
let next_window_focused = false;
@@ -4015,7 +4015,7 @@ $.fn.hideWindow = async function (options) {
// update title and window URL — only if this window was the one that owns the URL
const update_window_url = $(this).attr('data-update_window_url');
if ( ! window.is_dashboard_mode && (update_window_url === 'true' || update_window_url === null) ) {
- window.history.replaceState(null, document.title, '/');
+ window.history.replaceState(null, document.title, '/desktop');
document.title = i18n('window_title_puter');
}
}
diff --git a/src/gui/src/UI/UIWindowLogin.js b/src/gui/src/UI/UIWindowLogin.js
index 19a2b444d..cfc88ab3b 100644
--- a/src/gui/src/UI/UIWindowLogin.js
+++ b/src/gui/src/UI/UIWindowLogin.js
@@ -381,7 +381,8 @@ async function UIWindowLogin (options) {
if ( logo_clickable ) {
$(el_window).find('.auth-logo').on('click', function () {
- window.location.href = '/';
+ // keep desktop users on the desktop; everyone else goes to the root dashboard
+ window.location.href = window.location.pathname === '/desktop' ? '/desktop' : '/';
});
}
@@ -407,6 +408,12 @@ async function UIWindowLogin (options) {
$(el_window).find(btnClass).on('click', function () {
let url = `${window.gui_origin}/auth/oidc/${provider}/start?flow=login`;
+ // return to the interface the login started from (backend whitelists the path)
+ const return_to = window.location.pathname;
+ if ( return_to === '/desktop' || return_to === '/dashboard' ) {
+ url += `&return_to=${encodeURIComponent(return_to)}`;
+ }
+
const referrer = options.referrer ?? window.referrerStr ?? window.openerOrigin;
if ( referrer ) {
url += `&referrer=${encodeURIComponent(referrer)}`;
diff --git a/src/gui/src/UI/UIWindowSignup.js b/src/gui/src/UI/UIWindowSignup.js
index db2707af0..e7e319d91 100644
--- a/src/gui/src/UI/UIWindowSignup.js
+++ b/src/gui/src/UI/UIWindowSignup.js
@@ -148,7 +148,8 @@ function UIWindowSignup (options) {
}
if ( logo_clickable ) {
$(el_window).find('.auth-logo').on('click', function () {
- window.location.href = '/';
+ // keep desktop users on the desktop; everyone else goes to the root dashboard
+ window.location.href = window.location.pathname === '/desktop' ? '/desktop' : '/';
});
}
@@ -191,6 +192,11 @@ function UIWindowSignup (options) {
$(el_window).find(btnClass).css('display', 'flex');
$(el_window).find(btnClass).on('click', function () {
let url = `${window.gui_origin}/auth/oidc/${provider}/start?flow=signup`;
+ // return to the interface the signup started from (backend whitelists the path)
+ const return_to = window.location.pathname;
+ if ( return_to === '/desktop' || return_to === '/dashboard' ) {
+ url += `&return_to=${encodeURIComponent(return_to)}`;
+ }
const referrer = options.referrer ?? window.referrerStr;
if ( referrer ) {
url += `&referrer=${encodeURIComponent(referrer)}`;
diff --git a/src/gui/src/css/dashboard.css b/src/gui/src/css/dashboard.css
index 4dd1f2a2a..6b92c186d 100644
--- a/src/gui/src/css/dashboard.css
+++ b/src/gui/src/css/dashboard.css
@@ -75,6 +75,11 @@
--dashboard-bento-usage-surface: #eef2ff;
--dashboard-bento-usage-header-bg: #e0e7ff;
--dashboard-bento-usage-bar-fill: #ea580c;
+ /* Emerald (matches the green icon accent used elsewhere) */
+ --dashboard-bento-desktop-icon-bg: #059669;
+ --dashboard-bento-desktop-icon-shadow: rgba(5, 150, 105, 0.22);
+ --dashboard-bento-desktop-surface: #ecfdf5;
+ --dashboard-bento-desktop-header-bg: #d1fae5;
--dashboard-shadow-subtle: rgba(0, 0, 0, 0.04);
--dashboard-shadow-light: rgba(0, 0, 0, 0.06);
@@ -166,6 +171,10 @@ body {
--dashboard-bento-usage-surface: #1c1d2e;
--dashboard-bento-usage-header-bg: #252a42;
--dashboard-bento-usage-bar-fill: #fb923c;
+ --dashboard-bento-desktop-icon-bg: #10b981;
+ --dashboard-bento-desktop-icon-shadow: rgba(16, 185, 129, 0.32);
+ --dashboard-bento-desktop-surface: #16241f;
+ --dashboard-bento-desktop-header-bg: #172e26;
--dashboard-shadow-subtle: rgba(0, 0, 0, 0.2);
--dashboard-shadow-light: rgba(0, 0, 0, 0.3);
@@ -643,7 +652,7 @@ body {
@supports (backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px)) {
.myapps-search-wrap {
- background: rgba(255, 255, 255, 0.52);
+ background: rgba(255, 255, 255, 0);
backdrop-filter: blur(20px) saturate(1.5);
-webkit-backdrop-filter: blur(20px) saturate(1.5);
}
@@ -1074,9 +1083,6 @@ input.myapps-search::placeholder {
border-radius: 20px;
overflow: hidden;
border: 1px solid var(--dashboard-shadow-light);
- box-shadow:
- 0 1px 2px var(--dashboard-shadow-subtle),
- 0 4px 16px var(--dashboard-shadow-light);
}
/* Welcome Card — uses same surface as .bento-card (no separate welcome tint) */
@@ -1246,6 +1252,11 @@ input.myapps-search::placeholder {
color: #fff;
}
+.dashboard .bento-card-fancy-icon-desktop {
+ background-color: var(--dashboard-bento-desktop-icon-bg);
+ color: #fff;
+}
+
.dashboard .bento-card-fancy-text {
display: flex;
flex-direction: column;
@@ -1386,46 +1397,39 @@ input.myapps-search::placeholder {
color: var(--dashboard-text-tertiary);
}
-/* Desktop switch bento card */
-.dashboard .bento-desktop-switch {
+/* Open Desktop bento card (full width; links to the desktop interface) */
+.dashboard .bento-desktop {
grid-column: 1 / -1;
- min-height: auto;
- background-color: var(--dashboard-bento-apps-surface);
- padding: 16px 24px;
-}
-
-.dashboard .bento-desktop-switch-inner {
display: flex;
align-items: center;
- justify-content: space-between;
- gap: 16px;
-}
-
-.dashboard .bento-desktop-switch-text {
- font-size: 14px;
- font-weight: 500;
- color: var(--dashboard-text-secondary);
-}
-
-.dashboard .bento-desktop-switch-btn {
- flex-shrink: 0;
- padding: 8px 16px;
- font-size: 13px;
- font-weight: 500;
- color: var(--dashboard-background);
- background: var(--dashboard-icon-blue-start);
- border: 1px solid var(--dashboard-primary);
- border-radius: 8px;
- cursor: pointer;
+ gap: 14px;
+ padding: 18px 24px;
+ background-color: var(--dashboard-bento-desktop-header-bg);
text-decoration: none;
- transition: background 0.15s, color 0.15s;
+ color: inherit;
+ transition: box-shadow 0.15s ease;
}
@media (hover: hover) {
- .dashboard .bento-desktop-switch-btn:hover {
- background: var(--dashboard-icon-blue-end);
- color: white;
+ .dashboard .bento-desktop:hover {
+ box-shadow:
+ 0 2px 4px var(--dashboard-shadow-subtle),
+ 0 10px 28px var(--dashboard-bento-desktop-icon-shadow);
}
+
+ .dashboard .bento-desktop:hover .bento-desktop-arrow {
+ transform: translateX(3px);
+ color: var(--dashboard-text-primary);
+ }
+}
+
+.dashboard .bento-desktop-arrow {
+ margin-left: auto;
+ font-size: 22px;
+ font-weight: 300;
+ line-height: 1;
+ color: var(--dashboard-text-hint);
+ transition: transform 0.15s ease, color 0.15s ease;
}
/* Usage bento card (cool indigo surfaces; orange bars stay as progress accent) */
@@ -1701,6 +1705,11 @@ input.myapps-search::placeholder {
gap: 12px;
}
+ .dashboard .bento-desktop {
+ padding: 16px 20px;
+ gap: 12px;
+ }
+
.dashboard .bento-card-fancy-icon {
width: 40px;
height: 40px;
diff --git a/src/gui/src/i18n/translations/en.js b/src/gui/src/i18n/translations/en.js
index b1159eb19..55155441f 100644
--- a/src/gui/src/i18n/translations/en.js
+++ b/src/gui/src/i18n/translations/en.js
@@ -206,6 +206,7 @@ const en = {
or: 'or',
open: 'Open',
new_window: 'New Window',
+ open_desktop: 'Open Desktop',
open_in_ai: 'Open in AI',
open_in_new_tab: 'Open in New Tab',
open_in_new_window: 'Open in New Window',
diff --git a/src/gui/src/initgui.js b/src/gui/src/initgui.js
index 3614f057f..0e7ca5605 100644
--- a/src/gui/src/initgui.js
+++ b/src/gui/src/initgui.js
@@ -157,9 +157,29 @@ if ( jQuery ) {
}
// are we in dashboard mode?
-if ( window.location.pathname === '/dashboard' || window.location.pathname === '/dashboard/' ) {
- window.is_dashboard_mode = true;
- window.dashboard_initial_route = parseDashboardRoute();
+// The dashboard is the default interface at the root path; `/dashboard` is kept as an
+// alias, and `/desktop` loads the desktop instead. Root URLs that carry a desktop-only
+// flow keep booting the desktop: auth popups (`?embedded_in_popup=`), app deep links
+// (`?app=`), direct downloads (`?download=`), fullpage mode (`?puter.fullpage=`), and
+// iframe embeds.
+{
+ const pathname = window.location.pathname;
+ const search_params = new URLSearchParams(window.location.search);
+ if ( ['true', '1'].includes(search_params.get('embedded_in_popup')) ) {
+ window.embedded_in_popup = true;
+ }
+ // note: iframe detection is inlined because globals.js (window.is_embedded) loads after this module
+ const in_iframe = window.location !== window.parent.location;
+ const needs_desktop_at_root = window.embedded_in_popup
+ || in_iframe
+ || search_params.has('puter.fullpage')
+ || search_params.has('app')
+ || search_params.has('download');
+ const is_dashboard_alias = pathname === '/dashboard' || pathname === '/dashboard/';
+ if ( is_dashboard_alias || (pathname === '/' && !needs_desktop_at_root) ) {
+ window.is_dashboard_mode = true;
+ window.dashboard_initial_route = parseDashboardRoute();
+ }
}
/**
@@ -431,9 +451,9 @@ window.initgui = async function (options) {
//--------------------------------------------------------------------------------------
// Is GUI embedded in a popup?
// i.e. https://puter.com/?embedded_in_popup=true
+ // (the flag itself is parsed once at module level, alongside the dashboard-mode check)
//--------------------------------------------------------------------------------------
- if ( window.url_query_params.has('embedded_in_popup') && (window.url_query_params.get('embedded_in_popup') === 'true' || window.url_query_params.get('embedded_in_popup') === '1') ) {
- window.embedded_in_popup = true;
+ if ( window.embedded_in_popup ) {
$('body').addClass('embedded-in-popup');
// determine the origin of the opener (preserved across OIDC redirect via URL param, else referrer or messaging)
@@ -720,8 +740,9 @@ window.initgui = async function (options) {
// update auth data
await window.update_auth_data(query_param_auth_token, whoami, api_origin);
}
- // remove auth_token from URL
- window.history.pushState(null, document.title, '/');
+ // remove auth_token from URL, keeping the current path (e.g. `/` or `/desktop`)
+ // and hash (dashboard tab links like /#usage)
+ window.history.pushState(null, document.title, window.location.pathname + window.location.hash);
}
/**
@@ -845,8 +866,9 @@ window.initgui = async function (options) {
$('.taskbar').remove();
// disable native browser exit confirmation
window.onbeforeunload = null;
- // go to home page
- window.location.replace('/');
+ // go back to the interface the user was in: dashboard users to the root
+ // dashboard, desktop users to /desktop
+ window.location.replace(window.is_dashboard_mode ? '/' : '/desktop');
});
// -------------------------------------------------------------------------------------
@@ -1251,6 +1273,11 @@ window.initgui = async function (options) {
const needs_action = action === 'authme' || action === 'copyauth';
const reload_on_success = needs_action;
if ( window.logged_in_users.length > 0 ) {
+ // dashboard mode skips the wallpaper (fullpage), but the session list
+ // has no cover page — restore the wallpaper so it isn't on a blank page
+ if ( window.is_dashboard_mode ) {
+ window.refresh_desktop_background();
+ }
await UIWindowSessionList({
redirect_url: needs_action ? window.location.href : undefined,
});