fix(desktop): triage batch of GUI quality-of-life fixes - #37536
Conversation
…poser, tool cards, messaging, and platform plumbing A grab-bag of high-leverage UX fixes plus a few backend touches that the GUI needs to behave correctly on Windows. Sidebar / sessions - Decrement $sessionsTotal on delete + archive so "Load N more" stops claiming removed rows are still on the server. - Hide the "Group by workspace" toggle when no unpinned sessions exist. - Accept Cmd/Ctrl+N as a "new session" accelerator (in addition to bare Shift+N), and render the kbd hint per-platform. - Switch the statusbar to overflow-x-clip so untitled sessions don't paint a horizontal scrollbar at the bottom of the window. Messaging + Cron - Add [-webkit-app-region: no-drag] to the page-search input so clicks reach the field instead of routing to the OS window-drag handler. - Replace single-letter PlatformAvatar with brand glyphs from @icons-pack/react-simple-icons (telegram, discord, matrix, signal, whatsapp, mattermost, wechat, qq, ...). Letter monogram fallback for Slack / Dingtalk / Feishu / WeCom (removed from Simple Icons at brand owner request). - Drop the duplicate "Create first cron" button in the empty state. Composer - Dedupe pasted images by (name, size, lastModified, type) instead of Blob identity; Chromium hands us the same screenshot via both clipboard.items and clipboard.files with fresh File instances. - Enable spellcheck on the contentEditable, configure Chromium's spellchecker with the system locale on whenReady, and add replaceMisspelling + "Add to dictionary" entries to the context menu. - Render user messages through a minimal markdown pipeline (inline backtick code + fenced ``` blocks) while keeping @file:/@image: directive chips intact. - max-h-[60vh] overflow-y-auto + collisionPadding on the prompt-snippet submenu. - Bake cursor-pointer into the <Button> primitive (with disabled:cursor-default) and into titlebarButtonClass. Dialogs + tabs + version - Default DialogContent now has max-h-[85vh] overflow-y-auto so long bodies scroll instead of falling off-screen. - Right-rail preview tabs close on middle-click (button === 1), with an onMouseDown swallow to suppress Chromium autoscroll. - New refreshDesktopVersion() helper called from About mount, after every update check, and on throttled window focus so About reflects the just-installed binary. Keys + Artifacts + Terminal - Drop the global "Show advanced" toggle in KeysSettings. Provider groups now default-expand when they have any key set. - Extend openExternalUrl to handle file:// via shell.openPath, with showItemInFolder fallback when the OS can't open the file. - New lib/ansi.ts SGR parser + <AnsiText> component, applied to terminal/execute_code tool output. - ToolView gained stdout / stderr / rendersAnsi; tool-fallback renders the two streams as separate labeled blocks with stderr in a neutral tone (not destructive — many CLIs log info on stderr). - Drop 'stderr' from ERROR_MSG_KEYS in tool-result-summary. Paths + platform - resolveHermesCwd skips process.cwd() when packaged and prefers a user-configurable default project directory. - New hermes:setting:defaultProjectDir:{get,set,pick} IPC handlers + preload bridge + global.d.ts typing + a "Default project directory" row in Sessions settings. - FileOperations.delete_path(path, recursive=True) on the abstract base; ShellFileOperations.delete_file rewritten to run a cross- platform python3 -c snippet so deletes work on Windows shells (which have no rm/rm -rf). Fallback to `python` when `python3` isn't on PATH. - README troubleshooting block split into macOS/Linux + Windows PowerShell recipes. - Tightened renderer favicon links in index.html + added color-scheme and theme-color meta. Backend lifecycle (renderer-side mitigation) - New noteSessionActivity() heartbeat + session.ts watchdog: an 8-minute silence on the stream auto-clears stuck $workingSessionIds entries so "Session Busy" never gets permanently wedged. Wired into useSessionStateCache so every state update refreshes the timer. i18n spike - docs/desktop-i18n-rfc.md scoping a future language-switcher PR (recommends react-intl, audits IME/RTL/CJK in the composer + chat bubbles, 4-PR rollout plan, ~3-4 eng-weeks for the first non-English locale). Co-authored-by: Cursor <cursoragent@cursor.com>
🔎 Lint report:
|
Radix's DropdownMenuPrimitive.Portal renders content under document.body, outside the `.scrollbar-dt` scope on #root. Whenever a menu's max-height clipped its content (even by a pixel — common for the composer "+" menu that opens upward near the bottom of the window), the user saw the OS's chunky native scrollbar painted across the whole menu. Bake a thin, slot-styled scrollbar onto DropdownMenuContent and DropdownMenuSubContent via [scrollbar-width:thin] + WebKit pseudo-element arbitrary variants. The submenu also gets a max-h tied to --radix-dropdown-menu-content-available-height so long snippet lists scroll cleanly instead of running off the bottom of the viewport. Drop the now- redundant max-h-[60vh] override on the prompt-snippet submenu. Co-authored-by: Cursor <cursoragent@cursor.com>
…rcle Two regressions from the previous dropdown-scrollbar fix: - The parent menu rendered as a rounded oval. Long Tailwind v4 arbitrary- variant strings like [&::-webkit-scrollbar-thumb]:rounded-full inside a cn() call were being mis-resolved so the `rounded-full` leaked onto the menu container itself. Replaced the whole tower of arbitrary variants with a real `.dt-portal-scrollbar` class in styles.css that mirrors what `.scrollbar-dt` already does for #root descendants. Plain CSS, no Tailwind parser ambiguity. - The Prompt snippets submenu didn't open. Radix publishes --radix-dropdown-menu-content-available-height on Content but NOT on SubContent, so the `max-h` bound to that variable computed to 0 and the submenu collapsed to zero height. Switched SubContent to a fixed max-h-80 (≈20rem) which is plenty for a snippet list and never collapses. Co-authored-by: Cursor <cursoragent@cursor.com>
…alog The submenu refused to open when the parent dropdown was anchored at the bottom of the window (composer "+" button) — Radix's collision detection + SubContent positioning was fighting us. Rather than keep tuning side / sideOffset / collisionPadding / max-h until something stuck, replace the DropdownMenuSub with a clicked DropdownMenuItem that opens a proper Dialog. Side benefits over the submenu: - Each snippet gets a description line, so a glance is enough to pick one. - Focus management is handled by Dialog automatically. - Easy to grow (search, custom user snippets, categories) without another round of Radix positioning bugs. Also extract types/interfaces to the bottom of the file per workspace convention. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
This PR delivers a broad batch of Hermes Desktop (Electron) GUI quality-of-life improvements, plus a few cross-platform backend/main-process fixes needed for correct behavior on Windows, and an i18n scoping RFC for future work.
Changes:
- Improves Desktop UX in multiple surfaces (sidebar/session actions, dialogs/menus scrollability, keyboard accelerators, messaging icons, composer behaviors).
- Enhances chat/tool rendering (minimal markdown for user messages, ANSI-colored terminal output, stdout/stderr split with neutral stderr styling).
- Adds/adjusts Desktop platform plumbing (default project directory setting + persistence, file:// open handling, spellchecker configuration, cross-platform delete helper).
Reviewed changes
Copilot reviewed 37 out of 39 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/file_operations.py | Adds delete_path API and makes shell delete cross-platform via a Python snippet. |
| docs/desktop-i18n-rfc.md | Adds i18n scoping RFC (no functional change). |
| apps/desktop/src/styles.css | Adds themed scrollbar styling for portaled overlays via dt-portal-scrollbar. |
| apps/desktop/src/store/updates.ts | Adds refreshDesktopVersion() and uses it in poller/focus/update flow. |
| apps/desktop/src/store/session.ts | Adds session “working” watchdog + noteSessionActivity() heartbeat. |
| apps/desktop/src/lib/tool-result-summary.ts | Stops treating stderr as an error-message signal. |
| apps/desktop/src/lib/ansi.ts | Introduces minimal ANSI SGR parsing + Tailwind class mapping. |
| apps/desktop/src/global.d.ts | Types the new window.hermesDesktop.settings.* bridge API. |
| apps/desktop/src/components/ui/dropdown-menu.tsx | Applies dt-portal-scrollbar + fixes submenu max-height/overflow behavior. |
| apps/desktop/src/components/ui/dialog.tsx | Caps dialog content height and makes overflow scroll inside the dialog. |
| apps/desktop/src/components/ui/button.tsx | Makes Button cursor pointer by default, with disabled cursor override. |
| apps/desktop/src/components/assistant-ui/user-message-text.tsx | Adds minimal markdown rendering for user message text (inline/fenced code). |
| apps/desktop/src/components/assistant-ui/tool-fallback.tsx | Renders ANSI output and splits stdout/stderr into labeled blocks. |
| apps/desktop/src/components/assistant-ui/tool-fallback-model.ts | Extends ToolView with stdout/stderr/rendersAnsi; populates them for shell tools. |
| apps/desktop/src/components/assistant-ui/thread.tsx | Switches user message rendering to UserMessageText while preserving directive chips. |
| apps/desktop/src/components/assistant-ui/ansi-text.tsx | Adds React renderer for parsed ANSI segments. |
| apps/desktop/src/app/shell/titlebar.ts | Adds cursor-pointer to titlebar button styling. |
| apps/desktop/src/app/shell/statusbar-controls.tsx | Clips horizontal overflow to prevent statusbar scrollbar. |
| apps/desktop/src/app/settings/sessions-settings.tsx | Adds “Default project directory” setting UI. |
| apps/desktop/src/app/settings/keys-settings.tsx | Removes global “Show advanced”; auto-expands providers that have keys set. |
| apps/desktop/src/app/settings/about-settings.tsx | Refreshes desktop version on mount so About reflects updated binaries. |
| apps/desktop/src/app/session/hooks/use-session-state-cache.ts | Wires stream activity into the session watchdog heartbeat. |
| apps/desktop/src/app/session/hooks/use-session-actions.ts | Keeps $sessionsTotal in sync on delete/archive with rollback. |
| apps/desktop/src/app/page-search-shell.tsx | Marks header row as no-drag so the search input can be focused by click. |
| apps/desktop/src/app/messaging/platform-icon.tsx | Adds brand glyph platform avatars using @icons-pack/react-simple-icons. |
| apps/desktop/src/app/messaging/index.tsx | Switches messaging platform avatar rendering to the new glyph-based avatar. |
| apps/desktop/src/app/desktop-controller.tsx | Adds Cmd/Ctrl+N accelerator for new sessions (and keeps Shift+N behavior). |
| apps/desktop/src/app/cron/index.tsx | Removes duplicated empty-state “Create first cron” button. |
| apps/desktop/src/app/chat/sidebar/index.tsx | Hides grouping toggle when inapplicable; updates new-session shortcut hint; tweaks empty hint text. |
| apps/desktop/src/app/chat/right-rail/preview.tsx | Adds middle-click-to-close for preview tabs with autoscroll suppression. |
| apps/desktop/src/app/chat/composer/text-utils.ts | Dedupes pasted images by metadata fingerprint instead of object identity. |
| apps/desktop/src/app/chat/composer/index.tsx | Enables spellcheck and disables auto-capitalization/correction on the editor. |
| apps/desktop/src/app/chat/composer/context-menu.tsx | Adds collision padding/offset to prompt snippet submenu. |
| apps/desktop/README.md | Splits troubleshooting instructions into macOS/Linux vs Windows PowerShell. |
| apps/desktop/package.json | Adds @icons-pack/react-simple-icons dependency. |
| apps/desktop/index.html | Tightens favicon links and adds theme/color-scheme metadata. |
| apps/desktop/electron/preload.cjs | Exposes hermesDesktop.settings.* IPC bridge to the renderer. |
| apps/desktop/electron/main.cjs | Adds file:// open support, default project dir persistence/IPC, packaged cwd fix, context menu spellcheck items, and spellchecker init. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Reverses the previous direction on cron empty-state dedup. The body button is more discoverable for first-time users (it's anchored next to the "No scheduled jobs yet" copy that explains the feature) and frees the top bar from a global CTA that wasn't pulling its weight. - Empty (zero jobs): EmptyState renders the "Create first cron" button again, like the original design. - Empty (search filtered out all jobs): no button, just "Try a broader search query" copy. - Has jobs: small inline header above the list shows `N/M active` plus a single "New cron" button (right-aligned). The rows themselves already cover edit/pause/trigger/delete, so this is the only "create" affordance. Also drop the dead `<div className="hidden">…</div>` enabledCount line the previous patch left behind; the count is now visible in the new header instead of hidden. Co-authored-by: Cursor <cursoragent@cursor.com>
- sessions-settings: guard the WHOLE bridge call rather than chaining `?.settings.foo().then(...)` — the latter throws when `window.hermesDesktop` is undefined (non-Electron / Vitest contexts) because the chain short-circuits to `undefined.then(...)`. - file_operations: drop `Path.unlink(missing_ok=True)` (Py>=3.8) so the generated delete snippet still works on remote backends running Python 3.7. The existing FileNotFoundError handler covers the same case and works back to 3.4. - ansi.test.ts: add focused Vitest coverage for the SGR parser (basic/bright colors, bold toggles, default-fg reset, coalescing, 256-color / truecolor arg consumption, non-SGR CSI drop, empty SGR full-reset) so future refactors can't silently regress terminal rendering. Co-authored-by: Cursor <cursoragent@cursor.com>
Re-review complete. All three issues from the initial pass are cleanly resolved in 996d2bf:
No new issues found — this looks good to merge. |
`refreshDesktopVersion()` is called best-effort with `void` from `checkUpdates()`, `startUpdatePoller()`, and the window focus handler. If the IPC bridge rejects (main process shutting down during reload, bridge not yet ready on first paint), the rejection surfaces as an unhandled promise rejection in the renderer. Wrap the call in try/catch and return null on failure so callers can keep the existing fire-and-forget pattern safely. Co-authored-by: Cursor <cursoragent@cursor.com>
Re-review complete. The |
* fix(desktop): honor default project directory for new sessions The Settings picker persisted project-dir.json but the renderer kept seeding new chats from sticky localStorage home. Prefer the configured default on boot and session.create, pin TERMINAL_CWD at backend spawn, and reject packaged install-dir paths that regressed after #37536. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address review on default project dir PR Add workspace cwd precedence tests, extract isPackagedInstallPath for platform test coverage, and stop rewriting live $currentCwd when a session is already active (cache-only until the next new chat). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…#37536) * fix(desktop): triage 24 GUI quality-of-life fixes across sidebar, composer, tool cards, messaging, and platform plumbing A grab-bag of high-leverage UX fixes plus a few backend touches that the GUI needs to behave correctly on Windows. Sidebar / sessions - Decrement $sessionsTotal on delete + archive so "Load N more" stops claiming removed rows are still on the server. - Hide the "Group by workspace" toggle when no unpinned sessions exist. - Accept Cmd/Ctrl+N as a "new session" accelerator (in addition to bare Shift+N), and render the kbd hint per-platform. - Switch the statusbar to overflow-x-clip so untitled sessions don't paint a horizontal scrollbar at the bottom of the window. Messaging + Cron - Add [-webkit-app-region: no-drag] to the page-search input so clicks reach the field instead of routing to the OS window-drag handler. - Replace single-letter PlatformAvatar with brand glyphs from @icons-pack/react-simple-icons (telegram, discord, matrix, signal, whatsapp, mattermost, wechat, qq, ...). Letter monogram fallback for Slack / Dingtalk / Feishu / WeCom (removed from Simple Icons at brand owner request). - Drop the duplicate "Create first cron" button in the empty state. Composer - Dedupe pasted images by (name, size, lastModified, type) instead of Blob identity; Chromium hands us the same screenshot via both clipboard.items and clipboard.files with fresh File instances. - Enable spellcheck on the contentEditable, configure Chromium's spellchecker with the system locale on whenReady, and add replaceMisspelling + "Add to dictionary" entries to the context menu. - Render user messages through a minimal markdown pipeline (inline backtick code + fenced ``` blocks) while keeping @file:/@image: directive chips intact. - max-h-[60vh] overflow-y-auto + collisionPadding on the prompt-snippet submenu. - Bake cursor-pointer into the <Button> primitive (with disabled:cursor-default) and into titlebarButtonClass. Dialogs + tabs + version - Default DialogContent now has max-h-[85vh] overflow-y-auto so long bodies scroll instead of falling off-screen. - Right-rail preview tabs close on middle-click (button === 1), with an onMouseDown swallow to suppress Chromium autoscroll. - New refreshDesktopVersion() helper called from About mount, after every update check, and on throttled window focus so About reflects the just-installed binary. Keys + Artifacts + Terminal - Drop the global "Show advanced" toggle in KeysSettings. Provider groups now default-expand when they have any key set. - Extend openExternalUrl to handle file:// via shell.openPath, with showItemInFolder fallback when the OS can't open the file. - New lib/ansi.ts SGR parser + <AnsiText> component, applied to terminal/execute_code tool output. - ToolView gained stdout / stderr / rendersAnsi; tool-fallback renders the two streams as separate labeled blocks with stderr in a neutral tone (not destructive — many CLIs log info on stderr). - Drop 'stderr' from ERROR_MSG_KEYS in tool-result-summary. Paths + platform - resolveHermesCwd skips process.cwd() when packaged and prefers a user-configurable default project directory. - New hermes:setting:defaultProjectDir:{get,set,pick} IPC handlers + preload bridge + global.d.ts typing + a "Default project directory" row in Sessions settings. - FileOperations.delete_path(path, recursive=True) on the abstract base; ShellFileOperations.delete_file rewritten to run a cross- platform python3 -c snippet so deletes work on Windows shells (which have no rm/rm -rf). Fallback to `python` when `python3` isn't on PATH. - README troubleshooting block split into macOS/Linux + Windows PowerShell recipes. - Tightened renderer favicon links in index.html + added color-scheme and theme-color meta. Backend lifecycle (renderer-side mitigation) - New noteSessionActivity() heartbeat + session.ts watchdog: an 8-minute silence on the stream auto-clears stuck $workingSessionIds entries so "Session Busy" never gets permanently wedged. Wired into useSessionStateCache so every state update refreshes the timer. i18n spike - docs/desktop-i18n-rfc.md scoping a future language-switcher PR (recommends react-intl, audits IME/RTL/CJK in the composer + chat bubbles, 4-PR rollout plan, ~3-4 eng-weeks for the first non-English locale). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): replace native OS scrollbar in portaled dropdown menus Radix's DropdownMenuPrimitive.Portal renders content under document.body, outside the `.scrollbar-dt` scope on #root. Whenever a menu's max-height clipped its content (even by a pixel — common for the composer "+" menu that opens upward near the bottom of the window), the user saw the OS's chunky native scrollbar painted across the whole menu. Bake a thin, slot-styled scrollbar onto DropdownMenuContent and DropdownMenuSubContent via [scrollbar-width:thin] + WebKit pseudo-element arbitrary variants. The submenu also gets a max-h tied to --radix-dropdown-menu-content-available-height so long snippet lists scroll cleanly instead of running off the bottom of the viewport. Drop the now- redundant max-h-[60vh] override on the prompt-snippet submenu. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): unbork dropdown menu — submenu opens, parent isn't a circle Two regressions from the previous dropdown-scrollbar fix: - The parent menu rendered as a rounded oval. Long Tailwind v4 arbitrary- variant strings like [&::-webkit-scrollbar-thumb]:rounded-full inside a cn() call were being mis-resolved so the `rounded-full` leaked onto the menu container itself. Replaced the whole tower of arbitrary variants with a real `.dt-portal-scrollbar` class in styles.css that mirrors what `.scrollbar-dt` already does for #root descendants. Plain CSS, no Tailwind parser ambiguity. - The Prompt snippets submenu didn't open. Radix publishes --radix-dropdown-menu-content-available-height on Content but NOT on SubContent, so the `max-h` bound to that variable computed to 0 and the submenu collapsed to zero height. Switched SubContent to a fixed max-h-80 (≈20rem) which is plenty for a snippet list and never collapses. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): promote prompt snippets from Radix submenu to a real Dialog The submenu refused to open when the parent dropdown was anchored at the bottom of the window (composer "+" button) — Radix's collision detection + SubContent positioning was fighting us. Rather than keep tuning side / sideOffset / collisionPadding / max-h until something stuck, replace the DropdownMenuSub with a clicked DropdownMenuItem that opens a proper Dialog. Side benefits over the submenu: - Each snippet gets a description line, so a glance is enough to pick one. - Focus management is handled by Dialog automatically. - Easy to grow (search, custom user snippets, categories) without another round of Radix positioning bugs. Also extract types/interfaces to the bottom of the file per workspace convention. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): move cron 'New cron' button off the top bar into the body Reverses the previous direction on cron empty-state dedup. The body button is more discoverable for first-time users (it's anchored next to the "No scheduled jobs yet" copy that explains the feature) and frees the top bar from a global CTA that wasn't pulling its weight. - Empty (zero jobs): EmptyState renders the "Create first cron" button again, like the original design. - Empty (search filtered out all jobs): no button, just "Try a broader search query" copy. - Has jobs: small inline header above the list shows `N/M active` plus a single "New cron" button (right-aligned). The rows themselves already cover edit/pause/trigger/delete, so this is the only "create" affordance. Also drop the dead `<div className="hidden">…</div>` enabledCount line the previous patch left behind; the count is now visible in the new header instead of hidden. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address Copilot review on PR 37536 - sessions-settings: guard the WHOLE bridge call rather than chaining `?.settings.foo().then(...)` — the latter throws when `window.hermesDesktop` is undefined (non-Electron / Vitest contexts) because the chain short-circuits to `undefined.then(...)`. - file_operations: drop `Path.unlink(missing_ok=True)` (Py>=3.8) so the generated delete snippet still works on remote backends running Python 3.7. The existing FileNotFoundError handler covers the same case and works back to 3.4. - ansi.test.ts: add focused Vitest coverage for the SGR parser (basic/bright colors, bold toggles, default-fg reset, coalescing, 256-color / truecolor arg consumption, non-SGR CSI drop, empty SGR full-reset) so future refactors can't silently regress terminal rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop/updates): swallow refreshDesktopVersion bridge errors `refreshDesktopVersion()` is called best-effort with `void` from `checkUpdates()`, `startUpdatePoller()`, and the window focus handler. If the IPC bridge rejects (main process shutting down during reload, bridge not yet ready on first paint), the rejection surfaces as an unhandled promise rejection in the renderer. Wrap the call in try/catch and return null on failure so callers can keep the existing fire-and-forget pattern safely. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(desktop): drop work duplicated by other in-flight PRs - composer/text-utils.ts: revert paste-image dedupe — PR NousResearch#37596 ships the same fix with a cleaner content-key approach and a Vitest file (text-utils.test.ts). Letting that PR own the change. - docs/desktop-i18n-rfc.md: delete the i18n scoping RFC — PR NousResearch#37568 has already shipped a working i18n surface (homegrown nanostores `t()` helper over en/zh dictionaries), so the RFC's framework recommendation (`react-intl`) is now obsolete and would just contradict the implementation that's actually landing. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…esearch#43234) * fix(desktop): honor default project directory for new sessions The Settings picker persisted project-dir.json but the renderer kept seeding new chats from sticky localStorage home. Prefer the configured default on boot and session.create, pin TERMINAL_CWD at backend spawn, and reject packaged install-dir paths that regressed after NousResearch#37536. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address review on default project dir PR Add workspace cwd precedence tests, extract isPackagedInstallPath for platform test coverage, and stop rewriting live $currentCwd when a session is already active (cache-only until the next new chat). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(desktop): triage 24 GUI quality-of-life fixes across sidebar, composer, tool cards, messaging, and platform plumbing A grab-bag of high-leverage UX fixes plus a few backend touches that the GUI needs to behave correctly on Windows. Sidebar / sessions - Decrement $sessionsTotal on delete + archive so "Load N more" stops claiming removed rows are still on the server. - Hide the "Group by workspace" toggle when no unpinned sessions exist. - Accept Cmd/Ctrl+N as a "new session" accelerator (in addition to bare Shift+N), and render the kbd hint per-platform. - Switch the statusbar to overflow-x-clip so untitled sessions don't paint a horizontal scrollbar at the bottom of the window. Messaging + Cron - Add [-webkit-app-region: no-drag] to the page-search input so clicks reach the field instead of routing to the OS window-drag handler. - Replace single-letter PlatformAvatar with brand glyphs from @icons-pack/react-simple-icons (telegram, discord, matrix, signal, whatsapp, mattermost, wechat, qq, ...). Letter monogram fallback for Slack / Dingtalk / Feishu / WeCom (removed from Simple Icons at brand owner request). - Drop the duplicate "Create first cron" button in the empty state. Composer - Dedupe pasted images by (name, size, lastModified, type) instead of Blob identity; Chromium hands us the same screenshot via both clipboard.items and clipboard.files with fresh File instances. - Enable spellcheck on the contentEditable, configure Chromium's spellchecker with the system locale on whenReady, and add replaceMisspelling + "Add to dictionary" entries to the context menu. - Render user messages through a minimal markdown pipeline (inline backtick code + fenced ``` blocks) while keeping @file:/@image: directive chips intact. - max-h-[60vh] overflow-y-auto + collisionPadding on the prompt-snippet submenu. - Bake cursor-pointer into the <Button> primitive (with disabled:cursor-default) and into titlebarButtonClass. Dialogs + tabs + version - Default DialogContent now has max-h-[85vh] overflow-y-auto so long bodies scroll instead of falling off-screen. - Right-rail preview tabs close on middle-click (button === 1), with an onMouseDown swallow to suppress Chromium autoscroll. - New refreshDesktopVersion() helper called from About mount, after every update check, and on throttled window focus so About reflects the just-installed binary. Keys + Artifacts + Terminal - Drop the global "Show advanced" toggle in KeysSettings. Provider groups now default-expand when they have any key set. - Extend openExternalUrl to handle file:// via shell.openPath, with showItemInFolder fallback when the OS can't open the file. - New lib/ansi.ts SGR parser + <AnsiText> component, applied to terminal/execute_code tool output. - ToolView gained stdout / stderr / rendersAnsi; tool-fallback renders the two streams as separate labeled blocks with stderr in a neutral tone (not destructive — many CLIs log info on stderr). - Drop 'stderr' from ERROR_MSG_KEYS in tool-result-summary. Paths + platform - resolveHermesCwd skips process.cwd() when packaged and prefers a user-configurable default project directory. - New hermes:setting:defaultProjectDir:{get,set,pick} IPC handlers + preload bridge + global.d.ts typing + a "Default project directory" row in Sessions settings. - FileOperations.delete_path(path, recursive=True) on the abstract base; ShellFileOperations.delete_file rewritten to run a cross- platform python3 -c snippet so deletes work on Windows shells (which have no rm/rm -rf). Fallback to `python` when `python3` isn't on PATH. - README troubleshooting block split into macOS/Linux + Windows PowerShell recipes. - Tightened renderer favicon links in index.html + added color-scheme and theme-color meta. Backend lifecycle (renderer-side mitigation) - New noteSessionActivity() heartbeat + session.ts watchdog: an 8-minute silence on the stream auto-clears stuck $workingSessionIds entries so "Session Busy" never gets permanently wedged. Wired into useSessionStateCache so every state update refreshes the timer. i18n spike - docs/desktop-i18n-rfc.md scoping a future language-switcher PR (recommends react-intl, audits IME/RTL/CJK in the composer + chat bubbles, 4-PR rollout plan, ~3-4 eng-weeks for the first non-English locale). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): replace native OS scrollbar in portaled dropdown menus Radix's DropdownMenuPrimitive.Portal renders content under document.body, outside the `.scrollbar-dt` scope on #root. Whenever a menu's max-height clipped its content (even by a pixel — common for the composer "+" menu that opens upward near the bottom of the window), the user saw the OS's chunky native scrollbar painted across the whole menu. Bake a thin, slot-styled scrollbar onto DropdownMenuContent and DropdownMenuSubContent via [scrollbar-width:thin] + WebKit pseudo-element arbitrary variants. The submenu also gets a max-h tied to --radix-dropdown-menu-content-available-height so long snippet lists scroll cleanly instead of running off the bottom of the viewport. Drop the now- redundant max-h-[60vh] override on the prompt-snippet submenu. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): unbork dropdown menu — submenu opens, parent isn't a circle Two regressions from the previous dropdown-scrollbar fix: - The parent menu rendered as a rounded oval. Long Tailwind v4 arbitrary- variant strings like [&::-webkit-scrollbar-thumb]:rounded-full inside a cn() call were being mis-resolved so the `rounded-full` leaked onto the menu container itself. Replaced the whole tower of arbitrary variants with a real `.dt-portal-scrollbar` class in styles.css that mirrors what `.scrollbar-dt` already does for #root descendants. Plain CSS, no Tailwind parser ambiguity. - The Prompt snippets submenu didn't open. Radix publishes --radix-dropdown-menu-content-available-height on Content but NOT on SubContent, so the `max-h` bound to that variable computed to 0 and the submenu collapsed to zero height. Switched SubContent to a fixed max-h-80 (≈20rem) which is plenty for a snippet list and never collapses. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): promote prompt snippets from Radix submenu to a real Dialog The submenu refused to open when the parent dropdown was anchored at the bottom of the window (composer "+" button) — Radix's collision detection + SubContent positioning was fighting us. Rather than keep tuning side / sideOffset / collisionPadding / max-h until something stuck, replace the DropdownMenuSub with a clicked DropdownMenuItem that opens a proper Dialog. Side benefits over the submenu: - Each snippet gets a description line, so a glance is enough to pick one. - Focus management is handled by Dialog automatically. - Easy to grow (search, custom user snippets, categories) without another round of Radix positioning bugs. Also extract types/interfaces to the bottom of the file per workspace convention. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): move cron 'New cron' button off the top bar into the body Reverses the previous direction on cron empty-state dedup. The body button is more discoverable for first-time users (it's anchored next to the "No scheduled jobs yet" copy that explains the feature) and frees the top bar from a global CTA that wasn't pulling its weight. - Empty (zero jobs): EmptyState renders the "Create first cron" button again, like the original design. - Empty (search filtered out all jobs): no button, just "Try a broader search query" copy. - Has jobs: small inline header above the list shows `N/M active` plus a single "New cron" button (right-aligned). The rows themselves already cover edit/pause/trigger/delete, so this is the only "create" affordance. Also drop the dead `<div className="hidden">…</div>` enabledCount line the previous patch left behind; the count is now visible in the new header instead of hidden. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address Copilot review on PR 37536 - sessions-settings: guard the WHOLE bridge call rather than chaining `?.settings.foo().then(...)` — the latter throws when `window.hermesDesktop` is undefined (non-Electron / Vitest contexts) because the chain short-circuits to `undefined.then(...)`. - file_operations: drop `Path.unlink(missing_ok=True)` (Py>=3.8) so the generated delete snippet still works on remote backends running Python 3.7. The existing FileNotFoundError handler covers the same case and works back to 3.4. - ansi.test.ts: add focused Vitest coverage for the SGR parser (basic/bright colors, bold toggles, default-fg reset, coalescing, 256-color / truecolor arg consumption, non-SGR CSI drop, empty SGR full-reset) so future refactors can't silently regress terminal rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop/updates): swallow refreshDesktopVersion bridge errors `refreshDesktopVersion()` is called best-effort with `void` from `checkUpdates()`, `startUpdatePoller()`, and the window focus handler. If the IPC bridge rejects (main process shutting down during reload, bridge not yet ready on first paint), the rejection surfaces as an unhandled promise rejection in the renderer. Wrap the call in try/catch and return null on failure so callers can keep the existing fire-and-forget pattern safely. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(desktop): drop work duplicated by other in-flight PRs - composer/text-utils.ts: revert paste-image dedupe — PR #37596 ships the same fix with a cleaner content-key approach and a Vitest file (text-utils.test.ts). Letting that PR own the change. - docs/desktop-i18n-rfc.md: delete the i18n scoping RFC — PR #37568 has already shipped a working i18n surface (homegrown nanostores `t()` helper over en/zh dictionaries), so the RFC's framework recommendation (`react-intl`) is now obsolete and would just contradict the implementation that's actually landing. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(desktop): honor default project directory for new sessions The Settings picker persisted project-dir.json but the renderer kept seeding new chats from sticky localStorage home. Prefer the configured default on boot and session.create, pin TERMINAL_CWD at backend spawn, and reject packaged install-dir paths that regressed after #37536. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address review on default project dir PR Add workspace cwd precedence tests, extract isPackagedInstallPath for platform test coverage, and stop rewriting live $currentCwd when a session is already active (cache-only until the next new chat). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…#37536) * fix(desktop): triage 24 GUI quality-of-life fixes across sidebar, composer, tool cards, messaging, and platform plumbing A grab-bag of high-leverage UX fixes plus a few backend touches that the GUI needs to behave correctly on Windows. Sidebar / sessions - Decrement $sessionsTotal on delete + archive so "Load N more" stops claiming removed rows are still on the server. - Hide the "Group by workspace" toggle when no unpinned sessions exist. - Accept Cmd/Ctrl+N as a "new session" accelerator (in addition to bare Shift+N), and render the kbd hint per-platform. - Switch the statusbar to overflow-x-clip so untitled sessions don't paint a horizontal scrollbar at the bottom of the window. Messaging + Cron - Add [-webkit-app-region: no-drag] to the page-search input so clicks reach the field instead of routing to the OS window-drag handler. - Replace single-letter PlatformAvatar with brand glyphs from @icons-pack/react-simple-icons (telegram, discord, matrix, signal, whatsapp, mattermost, wechat, qq, ...). Letter monogram fallback for Slack / Dingtalk / Feishu / WeCom (removed from Simple Icons at brand owner request). - Drop the duplicate "Create first cron" button in the empty state. Composer - Dedupe pasted images by (name, size, lastModified, type) instead of Blob identity; Chromium hands us the same screenshot via both clipboard.items and clipboard.files with fresh File instances. - Enable spellcheck on the contentEditable, configure Chromium's spellchecker with the system locale on whenReady, and add replaceMisspelling + "Add to dictionary" entries to the context menu. - Render user messages through a minimal markdown pipeline (inline backtick code + fenced ``` blocks) while keeping @file:/@image: directive chips intact. - max-h-[60vh] overflow-y-auto + collisionPadding on the prompt-snippet submenu. - Bake cursor-pointer into the <Button> primitive (with disabled:cursor-default) and into titlebarButtonClass. Dialogs + tabs + version - Default DialogContent now has max-h-[85vh] overflow-y-auto so long bodies scroll instead of falling off-screen. - Right-rail preview tabs close on middle-click (button === 1), with an onMouseDown swallow to suppress Chromium autoscroll. - New refreshDesktopVersion() helper called from About mount, after every update check, and on throttled window focus so About reflects the just-installed binary. Keys + Artifacts + Terminal - Drop the global "Show advanced" toggle in KeysSettings. Provider groups now default-expand when they have any key set. - Extend openExternalUrl to handle file:// via shell.openPath, with showItemInFolder fallback when the OS can't open the file. - New lib/ansi.ts SGR parser + <AnsiText> component, applied to terminal/execute_code tool output. - ToolView gained stdout / stderr / rendersAnsi; tool-fallback renders the two streams as separate labeled blocks with stderr in a neutral tone (not destructive — many CLIs log info on stderr). - Drop 'stderr' from ERROR_MSG_KEYS in tool-result-summary. Paths + platform - resolveHermesCwd skips process.cwd() when packaged and prefers a user-configurable default project directory. - New hermes:setting:defaultProjectDir:{get,set,pick} IPC handlers + preload bridge + global.d.ts typing + a "Default project directory" row in Sessions settings. - FileOperations.delete_path(path, recursive=True) on the abstract base; ShellFileOperations.delete_file rewritten to run a cross- platform python3 -c snippet so deletes work on Windows shells (which have no rm/rm -rf). Fallback to `python` when `python3` isn't on PATH. - README troubleshooting block split into macOS/Linux + Windows PowerShell recipes. - Tightened renderer favicon links in index.html + added color-scheme and theme-color meta. Backend lifecycle (renderer-side mitigation) - New noteSessionActivity() heartbeat + session.ts watchdog: an 8-minute silence on the stream auto-clears stuck $workingSessionIds entries so "Session Busy" never gets permanently wedged. Wired into useSessionStateCache so every state update refreshes the timer. i18n spike - docs/desktop-i18n-rfc.md scoping a future language-switcher PR (recommends react-intl, audits IME/RTL/CJK in the composer + chat bubbles, 4-PR rollout plan, ~3-4 eng-weeks for the first non-English locale). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): replace native OS scrollbar in portaled dropdown menus Radix's DropdownMenuPrimitive.Portal renders content under document.body, outside the `.scrollbar-dt` scope on #root. Whenever a menu's max-height clipped its content (even by a pixel — common for the composer "+" menu that opens upward near the bottom of the window), the user saw the OS's chunky native scrollbar painted across the whole menu. Bake a thin, slot-styled scrollbar onto DropdownMenuContent and DropdownMenuSubContent via [scrollbar-width:thin] + WebKit pseudo-element arbitrary variants. The submenu also gets a max-h tied to --radix-dropdown-menu-content-available-height so long snippet lists scroll cleanly instead of running off the bottom of the viewport. Drop the now- redundant max-h-[60vh] override on the prompt-snippet submenu. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): unbork dropdown menu — submenu opens, parent isn't a circle Two regressions from the previous dropdown-scrollbar fix: - The parent menu rendered as a rounded oval. Long Tailwind v4 arbitrary- variant strings like [&::-webkit-scrollbar-thumb]:rounded-full inside a cn() call were being mis-resolved so the `rounded-full` leaked onto the menu container itself. Replaced the whole tower of arbitrary variants with a real `.dt-portal-scrollbar` class in styles.css that mirrors what `.scrollbar-dt` already does for #root descendants. Plain CSS, no Tailwind parser ambiguity. - The Prompt snippets submenu didn't open. Radix publishes --radix-dropdown-menu-content-available-height on Content but NOT on SubContent, so the `max-h` bound to that variable computed to 0 and the submenu collapsed to zero height. Switched SubContent to a fixed max-h-80 (≈20rem) which is plenty for a snippet list and never collapses. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): promote prompt snippets from Radix submenu to a real Dialog The submenu refused to open when the parent dropdown was anchored at the bottom of the window (composer "+" button) — Radix's collision detection + SubContent positioning was fighting us. Rather than keep tuning side / sideOffset / collisionPadding / max-h until something stuck, replace the DropdownMenuSub with a clicked DropdownMenuItem that opens a proper Dialog. Side benefits over the submenu: - Each snippet gets a description line, so a glance is enough to pick one. - Focus management is handled by Dialog automatically. - Easy to grow (search, custom user snippets, categories) without another round of Radix positioning bugs. Also extract types/interfaces to the bottom of the file per workspace convention. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): move cron 'New cron' button off the top bar into the body Reverses the previous direction on cron empty-state dedup. The body button is more discoverable for first-time users (it's anchored next to the "No scheduled jobs yet" copy that explains the feature) and frees the top bar from a global CTA that wasn't pulling its weight. - Empty (zero jobs): EmptyState renders the "Create first cron" button again, like the original design. - Empty (search filtered out all jobs): no button, just "Try a broader search query" copy. - Has jobs: small inline header above the list shows `N/M active` plus a single "New cron" button (right-aligned). The rows themselves already cover edit/pause/trigger/delete, so this is the only "create" affordance. Also drop the dead `<div className="hidden">…</div>` enabledCount line the previous patch left behind; the count is now visible in the new header instead of hidden. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address Copilot review on PR 37536 - sessions-settings: guard the WHOLE bridge call rather than chaining `?.settings.foo().then(...)` — the latter throws when `window.hermesDesktop` is undefined (non-Electron / Vitest contexts) because the chain short-circuits to `undefined.then(...)`. - file_operations: drop `Path.unlink(missing_ok=True)` (Py>=3.8) so the generated delete snippet still works on remote backends running Python 3.7. The existing FileNotFoundError handler covers the same case and works back to 3.4. - ansi.test.ts: add focused Vitest coverage for the SGR parser (basic/bright colors, bold toggles, default-fg reset, coalescing, 256-color / truecolor arg consumption, non-SGR CSI drop, empty SGR full-reset) so future refactors can't silently regress terminal rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop/updates): swallow refreshDesktopVersion bridge errors `refreshDesktopVersion()` is called best-effort with `void` from `checkUpdates()`, `startUpdatePoller()`, and the window focus handler. If the IPC bridge rejects (main process shutting down during reload, bridge not yet ready on first paint), the rejection surfaces as an unhandled promise rejection in the renderer. Wrap the call in try/catch and return null on failure so callers can keep the existing fire-and-forget pattern safely. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(desktop): drop work duplicated by other in-flight PRs - composer/text-utils.ts: revert paste-image dedupe — PR NousResearch#37596 ships the same fix with a cleaner content-key approach and a Vitest file (text-utils.test.ts). Letting that PR own the change. - docs/desktop-i18n-rfc.md: delete the i18n scoping RFC — PR NousResearch#37568 has already shipped a working i18n surface (homegrown nanostores `t()` helper over en/zh dictionaries), so the RFC's framework recommendation (`react-intl`) is now obsolete and would just contradict the implementation that's actually landing. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…esearch#43234) * fix(desktop): honor default project directory for new sessions The Settings picker persisted project-dir.json but the renderer kept seeding new chats from sticky localStorage home. Prefer the configured default on boot and session.create, pin TERMINAL_CWD at backend spawn, and reject packaged install-dir paths that regressed after NousResearch#37536. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address review on default project dir PR Add workspace cwd precedence tests, extract isPackagedInstallPath for platform test coverage, and stop rewriting live $currentCwd when a session is already active (cache-only until the next new chat). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…#37536) * fix(desktop): triage 24 GUI quality-of-life fixes across sidebar, composer, tool cards, messaging, and platform plumbing A grab-bag of high-leverage UX fixes plus a few backend touches that the GUI needs to behave correctly on Windows. Sidebar / sessions - Decrement $sessionsTotal on delete + archive so "Load N more" stops claiming removed rows are still on the server. - Hide the "Group by workspace" toggle when no unpinned sessions exist. - Accept Cmd/Ctrl+N as a "new session" accelerator (in addition to bare Shift+N), and render the kbd hint per-platform. - Switch the statusbar to overflow-x-clip so untitled sessions don't paint a horizontal scrollbar at the bottom of the window. Messaging + Cron - Add [-webkit-app-region: no-drag] to the page-search input so clicks reach the field instead of routing to the OS window-drag handler. - Replace single-letter PlatformAvatar with brand glyphs from @icons-pack/react-simple-icons (telegram, discord, matrix, signal, whatsapp, mattermost, wechat, qq, ...). Letter monogram fallback for Slack / Dingtalk / Feishu / WeCom (removed from Simple Icons at brand owner request). - Drop the duplicate "Create first cron" button in the empty state. Composer - Dedupe pasted images by (name, size, lastModified, type) instead of Blob identity; Chromium hands us the same screenshot via both clipboard.items and clipboard.files with fresh File instances. - Enable spellcheck on the contentEditable, configure Chromium's spellchecker with the system locale on whenReady, and add replaceMisspelling + "Add to dictionary" entries to the context menu. - Render user messages through a minimal markdown pipeline (inline backtick code + fenced ``` blocks) while keeping @file:/@image: directive chips intact. - max-h-[60vh] overflow-y-auto + collisionPadding on the prompt-snippet submenu. - Bake cursor-pointer into the <Button> primitive (with disabled:cursor-default) and into titlebarButtonClass. Dialogs + tabs + version - Default DialogContent now has max-h-[85vh] overflow-y-auto so long bodies scroll instead of falling off-screen. - Right-rail preview tabs close on middle-click (button === 1), with an onMouseDown swallow to suppress Chromium autoscroll. - New refreshDesktopVersion() helper called from About mount, after every update check, and on throttled window focus so About reflects the just-installed binary. Keys + Artifacts + Terminal - Drop the global "Show advanced" toggle in KeysSettings. Provider groups now default-expand when they have any key set. - Extend openExternalUrl to handle file:// via shell.openPath, with showItemInFolder fallback when the OS can't open the file. - New lib/ansi.ts SGR parser + <AnsiText> component, applied to terminal/execute_code tool output. - ToolView gained stdout / stderr / rendersAnsi; tool-fallback renders the two streams as separate labeled blocks with stderr in a neutral tone (not destructive — many CLIs log info on stderr). - Drop 'stderr' from ERROR_MSG_KEYS in tool-result-summary. Paths + platform - resolveHermesCwd skips process.cwd() when packaged and prefers a user-configurable default project directory. - New hermes:setting:defaultProjectDir:{get,set,pick} IPC handlers + preload bridge + global.d.ts typing + a "Default project directory" row in Sessions settings. - FileOperations.delete_path(path, recursive=True) on the abstract base; ShellFileOperations.delete_file rewritten to run a cross- platform python3 -c snippet so deletes work on Windows shells (which have no rm/rm -rf). Fallback to `python` when `python3` isn't on PATH. - README troubleshooting block split into macOS/Linux + Windows PowerShell recipes. - Tightened renderer favicon links in index.html + added color-scheme and theme-color meta. Backend lifecycle (renderer-side mitigation) - New noteSessionActivity() heartbeat + session.ts watchdog: an 8-minute silence on the stream auto-clears stuck $workingSessionIds entries so "Session Busy" never gets permanently wedged. Wired into useSessionStateCache so every state update refreshes the timer. i18n spike - docs/desktop-i18n-rfc.md scoping a future language-switcher PR (recommends react-intl, audits IME/RTL/CJK in the composer + chat bubbles, 4-PR rollout plan, ~3-4 eng-weeks for the first non-English locale). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): replace native OS scrollbar in portaled dropdown menus Radix's DropdownMenuPrimitive.Portal renders content under document.body, outside the `.scrollbar-dt` scope on #root. Whenever a menu's max-height clipped its content (even by a pixel — common for the composer "+" menu that opens upward near the bottom of the window), the user saw the OS's chunky native scrollbar painted across the whole menu. Bake a thin, slot-styled scrollbar onto DropdownMenuContent and DropdownMenuSubContent via [scrollbar-width:thin] + WebKit pseudo-element arbitrary variants. The submenu also gets a max-h tied to --radix-dropdown-menu-content-available-height so long snippet lists scroll cleanly instead of running off the bottom of the viewport. Drop the now- redundant max-h-[60vh] override on the prompt-snippet submenu. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): unbork dropdown menu — submenu opens, parent isn't a circle Two regressions from the previous dropdown-scrollbar fix: - The parent menu rendered as a rounded oval. Long Tailwind v4 arbitrary- variant strings like [&::-webkit-scrollbar-thumb]:rounded-full inside a cn() call were being mis-resolved so the `rounded-full` leaked onto the menu container itself. Replaced the whole tower of arbitrary variants with a real `.dt-portal-scrollbar` class in styles.css that mirrors what `.scrollbar-dt` already does for #root descendants. Plain CSS, no Tailwind parser ambiguity. - The Prompt snippets submenu didn't open. Radix publishes --radix-dropdown-menu-content-available-height on Content but NOT on SubContent, so the `max-h` bound to that variable computed to 0 and the submenu collapsed to zero height. Switched SubContent to a fixed max-h-80 (≈20rem) which is plenty for a snippet list and never collapses. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): promote prompt snippets from Radix submenu to a real Dialog The submenu refused to open when the parent dropdown was anchored at the bottom of the window (composer "+" button) — Radix's collision detection + SubContent positioning was fighting us. Rather than keep tuning side / sideOffset / collisionPadding / max-h until something stuck, replace the DropdownMenuSub with a clicked DropdownMenuItem that opens a proper Dialog. Side benefits over the submenu: - Each snippet gets a description line, so a glance is enough to pick one. - Focus management is handled by Dialog automatically. - Easy to grow (search, custom user snippets, categories) without another round of Radix positioning bugs. Also extract types/interfaces to the bottom of the file per workspace convention. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): move cron 'New cron' button off the top bar into the body Reverses the previous direction on cron empty-state dedup. The body button is more discoverable for first-time users (it's anchored next to the "No scheduled jobs yet" copy that explains the feature) and frees the top bar from a global CTA that wasn't pulling its weight. - Empty (zero jobs): EmptyState renders the "Create first cron" button again, like the original design. - Empty (search filtered out all jobs): no button, just "Try a broader search query" copy. - Has jobs: small inline header above the list shows `N/M active` plus a single "New cron" button (right-aligned). The rows themselves already cover edit/pause/trigger/delete, so this is the only "create" affordance. Also drop the dead `<div className="hidden">…</div>` enabledCount line the previous patch left behind; the count is now visible in the new header instead of hidden. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address Copilot review on PR 37536 - sessions-settings: guard the WHOLE bridge call rather than chaining `?.settings.foo().then(...)` — the latter throws when `window.hermesDesktop` is undefined (non-Electron / Vitest contexts) because the chain short-circuits to `undefined.then(...)`. - file_operations: drop `Path.unlink(missing_ok=True)` (Py>=3.8) so the generated delete snippet still works on remote backends running Python 3.7. The existing FileNotFoundError handler covers the same case and works back to 3.4. - ansi.test.ts: add focused Vitest coverage for the SGR parser (basic/bright colors, bold toggles, default-fg reset, coalescing, 256-color / truecolor arg consumption, non-SGR CSI drop, empty SGR full-reset) so future refactors can't silently regress terminal rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop/updates): swallow refreshDesktopVersion bridge errors `refreshDesktopVersion()` is called best-effort with `void` from `checkUpdates()`, `startUpdatePoller()`, and the window focus handler. If the IPC bridge rejects (main process shutting down during reload, bridge not yet ready on first paint), the rejection surfaces as an unhandled promise rejection in the renderer. Wrap the call in try/catch and return null on failure so callers can keep the existing fire-and-forget pattern safely. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(desktop): drop work duplicated by other in-flight PRs - composer/text-utils.ts: revert paste-image dedupe — PR NousResearch#37596 ships the same fix with a cleaner content-key approach and a Vitest file (text-utils.test.ts). Letting that PR own the change. - docs/desktop-i18n-rfc.md: delete the i18n scoping RFC — PR NousResearch#37568 has already shipped a working i18n surface (homegrown nanostores `t()` helper over en/zh dictionaries), so the RFC's framework recommendation (`react-intl`) is now obsolete and would just contradict the implementation that's actually landing. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…esearch#43234) * fix(desktop): honor default project directory for new sessions The Settings picker persisted project-dir.json but the renderer kept seeding new chats from sticky localStorage home. Prefer the configured default on boot and session.create, pin TERMINAL_CWD at backend spawn, and reject packaged install-dir paths that regressed after NousResearch#37536. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address review on default project dir PR Add workspace cwd precedence tests, extract isPackagedInstallPath for platform test coverage, and stop rewriting live $currentCwd when a session is already active (cache-only until the next new chat). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…#37536) * fix(desktop): triage 24 GUI quality-of-life fixes across sidebar, composer, tool cards, messaging, and platform plumbing A grab-bag of high-leverage UX fixes plus a few backend touches that the GUI needs to behave correctly on Windows. Sidebar / sessions - Decrement $sessionsTotal on delete + archive so "Load N more" stops claiming removed rows are still on the server. - Hide the "Group by workspace" toggle when no unpinned sessions exist. - Accept Cmd/Ctrl+N as a "new session" accelerator (in addition to bare Shift+N), and render the kbd hint per-platform. - Switch the statusbar to overflow-x-clip so untitled sessions don't paint a horizontal scrollbar at the bottom of the window. Messaging + Cron - Add [-webkit-app-region: no-drag] to the page-search input so clicks reach the field instead of routing to the OS window-drag handler. - Replace single-letter PlatformAvatar with brand glyphs from @icons-pack/react-simple-icons (telegram, discord, matrix, signal, whatsapp, mattermost, wechat, qq, ...). Letter monogram fallback for Slack / Dingtalk / Feishu / WeCom (removed from Simple Icons at brand owner request). - Drop the duplicate "Create first cron" button in the empty state. Composer - Dedupe pasted images by (name, size, lastModified, type) instead of Blob identity; Chromium hands us the same screenshot via both clipboard.items and clipboard.files with fresh File instances. - Enable spellcheck on the contentEditable, configure Chromium's spellchecker with the system locale on whenReady, and add replaceMisspelling + "Add to dictionary" entries to the context menu. - Render user messages through a minimal markdown pipeline (inline backtick code + fenced ``` blocks) while keeping @file:/@image: directive chips intact. - max-h-[60vh] overflow-y-auto + collisionPadding on the prompt-snippet submenu. - Bake cursor-pointer into the <Button> primitive (with disabled:cursor-default) and into titlebarButtonClass. Dialogs + tabs + version - Default DialogContent now has max-h-[85vh] overflow-y-auto so long bodies scroll instead of falling off-screen. - Right-rail preview tabs close on middle-click (button === 1), with an onMouseDown swallow to suppress Chromium autoscroll. - New refreshDesktopVersion() helper called from About mount, after every update check, and on throttled window focus so About reflects the just-installed binary. Keys + Artifacts + Terminal - Drop the global "Show advanced" toggle in KeysSettings. Provider groups now default-expand when they have any key set. - Extend openExternalUrl to handle file:// via shell.openPath, with showItemInFolder fallback when the OS can't open the file. - New lib/ansi.ts SGR parser + <AnsiText> component, applied to terminal/execute_code tool output. - ToolView gained stdout / stderr / rendersAnsi; tool-fallback renders the two streams as separate labeled blocks with stderr in a neutral tone (not destructive — many CLIs log info on stderr). - Drop 'stderr' from ERROR_MSG_KEYS in tool-result-summary. Paths + platform - resolveHermesCwd skips process.cwd() when packaged and prefers a user-configurable default project directory. - New hermes:setting:defaultProjectDir:{get,set,pick} IPC handlers + preload bridge + global.d.ts typing + a "Default project directory" row in Sessions settings. - FileOperations.delete_path(path, recursive=True) on the abstract base; ShellFileOperations.delete_file rewritten to run a cross- platform python3 -c snippet so deletes work on Windows shells (which have no rm/rm -rf). Fallback to `python` when `python3` isn't on PATH. - README troubleshooting block split into macOS/Linux + Windows PowerShell recipes. - Tightened renderer favicon links in index.html + added color-scheme and theme-color meta. Backend lifecycle (renderer-side mitigation) - New noteSessionActivity() heartbeat + session.ts watchdog: an 8-minute silence on the stream auto-clears stuck $workingSessionIds entries so "Session Busy" never gets permanently wedged. Wired into useSessionStateCache so every state update refreshes the timer. i18n spike - docs/desktop-i18n-rfc.md scoping a future language-switcher PR (recommends react-intl, audits IME/RTL/CJK in the composer + chat bubbles, 4-PR rollout plan, ~3-4 eng-weeks for the first non-English locale). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): replace native OS scrollbar in portaled dropdown menus Radix's DropdownMenuPrimitive.Portal renders content under document.body, outside the `.scrollbar-dt` scope on #root. Whenever a menu's max-height clipped its content (even by a pixel — common for the composer "+" menu that opens upward near the bottom of the window), the user saw the OS's chunky native scrollbar painted across the whole menu. Bake a thin, slot-styled scrollbar onto DropdownMenuContent and DropdownMenuSubContent via [scrollbar-width:thin] + WebKit pseudo-element arbitrary variants. The submenu also gets a max-h tied to --radix-dropdown-menu-content-available-height so long snippet lists scroll cleanly instead of running off the bottom of the viewport. Drop the now- redundant max-h-[60vh] override on the prompt-snippet submenu. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): unbork dropdown menu — submenu opens, parent isn't a circle Two regressions from the previous dropdown-scrollbar fix: - The parent menu rendered as a rounded oval. Long Tailwind v4 arbitrary- variant strings like [&::-webkit-scrollbar-thumb]:rounded-full inside a cn() call were being mis-resolved so the `rounded-full` leaked onto the menu container itself. Replaced the whole tower of arbitrary variants with a real `.dt-portal-scrollbar` class in styles.css that mirrors what `.scrollbar-dt` already does for #root descendants. Plain CSS, no Tailwind parser ambiguity. - The Prompt snippets submenu didn't open. Radix publishes --radix-dropdown-menu-content-available-height on Content but NOT on SubContent, so the `max-h` bound to that variable computed to 0 and the submenu collapsed to zero height. Switched SubContent to a fixed max-h-80 (≈20rem) which is plenty for a snippet list and never collapses. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): promote prompt snippets from Radix submenu to a real Dialog The submenu refused to open when the parent dropdown was anchored at the bottom of the window (composer "+" button) — Radix's collision detection + SubContent positioning was fighting us. Rather than keep tuning side / sideOffset / collisionPadding / max-h until something stuck, replace the DropdownMenuSub with a clicked DropdownMenuItem that opens a proper Dialog. Side benefits over the submenu: - Each snippet gets a description line, so a glance is enough to pick one. - Focus management is handled by Dialog automatically. - Easy to grow (search, custom user snippets, categories) without another round of Radix positioning bugs. Also extract types/interfaces to the bottom of the file per workspace convention. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): move cron 'New cron' button off the top bar into the body Reverses the previous direction on cron empty-state dedup. The body button is more discoverable for first-time users (it's anchored next to the "No scheduled jobs yet" copy that explains the feature) and frees the top bar from a global CTA that wasn't pulling its weight. - Empty (zero jobs): EmptyState renders the "Create first cron" button again, like the original design. - Empty (search filtered out all jobs): no button, just "Try a broader search query" copy. - Has jobs: small inline header above the list shows `N/M active` plus a single "New cron" button (right-aligned). The rows themselves already cover edit/pause/trigger/delete, so this is the only "create" affordance. Also drop the dead `<div className="hidden">…</div>` enabledCount line the previous patch left behind; the count is now visible in the new header instead of hidden. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address Copilot review on PR 37536 - sessions-settings: guard the WHOLE bridge call rather than chaining `?.settings.foo().then(...)` — the latter throws when `window.hermesDesktop` is undefined (non-Electron / Vitest contexts) because the chain short-circuits to `undefined.then(...)`. - file_operations: drop `Path.unlink(missing_ok=True)` (Py>=3.8) so the generated delete snippet still works on remote backends running Python 3.7. The existing FileNotFoundError handler covers the same case and works back to 3.4. - ansi.test.ts: add focused Vitest coverage for the SGR parser (basic/bright colors, bold toggles, default-fg reset, coalescing, 256-color / truecolor arg consumption, non-SGR CSI drop, empty SGR full-reset) so future refactors can't silently regress terminal rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop/updates): swallow refreshDesktopVersion bridge errors `refreshDesktopVersion()` is called best-effort with `void` from `checkUpdates()`, `startUpdatePoller()`, and the window focus handler. If the IPC bridge rejects (main process shutting down during reload, bridge not yet ready on first paint), the rejection surfaces as an unhandled promise rejection in the renderer. Wrap the call in try/catch and return null on failure so callers can keep the existing fire-and-forget pattern safely. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(desktop): drop work duplicated by other in-flight PRs - composer/text-utils.ts: revert paste-image dedupe — PR NousResearch#37596 ships the same fix with a cleaner content-key approach and a Vitest file (text-utils.test.ts). Letting that PR own the change. - docs/desktop-i18n-rfc.md: delete the i18n scoping RFC — PR NousResearch#37568 has already shipped a working i18n surface (homegrown nanostores `t()` helper over en/zh dictionaries), so the RFC's framework recommendation (`react-intl`) is now obsolete and would just contradict the implementation that's actually landing. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…esearch#43234) * fix(desktop): honor default project directory for new sessions The Settings picker persisted project-dir.json but the renderer kept seeding new chats from sticky localStorage home. Prefer the configured default on boot and session.create, pin TERMINAL_CWD at backend spawn, and reject packaged install-dir paths that regressed after NousResearch#37536. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address review on default project dir PR Add workspace cwd precedence tests, extract isPackagedInstallPath for platform test coverage, and stop rewriting live $currentCwd when a session is already active (cache-only until the next new chat). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…#37536) * fix(desktop): triage 24 GUI quality-of-life fixes across sidebar, composer, tool cards, messaging, and platform plumbing A grab-bag of high-leverage UX fixes plus a few backend touches that the GUI needs to behave correctly on Windows. Sidebar / sessions - Decrement $sessionsTotal on delete + archive so "Load N more" stops claiming removed rows are still on the server. - Hide the "Group by workspace" toggle when no unpinned sessions exist. - Accept Cmd/Ctrl+N as a "new session" accelerator (in addition to bare Shift+N), and render the kbd hint per-platform. - Switch the statusbar to overflow-x-clip so untitled sessions don't paint a horizontal scrollbar at the bottom of the window. Messaging + Cron - Add [-webkit-app-region: no-drag] to the page-search input so clicks reach the field instead of routing to the OS window-drag handler. - Replace single-letter PlatformAvatar with brand glyphs from @icons-pack/react-simple-icons (telegram, discord, matrix, signal, whatsapp, mattermost, wechat, qq, ...). Letter monogram fallback for Slack / Dingtalk / Feishu / WeCom (removed from Simple Icons at brand owner request). - Drop the duplicate "Create first cron" button in the empty state. Composer - Dedupe pasted images by (name, size, lastModified, type) instead of Blob identity; Chromium hands us the same screenshot via both clipboard.items and clipboard.files with fresh File instances. - Enable spellcheck on the contentEditable, configure Chromium's spellchecker with the system locale on whenReady, and add replaceMisspelling + "Add to dictionary" entries to the context menu. - Render user messages through a minimal markdown pipeline (inline backtick code + fenced ``` blocks) while keeping @file:/@image: directive chips intact. - max-h-[60vh] overflow-y-auto + collisionPadding on the prompt-snippet submenu. - Bake cursor-pointer into the <Button> primitive (with disabled:cursor-default) and into titlebarButtonClass. Dialogs + tabs + version - Default DialogContent now has max-h-[85vh] overflow-y-auto so long bodies scroll instead of falling off-screen. - Right-rail preview tabs close on middle-click (button === 1), with an onMouseDown swallow to suppress Chromium autoscroll. - New refreshDesktopVersion() helper called from About mount, after every update check, and on throttled window focus so About reflects the just-installed binary. Keys + Artifacts + Terminal - Drop the global "Show advanced" toggle in KeysSettings. Provider groups now default-expand when they have any key set. - Extend openExternalUrl to handle file:// via shell.openPath, with showItemInFolder fallback when the OS can't open the file. - New lib/ansi.ts SGR parser + <AnsiText> component, applied to terminal/execute_code tool output. - ToolView gained stdout / stderr / rendersAnsi; tool-fallback renders the two streams as separate labeled blocks with stderr in a neutral tone (not destructive — many CLIs log info on stderr). - Drop 'stderr' from ERROR_MSG_KEYS in tool-result-summary. Paths + platform - resolveHermesCwd skips process.cwd() when packaged and prefers a user-configurable default project directory. - New hermes:setting:defaultProjectDir:{get,set,pick} IPC handlers + preload bridge + global.d.ts typing + a "Default project directory" row in Sessions settings. - FileOperations.delete_path(path, recursive=True) on the abstract base; ShellFileOperations.delete_file rewritten to run a cross- platform python3 -c snippet so deletes work on Windows shells (which have no rm/rm -rf). Fallback to `python` when `python3` isn't on PATH. - README troubleshooting block split into macOS/Linux + Windows PowerShell recipes. - Tightened renderer favicon links in index.html + added color-scheme and theme-color meta. Backend lifecycle (renderer-side mitigation) - New noteSessionActivity() heartbeat + session.ts watchdog: an 8-minute silence on the stream auto-clears stuck $workingSessionIds entries so "Session Busy" never gets permanently wedged. Wired into useSessionStateCache so every state update refreshes the timer. i18n spike - docs/desktop-i18n-rfc.md scoping a future language-switcher PR (recommends react-intl, audits IME/RTL/CJK in the composer + chat bubbles, 4-PR rollout plan, ~3-4 eng-weeks for the first non-English locale). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): replace native OS scrollbar in portaled dropdown menus Radix's DropdownMenuPrimitive.Portal renders content under document.body, outside the `.scrollbar-dt` scope on #root. Whenever a menu's max-height clipped its content (even by a pixel — common for the composer "+" menu that opens upward near the bottom of the window), the user saw the OS's chunky native scrollbar painted across the whole menu. Bake a thin, slot-styled scrollbar onto DropdownMenuContent and DropdownMenuSubContent via [scrollbar-width:thin] + WebKit pseudo-element arbitrary variants. The submenu also gets a max-h tied to --radix-dropdown-menu-content-available-height so long snippet lists scroll cleanly instead of running off the bottom of the viewport. Drop the now- redundant max-h-[60vh] override on the prompt-snippet submenu. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): unbork dropdown menu — submenu opens, parent isn't a circle Two regressions from the previous dropdown-scrollbar fix: - The parent menu rendered as a rounded oval. Long Tailwind v4 arbitrary- variant strings like [&::-webkit-scrollbar-thumb]:rounded-full inside a cn() call were being mis-resolved so the `rounded-full` leaked onto the menu container itself. Replaced the whole tower of arbitrary variants with a real `.dt-portal-scrollbar` class in styles.css that mirrors what `.scrollbar-dt` already does for #root descendants. Plain CSS, no Tailwind parser ambiguity. - The Prompt snippets submenu didn't open. Radix publishes --radix-dropdown-menu-content-available-height on Content but NOT on SubContent, so the `max-h` bound to that variable computed to 0 and the submenu collapsed to zero height. Switched SubContent to a fixed max-h-80 (≈20rem) which is plenty for a snippet list and never collapses. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): promote prompt snippets from Radix submenu to a real Dialog The submenu refused to open when the parent dropdown was anchored at the bottom of the window (composer "+" button) — Radix's collision detection + SubContent positioning was fighting us. Rather than keep tuning side / sideOffset / collisionPadding / max-h until something stuck, replace the DropdownMenuSub with a clicked DropdownMenuItem that opens a proper Dialog. Side benefits over the submenu: - Each snippet gets a description line, so a glance is enough to pick one. - Focus management is handled by Dialog automatically. - Easy to grow (search, custom user snippets, categories) without another round of Radix positioning bugs. Also extract types/interfaces to the bottom of the file per workspace convention. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): move cron 'New cron' button off the top bar into the body Reverses the previous direction on cron empty-state dedup. The body button is more discoverable for first-time users (it's anchored next to the "No scheduled jobs yet" copy that explains the feature) and frees the top bar from a global CTA that wasn't pulling its weight. - Empty (zero jobs): EmptyState renders the "Create first cron" button again, like the original design. - Empty (search filtered out all jobs): no button, just "Try a broader search query" copy. - Has jobs: small inline header above the list shows `N/M active` plus a single "New cron" button (right-aligned). The rows themselves already cover edit/pause/trigger/delete, so this is the only "create" affordance. Also drop the dead `<div className="hidden">…</div>` enabledCount line the previous patch left behind; the count is now visible in the new header instead of hidden. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address Copilot review on PR 37536 - sessions-settings: guard the WHOLE bridge call rather than chaining `?.settings.foo().then(...)` — the latter throws when `window.hermesDesktop` is undefined (non-Electron / Vitest contexts) because the chain short-circuits to `undefined.then(...)`. - file_operations: drop `Path.unlink(missing_ok=True)` (Py>=3.8) so the generated delete snippet still works on remote backends running Python 3.7. The existing FileNotFoundError handler covers the same case and works back to 3.4. - ansi.test.ts: add focused Vitest coverage for the SGR parser (basic/bright colors, bold toggles, default-fg reset, coalescing, 256-color / truecolor arg consumption, non-SGR CSI drop, empty SGR full-reset) so future refactors can't silently regress terminal rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop/updates): swallow refreshDesktopVersion bridge errors `refreshDesktopVersion()` is called best-effort with `void` from `checkUpdates()`, `startUpdatePoller()`, and the window focus handler. If the IPC bridge rejects (main process shutting down during reload, bridge not yet ready on first paint), the rejection surfaces as an unhandled promise rejection in the renderer. Wrap the call in try/catch and return null on failure so callers can keep the existing fire-and-forget pattern safely. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(desktop): drop work duplicated by other in-flight PRs - composer/text-utils.ts: revert paste-image dedupe — PR NousResearch#37596 ships the same fix with a cleaner content-key approach and a Vitest file (text-utils.test.ts). Letting that PR own the change. - docs/desktop-i18n-rfc.md: delete the i18n scoping RFC — PR NousResearch#37568 has already shipped a working i18n surface (homegrown nanostores `t()` helper over en/zh dictionaries), so the RFC's framework recommendation (`react-intl`) is now obsolete and would just contradict the implementation that's actually landing. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…esearch#43234) * fix(desktop): honor default project directory for new sessions The Settings picker persisted project-dir.json but the renderer kept seeding new chats from sticky localStorage home. Prefer the configured default on boot and session.create, pin TERMINAL_CWD at backend spawn, and reject packaged install-dir paths that regressed after NousResearch#37536. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address review on default project dir PR Add workspace cwd precedence tests, extract isPackagedInstallPath for platform test coverage, and stop rewriting live $currentCwd when a session is already active (cache-only until the next new chat). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…#37536) * fix(desktop): triage 24 GUI quality-of-life fixes across sidebar, composer, tool cards, messaging, and platform plumbing A grab-bag of high-leverage UX fixes plus a few backend touches that the GUI needs to behave correctly on Windows. Sidebar / sessions - Decrement $sessionsTotal on delete + archive so "Load N more" stops claiming removed rows are still on the server. - Hide the "Group by workspace" toggle when no unpinned sessions exist. - Accept Cmd/Ctrl+N as a "new session" accelerator (in addition to bare Shift+N), and render the kbd hint per-platform. - Switch the statusbar to overflow-x-clip so untitled sessions don't paint a horizontal scrollbar at the bottom of the window. Messaging + Cron - Add [-webkit-app-region: no-drag] to the page-search input so clicks reach the field instead of routing to the OS window-drag handler. - Replace single-letter PlatformAvatar with brand glyphs from @icons-pack/react-simple-icons (telegram, discord, matrix, signal, whatsapp, mattermost, wechat, qq, ...). Letter monogram fallback for Slack / Dingtalk / Feishu / WeCom (removed from Simple Icons at brand owner request). - Drop the duplicate "Create first cron" button in the empty state. Composer - Dedupe pasted images by (name, size, lastModified, type) instead of Blob identity; Chromium hands us the same screenshot via both clipboard.items and clipboard.files with fresh File instances. - Enable spellcheck on the contentEditable, configure Chromium's spellchecker with the system locale on whenReady, and add replaceMisspelling + "Add to dictionary" entries to the context menu. - Render user messages through a minimal markdown pipeline (inline backtick code + fenced ``` blocks) while keeping @file:/@image: directive chips intact. - max-h-[60vh] overflow-y-auto + collisionPadding on the prompt-snippet submenu. - Bake cursor-pointer into the <Button> primitive (with disabled:cursor-default) and into titlebarButtonClass. Dialogs + tabs + version - Default DialogContent now has max-h-[85vh] overflow-y-auto so long bodies scroll instead of falling off-screen. - Right-rail preview tabs close on middle-click (button === 1), with an onMouseDown swallow to suppress Chromium autoscroll. - New refreshDesktopVersion() helper called from About mount, after every update check, and on throttled window focus so About reflects the just-installed binary. Keys + Artifacts + Terminal - Drop the global "Show advanced" toggle in KeysSettings. Provider groups now default-expand when they have any key set. - Extend openExternalUrl to handle file:// via shell.openPath, with showItemInFolder fallback when the OS can't open the file. - New lib/ansi.ts SGR parser + <AnsiText> component, applied to terminal/execute_code tool output. - ToolView gained stdout / stderr / rendersAnsi; tool-fallback renders the two streams as separate labeled blocks with stderr in a neutral tone (not destructive — many CLIs log info on stderr). - Drop 'stderr' from ERROR_MSG_KEYS in tool-result-summary. Paths + platform - resolveHermesCwd skips process.cwd() when packaged and prefers a user-configurable default project directory. - New hermes:setting:defaultProjectDir:{get,set,pick} IPC handlers + preload bridge + global.d.ts typing + a "Default project directory" row in Sessions settings. - FileOperations.delete_path(path, recursive=True) on the abstract base; ShellFileOperations.delete_file rewritten to run a cross- platform python3 -c snippet so deletes work on Windows shells (which have no rm/rm -rf). Fallback to `python` when `python3` isn't on PATH. - README troubleshooting block split into macOS/Linux + Windows PowerShell recipes. - Tightened renderer favicon links in index.html + added color-scheme and theme-color meta. Backend lifecycle (renderer-side mitigation) - New noteSessionActivity() heartbeat + session.ts watchdog: an 8-minute silence on the stream auto-clears stuck $workingSessionIds entries so "Session Busy" never gets permanently wedged. Wired into useSessionStateCache so every state update refreshes the timer. i18n spike - docs/desktop-i18n-rfc.md scoping a future language-switcher PR (recommends react-intl, audits IME/RTL/CJK in the composer + chat bubbles, 4-PR rollout plan, ~3-4 eng-weeks for the first non-English locale). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): replace native OS scrollbar in portaled dropdown menus Radix's DropdownMenuPrimitive.Portal renders content under document.body, outside the `.scrollbar-dt` scope on #root. Whenever a menu's max-height clipped its content (even by a pixel — common for the composer "+" menu that opens upward near the bottom of the window), the user saw the OS's chunky native scrollbar painted across the whole menu. Bake a thin, slot-styled scrollbar onto DropdownMenuContent and DropdownMenuSubContent via [scrollbar-width:thin] + WebKit pseudo-element arbitrary variants. The submenu also gets a max-h tied to --radix-dropdown-menu-content-available-height so long snippet lists scroll cleanly instead of running off the bottom of the viewport. Drop the now- redundant max-h-[60vh] override on the prompt-snippet submenu. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): unbork dropdown menu — submenu opens, parent isn't a circle Two regressions from the previous dropdown-scrollbar fix: - The parent menu rendered as a rounded oval. Long Tailwind v4 arbitrary- variant strings like [&::-webkit-scrollbar-thumb]:rounded-full inside a cn() call were being mis-resolved so the `rounded-full` leaked onto the menu container itself. Replaced the whole tower of arbitrary variants with a real `.dt-portal-scrollbar` class in styles.css that mirrors what `.scrollbar-dt` already does for #root descendants. Plain CSS, no Tailwind parser ambiguity. - The Prompt snippets submenu didn't open. Radix publishes --radix-dropdown-menu-content-available-height on Content but NOT on SubContent, so the `max-h` bound to that variable computed to 0 and the submenu collapsed to zero height. Switched SubContent to a fixed max-h-80 (≈20rem) which is plenty for a snippet list and never collapses. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): promote prompt snippets from Radix submenu to a real Dialog The submenu refused to open when the parent dropdown was anchored at the bottom of the window (composer "+" button) — Radix's collision detection + SubContent positioning was fighting us. Rather than keep tuning side / sideOffset / collisionPadding / max-h until something stuck, replace the DropdownMenuSub with a clicked DropdownMenuItem that opens a proper Dialog. Side benefits over the submenu: - Each snippet gets a description line, so a glance is enough to pick one. - Focus management is handled by Dialog automatically. - Easy to grow (search, custom user snippets, categories) without another round of Radix positioning bugs. Also extract types/interfaces to the bottom of the file per workspace convention. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): move cron 'New cron' button off the top bar into the body Reverses the previous direction on cron empty-state dedup. The body button is more discoverable for first-time users (it's anchored next to the "No scheduled jobs yet" copy that explains the feature) and frees the top bar from a global CTA that wasn't pulling its weight. - Empty (zero jobs): EmptyState renders the "Create first cron" button again, like the original design. - Empty (search filtered out all jobs): no button, just "Try a broader search query" copy. - Has jobs: small inline header above the list shows `N/M active` plus a single "New cron" button (right-aligned). The rows themselves already cover edit/pause/trigger/delete, so this is the only "create" affordance. Also drop the dead `<div className="hidden">…</div>` enabledCount line the previous patch left behind; the count is now visible in the new header instead of hidden. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address Copilot review on PR 37536 - sessions-settings: guard the WHOLE bridge call rather than chaining `?.settings.foo().then(...)` — the latter throws when `window.hermesDesktop` is undefined (non-Electron / Vitest contexts) because the chain short-circuits to `undefined.then(...)`. - file_operations: drop `Path.unlink(missing_ok=True)` (Py>=3.8) so the generated delete snippet still works on remote backends running Python 3.7. The existing FileNotFoundError handler covers the same case and works back to 3.4. - ansi.test.ts: add focused Vitest coverage for the SGR parser (basic/bright colors, bold toggles, default-fg reset, coalescing, 256-color / truecolor arg consumption, non-SGR CSI drop, empty SGR full-reset) so future refactors can't silently regress terminal rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop/updates): swallow refreshDesktopVersion bridge errors `refreshDesktopVersion()` is called best-effort with `void` from `checkUpdates()`, `startUpdatePoller()`, and the window focus handler. If the IPC bridge rejects (main process shutting down during reload, bridge not yet ready on first paint), the rejection surfaces as an unhandled promise rejection in the renderer. Wrap the call in try/catch and return null on failure so callers can keep the existing fire-and-forget pattern safely. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(desktop): drop work duplicated by other in-flight PRs - composer/text-utils.ts: revert paste-image dedupe — PR NousResearch#37596 ships the same fix with a cleaner content-key approach and a Vitest file (text-utils.test.ts). Letting that PR own the change. - docs/desktop-i18n-rfc.md: delete the i18n scoping RFC — PR NousResearch#37568 has already shipped a working i18n surface (homegrown nanostores `t()` helper over en/zh dictionaries), so the RFC's framework recommendation (`react-intl`) is now obsolete and would just contradict the implementation that's actually landing. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…esearch#43234) * fix(desktop): honor default project directory for new sessions The Settings picker persisted project-dir.json but the renderer kept seeding new chats from sticky localStorage home. Prefer the configured default on boot and session.create, pin TERMINAL_CWD at backend spawn, and reject packaged install-dir paths that regressed after NousResearch#37536. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address review on default project dir PR Add workspace cwd precedence tests, extract isPackagedInstallPath for platform test coverage, and stop rewriting live $currentCwd when a session is already active (cache-only until the next new chat). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…#37536) * fix(desktop): triage 24 GUI quality-of-life fixes across sidebar, composer, tool cards, messaging, and platform plumbing A grab-bag of high-leverage UX fixes plus a few backend touches that the GUI needs to behave correctly on Windows. Sidebar / sessions - Decrement $sessionsTotal on delete + archive so "Load N more" stops claiming removed rows are still on the server. - Hide the "Group by workspace" toggle when no unpinned sessions exist. - Accept Cmd/Ctrl+N as a "new session" accelerator (in addition to bare Shift+N), and render the kbd hint per-platform. - Switch the statusbar to overflow-x-clip so untitled sessions don't paint a horizontal scrollbar at the bottom of the window. Messaging + Cron - Add [-webkit-app-region: no-drag] to the page-search input so clicks reach the field instead of routing to the OS window-drag handler. - Replace single-letter PlatformAvatar with brand glyphs from @icons-pack/react-simple-icons (telegram, discord, matrix, signal, whatsapp, mattermost, wechat, qq, ...). Letter monogram fallback for Slack / Dingtalk / Feishu / WeCom (removed from Simple Icons at brand owner request). - Drop the duplicate "Create first cron" button in the empty state. Composer - Dedupe pasted images by (name, size, lastModified, type) instead of Blob identity; Chromium hands us the same screenshot via both clipboard.items and clipboard.files with fresh File instances. - Enable spellcheck on the contentEditable, configure Chromium's spellchecker with the system locale on whenReady, and add replaceMisspelling + "Add to dictionary" entries to the context menu. - Render user messages through a minimal markdown pipeline (inline backtick code + fenced ``` blocks) while keeping @file:/@image: directive chips intact. - max-h-[60vh] overflow-y-auto + collisionPadding on the prompt-snippet submenu. - Bake cursor-pointer into the <Button> primitive (with disabled:cursor-default) and into titlebarButtonClass. Dialogs + tabs + version - Default DialogContent now has max-h-[85vh] overflow-y-auto so long bodies scroll instead of falling off-screen. - Right-rail preview tabs close on middle-click (button === 1), with an onMouseDown swallow to suppress Chromium autoscroll. - New refreshDesktopVersion() helper called from About mount, after every update check, and on throttled window focus so About reflects the just-installed binary. Keys + Artifacts + Terminal - Drop the global "Show advanced" toggle in KeysSettings. Provider groups now default-expand when they have any key set. - Extend openExternalUrl to handle file:// via shell.openPath, with showItemInFolder fallback when the OS can't open the file. - New lib/ansi.ts SGR parser + <AnsiText> component, applied to terminal/execute_code tool output. - ToolView gained stdout / stderr / rendersAnsi; tool-fallback renders the two streams as separate labeled blocks with stderr in a neutral tone (not destructive — many CLIs log info on stderr). - Drop 'stderr' from ERROR_MSG_KEYS in tool-result-summary. Paths + platform - resolveHermesCwd skips process.cwd() when packaged and prefers a user-configurable default project directory. - New hermes:setting:defaultProjectDir:{get,set,pick} IPC handlers + preload bridge + global.d.ts typing + a "Default project directory" row in Sessions settings. - FileOperations.delete_path(path, recursive=True) on the abstract base; ShellFileOperations.delete_file rewritten to run a cross- platform python3 -c snippet so deletes work on Windows shells (which have no rm/rm -rf). Fallback to `python` when `python3` isn't on PATH. - README troubleshooting block split into macOS/Linux + Windows PowerShell recipes. - Tightened renderer favicon links in index.html + added color-scheme and theme-color meta. Backend lifecycle (renderer-side mitigation) - New noteSessionActivity() heartbeat + session.ts watchdog: an 8-minute silence on the stream auto-clears stuck $workingSessionIds entries so "Session Busy" never gets permanently wedged. Wired into useSessionStateCache so every state update refreshes the timer. i18n spike - docs/desktop-i18n-rfc.md scoping a future language-switcher PR (recommends react-intl, audits IME/RTL/CJK in the composer + chat bubbles, 4-PR rollout plan, ~3-4 eng-weeks for the first non-English locale). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): replace native OS scrollbar in portaled dropdown menus Radix's DropdownMenuPrimitive.Portal renders content under document.body, outside the `.scrollbar-dt` scope on #root. Whenever a menu's max-height clipped its content (even by a pixel — common for the composer "+" menu that opens upward near the bottom of the window), the user saw the OS's chunky native scrollbar painted across the whole menu. Bake a thin, slot-styled scrollbar onto DropdownMenuContent and DropdownMenuSubContent via [scrollbar-width:thin] + WebKit pseudo-element arbitrary variants. The submenu also gets a max-h tied to --radix-dropdown-menu-content-available-height so long snippet lists scroll cleanly instead of running off the bottom of the viewport. Drop the now- redundant max-h-[60vh] override on the prompt-snippet submenu. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): unbork dropdown menu — submenu opens, parent isn't a circle Two regressions from the previous dropdown-scrollbar fix: - The parent menu rendered as a rounded oval. Long Tailwind v4 arbitrary- variant strings like [&::-webkit-scrollbar-thumb]:rounded-full inside a cn() call were being mis-resolved so the `rounded-full` leaked onto the menu container itself. Replaced the whole tower of arbitrary variants with a real `.dt-portal-scrollbar` class in styles.css that mirrors what `.scrollbar-dt` already does for #root descendants. Plain CSS, no Tailwind parser ambiguity. - The Prompt snippets submenu didn't open. Radix publishes --radix-dropdown-menu-content-available-height on Content but NOT on SubContent, so the `max-h` bound to that variable computed to 0 and the submenu collapsed to zero height. Switched SubContent to a fixed max-h-80 (≈20rem) which is plenty for a snippet list and never collapses. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): promote prompt snippets from Radix submenu to a real Dialog The submenu refused to open when the parent dropdown was anchored at the bottom of the window (composer "+" button) — Radix's collision detection + SubContent positioning was fighting us. Rather than keep tuning side / sideOffset / collisionPadding / max-h until something stuck, replace the DropdownMenuSub with a clicked DropdownMenuItem that opens a proper Dialog. Side benefits over the submenu: - Each snippet gets a description line, so a glance is enough to pick one. - Focus management is handled by Dialog automatically. - Easy to grow (search, custom user snippets, categories) without another round of Radix positioning bugs. Also extract types/interfaces to the bottom of the file per workspace convention. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): move cron 'New cron' button off the top bar into the body Reverses the previous direction on cron empty-state dedup. The body button is more discoverable for first-time users (it's anchored next to the "No scheduled jobs yet" copy that explains the feature) and frees the top bar from a global CTA that wasn't pulling its weight. - Empty (zero jobs): EmptyState renders the "Create first cron" button again, like the original design. - Empty (search filtered out all jobs): no button, just "Try a broader search query" copy. - Has jobs: small inline header above the list shows `N/M active` plus a single "New cron" button (right-aligned). The rows themselves already cover edit/pause/trigger/delete, so this is the only "create" affordance. Also drop the dead `<div className="hidden">…</div>` enabledCount line the previous patch left behind; the count is now visible in the new header instead of hidden. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address Copilot review on PR 37536 - sessions-settings: guard the WHOLE bridge call rather than chaining `?.settings.foo().then(...)` — the latter throws when `window.hermesDesktop` is undefined (non-Electron / Vitest contexts) because the chain short-circuits to `undefined.then(...)`. - file_operations: drop `Path.unlink(missing_ok=True)` (Py>=3.8) so the generated delete snippet still works on remote backends running Python 3.7. The existing FileNotFoundError handler covers the same case and works back to 3.4. - ansi.test.ts: add focused Vitest coverage for the SGR parser (basic/bright colors, bold toggles, default-fg reset, coalescing, 256-color / truecolor arg consumption, non-SGR CSI drop, empty SGR full-reset) so future refactors can't silently regress terminal rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop/updates): swallow refreshDesktopVersion bridge errors `refreshDesktopVersion()` is called best-effort with `void` from `checkUpdates()`, `startUpdatePoller()`, and the window focus handler. If the IPC bridge rejects (main process shutting down during reload, bridge not yet ready on first paint), the rejection surfaces as an unhandled promise rejection in the renderer. Wrap the call in try/catch and return null on failure so callers can keep the existing fire-and-forget pattern safely. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(desktop): drop work duplicated by other in-flight PRs - composer/text-utils.ts: revert paste-image dedupe — PR NousResearch#37596 ships the same fix with a cleaner content-key approach and a Vitest file (text-utils.test.ts). Letting that PR own the change. - docs/desktop-i18n-rfc.md: delete the i18n scoping RFC — PR NousResearch#37568 has already shipped a working i18n surface (homegrown nanostores `t()` helper over en/zh dictionaries), so the RFC's framework recommendation (`react-intl`) is now obsolete and would just contradict the implementation that's actually landing. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…esearch#43234) * fix(desktop): honor default project directory for new sessions The Settings picker persisted project-dir.json but the renderer kept seeding new chats from sticky localStorage home. Prefer the configured default on boot and session.create, pin TERMINAL_CWD at backend spawn, and reject packaged install-dir paths that regressed after NousResearch#37536. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address review on default project dir PR Add workspace cwd precedence tests, extract isPackagedInstallPath for platform test coverage, and stop rewriting live $currentCwd when a session is already active (cache-only until the next new chat). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…#37536) * fix(desktop): triage 24 GUI quality-of-life fixes across sidebar, composer, tool cards, messaging, and platform plumbing A grab-bag of high-leverage UX fixes plus a few backend touches that the GUI needs to behave correctly on Windows. Sidebar / sessions - Decrement $sessionsTotal on delete + archive so "Load N more" stops claiming removed rows are still on the server. - Hide the "Group by workspace" toggle when no unpinned sessions exist. - Accept Cmd/Ctrl+N as a "new session" accelerator (in addition to bare Shift+N), and render the kbd hint per-platform. - Switch the statusbar to overflow-x-clip so untitled sessions don't paint a horizontal scrollbar at the bottom of the window. Messaging + Cron - Add [-webkit-app-region: no-drag] to the page-search input so clicks reach the field instead of routing to the OS window-drag handler. - Replace single-letter PlatformAvatar with brand glyphs from @icons-pack/react-simple-icons (telegram, discord, matrix, signal, whatsapp, mattermost, wechat, qq, ...). Letter monogram fallback for Slack / Dingtalk / Feishu / WeCom (removed from Simple Icons at brand owner request). - Drop the duplicate "Create first cron" button in the empty state. Composer - Dedupe pasted images by (name, size, lastModified, type) instead of Blob identity; Chromium hands us the same screenshot via both clipboard.items and clipboard.files with fresh File instances. - Enable spellcheck on the contentEditable, configure Chromium's spellchecker with the system locale on whenReady, and add replaceMisspelling + "Add to dictionary" entries to the context menu. - Render user messages through a minimal markdown pipeline (inline backtick code + fenced ``` blocks) while keeping @file:/@image: directive chips intact. - max-h-[60vh] overflow-y-auto + collisionPadding on the prompt-snippet submenu. - Bake cursor-pointer into the <Button> primitive (with disabled:cursor-default) and into titlebarButtonClass. Dialogs + tabs + version - Default DialogContent now has max-h-[85vh] overflow-y-auto so long bodies scroll instead of falling off-screen. - Right-rail preview tabs close on middle-click (button === 1), with an onMouseDown swallow to suppress Chromium autoscroll. - New refreshDesktopVersion() helper called from About mount, after every update check, and on throttled window focus so About reflects the just-installed binary. Keys + Artifacts + Terminal - Drop the global "Show advanced" toggle in KeysSettings. Provider groups now default-expand when they have any key set. - Extend openExternalUrl to handle file:// via shell.openPath, with showItemInFolder fallback when the OS can't open the file. - New lib/ansi.ts SGR parser + <AnsiText> component, applied to terminal/execute_code tool output. - ToolView gained stdout / stderr / rendersAnsi; tool-fallback renders the two streams as separate labeled blocks with stderr in a neutral tone (not destructive — many CLIs log info on stderr). - Drop 'stderr' from ERROR_MSG_KEYS in tool-result-summary. Paths + platform - resolveHermesCwd skips process.cwd() when packaged and prefers a user-configurable default project directory. - New hermes:setting:defaultProjectDir:{get,set,pick} IPC handlers + preload bridge + global.d.ts typing + a "Default project directory" row in Sessions settings. - FileOperations.delete_path(path, recursive=True) on the abstract base; ShellFileOperations.delete_file rewritten to run a cross- platform python3 -c snippet so deletes work on Windows shells (which have no rm/rm -rf). Fallback to `python` when `python3` isn't on PATH. - README troubleshooting block split into macOS/Linux + Windows PowerShell recipes. - Tightened renderer favicon links in index.html + added color-scheme and theme-color meta. Backend lifecycle (renderer-side mitigation) - New noteSessionActivity() heartbeat + session.ts watchdog: an 8-minute silence on the stream auto-clears stuck $workingSessionIds entries so "Session Busy" never gets permanently wedged. Wired into useSessionStateCache so every state update refreshes the timer. i18n spike - docs/desktop-i18n-rfc.md scoping a future language-switcher PR (recommends react-intl, audits IME/RTL/CJK in the composer + chat bubbles, 4-PR rollout plan, ~3-4 eng-weeks for the first non-English locale). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): replace native OS scrollbar in portaled dropdown menus Radix's DropdownMenuPrimitive.Portal renders content under document.body, outside the `.scrollbar-dt` scope on #root. Whenever a menu's max-height clipped its content (even by a pixel — common for the composer "+" menu that opens upward near the bottom of the window), the user saw the OS's chunky native scrollbar painted across the whole menu. Bake a thin, slot-styled scrollbar onto DropdownMenuContent and DropdownMenuSubContent via [scrollbar-width:thin] + WebKit pseudo-element arbitrary variants. The submenu also gets a max-h tied to --radix-dropdown-menu-content-available-height so long snippet lists scroll cleanly instead of running off the bottom of the viewport. Drop the now- redundant max-h-[60vh] override on the prompt-snippet submenu. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): unbork dropdown menu — submenu opens, parent isn't a circle Two regressions from the previous dropdown-scrollbar fix: - The parent menu rendered as a rounded oval. Long Tailwind v4 arbitrary- variant strings like [&::-webkit-scrollbar-thumb]:rounded-full inside a cn() call were being mis-resolved so the `rounded-full` leaked onto the menu container itself. Replaced the whole tower of arbitrary variants with a real `.dt-portal-scrollbar` class in styles.css that mirrors what `.scrollbar-dt` already does for #root descendants. Plain CSS, no Tailwind parser ambiguity. - The Prompt snippets submenu didn't open. Radix publishes --radix-dropdown-menu-content-available-height on Content but NOT on SubContent, so the `max-h` bound to that variable computed to 0 and the submenu collapsed to zero height. Switched SubContent to a fixed max-h-80 (≈20rem) which is plenty for a snippet list and never collapses. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): promote prompt snippets from Radix submenu to a real Dialog The submenu refused to open when the parent dropdown was anchored at the bottom of the window (composer "+" button) — Radix's collision detection + SubContent positioning was fighting us. Rather than keep tuning side / sideOffset / collisionPadding / max-h until something stuck, replace the DropdownMenuSub with a clicked DropdownMenuItem that opens a proper Dialog. Side benefits over the submenu: - Each snippet gets a description line, so a glance is enough to pick one. - Focus management is handled by Dialog automatically. - Easy to grow (search, custom user snippets, categories) without another round of Radix positioning bugs. Also extract types/interfaces to the bottom of the file per workspace convention. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): move cron 'New cron' button off the top bar into the body Reverses the previous direction on cron empty-state dedup. The body button is more discoverable for first-time users (it's anchored next to the "No scheduled jobs yet" copy that explains the feature) and frees the top bar from a global CTA that wasn't pulling its weight. - Empty (zero jobs): EmptyState renders the "Create first cron" button again, like the original design. - Empty (search filtered out all jobs): no button, just "Try a broader search query" copy. - Has jobs: small inline header above the list shows `N/M active` plus a single "New cron" button (right-aligned). The rows themselves already cover edit/pause/trigger/delete, so this is the only "create" affordance. Also drop the dead `<div className="hidden">…</div>` enabledCount line the previous patch left behind; the count is now visible in the new header instead of hidden. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address Copilot review on PR 37536 - sessions-settings: guard the WHOLE bridge call rather than chaining `?.settings.foo().then(...)` — the latter throws when `window.hermesDesktop` is undefined (non-Electron / Vitest contexts) because the chain short-circuits to `undefined.then(...)`. - file_operations: drop `Path.unlink(missing_ok=True)` (Py>=3.8) so the generated delete snippet still works on remote backends running Python 3.7. The existing FileNotFoundError handler covers the same case and works back to 3.4. - ansi.test.ts: add focused Vitest coverage for the SGR parser (basic/bright colors, bold toggles, default-fg reset, coalescing, 256-color / truecolor arg consumption, non-SGR CSI drop, empty SGR full-reset) so future refactors can't silently regress terminal rendering. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop/updates): swallow refreshDesktopVersion bridge errors `refreshDesktopVersion()` is called best-effort with `void` from `checkUpdates()`, `startUpdatePoller()`, and the window focus handler. If the IPC bridge rejects (main process shutting down during reload, bridge not yet ready on first paint), the rejection surfaces as an unhandled promise rejection in the renderer. Wrap the call in try/catch and return null on failure so callers can keep the existing fire-and-forget pattern safely. Co-authored-by: Cursor <cursoragent@cursor.com> * chore(desktop): drop work duplicated by other in-flight PRs - composer/text-utils.ts: revert paste-image dedupe — PR NousResearch#37596 ships the same fix with a cleaner content-key approach and a Vitest file (text-utils.test.ts). Letting that PR own the change. - docs/desktop-i18n-rfc.md: delete the i18n scoping RFC — PR NousResearch#37568 has already shipped a working i18n surface (homegrown nanostores `t()` helper over en/zh dictionaries), so the RFC's framework recommendation (`react-intl`) is now obsolete and would just contradict the implementation that's actually landing. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
…esearch#43234) * fix(desktop): honor default project directory for new sessions The Settings picker persisted project-dir.json but the renderer kept seeding new chats from sticky localStorage home. Prefer the configured default on boot and session.create, pin TERMINAL_CWD at backend spawn, and reject packaged install-dir paths that regressed after NousResearch#37536. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(desktop): address review on default project dir PR Add workspace cwd precedence tests, extract isPackagedInstallPath for platform test coverage, and stop rewriting live $currentCwd when a session is already active (cache-only until the next new chat). Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
What does this PR do?
Triages a batch of Hermes Desktop (
apps/desktop) GUI quality-of-lifeissues. Most are renderer-only; a handful touch
electron/main.cjs(IPC, spellchecker,
file://, default project dir) and one touchestools/file_operations.py(Windows-safe deletes).Overlap with in-flight PRs (already dropped from this branch)
composer/text-utils.ts) — dropped in6dff9bee. PR fix(desktop): dedupe pasted clipboard images #37596 ships the same fix with a cleaner content-keyapproach and a Vitest file. Letting that PR own the change.
docs/desktop-i18n-rfc.md) — dropped in6dff9bee.PR feat(desktop): add i18n system with Chinese (zh) translation #37568 has already shipped a working i18n surface (homegrown
nanostores
t()helper + en/zh dictionaries), making the RFC'sframework recommendation (
react-intl) obsolete.Adjacent (not duplicate, but worth flagging for the merge)
concern via a sticky last-used
WORKSPACE_CWDlocalStorage key +a
_SESSION_CWDcontextvar inagent/runtime_cwd.py. The"Default project directory" work in this PR is intentionally a
different UX (explicit Settings picker + IPC-persisted JSON +
packaged
process.cwd()skip), so the two are kept side-by-side;they merge cleanly conceptually.
composer/index.tsxand
assistant-ui/thread.tsxfiles but only thekeydownhandlers; no logical conflict with the spellcheck attrs / user-
message-text switch here.
sidebar/index.tsxon adifferent surface than the group-by-workspace hide / kbd hint here.
Related Issue
Fixes #
Type of Change
Status of the triage list
Honest accounting against the original 23-item triage list.
Shipped (21):
app/messaging/platform-icon.tsx(new) +app/messaging/index.tsx,@icons-pack/react-simple-iconsapp/page-search-shell.tsx—[-webkit-app-region:no-drag]on the input containerapp/chat/composer/context-menu.tsx— converted the Radix submenu to a<Dialog>app/settings/keys-settings.tsx— auto-expand any provider that has a key setapp/cron/index.tsx— keep the empty-state "Create first cron" + inline "New cron" above the list when populatedcursor-pointeron clickablescomponents/ui/button.tsx+app/shell/titlebar.tscomponents/ui/dialog.tsx— defaultmax-h-[85vh] overflow-y-autoonDialogContentcomponents/assistant-ui/user-message-text.tsx(new) +thread.tsx— minimal markdown (inline + fenced) while keeping@file:/@image:chipsstore/updates.tsrefreshDesktopVersion()(with try/catch on the bridge), called from About mount, post-checkUpdates, the poller, and throttledfocusapp/chat/right-rail/preview.tsx—onAuxClick(button === 1) +onMouseDownswallow for Chromium autoscrollCmd/Ctrl+Nfor new sessionapp/desktop-controller.tsxglobal shortcut (works while typing) +Shift+Nwhen no input is focused; sidebar kbd hint updatedapp/shell/statusbar-controls.tsx—overflow-x-clipso a long "Connecting…" status item can't paint a scrollbar at the bottom of the windowapp/chat/sidebar/index.tsx— hide the toggle when there are no unpinned sessionsapp/session/hooks/use-session-actions.ts— decrement$sessionsTotalon success (with rollback on failure)HOME_DIR/PROJECTS_DIR(don't run fromwin-unpacked)electron/main.cjs—resolveHermesCwdskipsprocess.cwd()when packaged + newhermes:setting:defaultProjectDir:{get,set,pick}IPC; renderer side:app/settings/sessions-settings.tsx"Default project directory" rowapp/chat/composer/index.tsx—spellCheck+autoCorrect/Capitalize="off";electron/main.cjs—configureSpellChecker()(system locale) + context-menu replaceMisspelling + "Add to dictionary"electron/main.cjs—openExternalUrlhandlesfile://viashell.openPath→showItemInFolderfallbacklib/ansi.ts+components/assistant-ui/ansi-text.tsx(new) +tool-fallback.tsx; SGR 0/1/22/30-37/39/90-97 + graceful 38;5/38;2 consumption. Vitest coverage inlib/ansi.test.ts(14 cases)components/assistant-ui/tool-fallback-model.ts—ToolViewgainsstdout/stderr/rendersAnsi;tool-fallback.tsxrenders two labeled blocks with stderr in a neutral tone (npm/git/gcc emit informational text on stderr);lib/tool-result-summary.ts—'stderr'removed fromERROR_MSG_KEYSrm -rfdoesn't reliably work on Windowstools/file_operations.py— new abstractdelete_path(path, recursive=False);ShellFileOperationsrewritten to run a cross-platformpython3 -csnippet (pythonfallback) that usespathlib.Path.unlink+shutil.rmtree.README.mdtroubleshooting split into macOS/Linux + Windows PowerShell recipesPartial (1):
store/session.tsarms an 8-minute timer on everysetSessionWorking(true)/ state-update event (noteSessionActivity()); when it fires it forcibly drops the session from$workingSessionIdsso the UI never stays wedged forever.session.idle. Renderer subscriber and clear-on-idle is wired but has nothing to subscribe to until the backend cooperates. Should land in a follow-up PR with item #23.Not in this PR (1):
apps/desktop/. No code outsideapps/desktop/,tools/file_operations.py, anddocs/desktop-i18n-rfc.mdis touched in this PR. Follow-up needs to: thread a cancellation flag through the planner so the in-flight turn drops generated output, and have the gateway emit asession.idleevent the renderer can listen to.Other (not in the triage list):
components/ui/dropdown-menu.tsx+.dt-portal-scrollbarinstyles.css); the parent menu briefly rendered as a circle and the submenu wouldn't open mid-loop, both fixed.index.html(type,sizes,shortcut,color-scheme,theme-color).window.hermesDesktop?.settingschains insessions-settings.tsx(don't.then(...)anundefinedshort-circuit), droppedPath.unlink(missing_ok=True)so the remote-delete snippet still works on Python 3.7 backends, and wrappedrefreshDesktopVersion()in try/catch so the fire-and-forget callers can't generate unhandled promise rejections.How to Test
Sidebar / sessions
Cmd/Ctrl+N(orShift+Nwith no input focused) to start a new session.Messaging
Composer
`code`and a fenced```block — user bubble renders styled<code>/<pre>;@file:etc. chips still resolve.+menu → "Prompt snippets…" — modal opens with descriptions, selecting one inserts text.(Paste-image dedupe is covered by PR #37596; not re-tested here.)
Tool cards
terminal/execute_codethat emits both stdout and stderr — two labeled blocks, stderr in a neutral tone (not red).ls --color=always /tmp— ANSI colors render as styled spans.Settings
Cross-platform
delete_file/delete_pathtool calls succeed (the rewritten snippet runs throughpython -cinstead ofrm).file://URL — opens in the default OS handler, or reveals the file in the file manager if the OS can't open it.Session Busy (partial)
Checklist
Code
npm run test:ui -- src/lib/ansi.test.tspasses (14/14); pre-existing unrelated failures (pane-shellwidth-override,electron/*.test.cjscollected by Vitest) confirmed to baseline againstorigin/main. Python: thedelete_pathsnippet was sanity-tested locally against the four behaviors (missing file, file, dir-non-recursive, dir-recursive).lib/ansi.ts(14 cases) added during the review loop. The rest is renderer-only UX where Vitest coverage would need a full jsdom mount; happy to add if reviewers want it.file_operations.pyrewrite.Documentation & Housekeeping
apps/desktop/README.mdtroubleshooting)cli-config.yamlchangesrm, packagedprocess.cwd(), README guidance)Screenshots / Logs
Happy to capture before/after of the messaging icons, stdout/stderr split, ANSI rendering, and the spellchecker context menu if reviewers want them.