dbbd1d4d050146c8e2d0cd01eaa8543993dd98f9
82 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e0f6a35ac6 |
fix(desktop): render debug-report paste URLs as real clickable links
System messages (slash-command output like /debug, plus the generic system-message fallback) were rendered as plain text, so the uploaded paste.rs URLs in a debug report were neither clickable nor easily copyable. Route both through LinkifiedText so URLs become real <a> links (open externally via the desktop bridge, selectable/copyable text). Add an opt-in explicitOnly mode that matches only explicit http(s):// / www. URLs, used here so filename-shaped tokens in the report (agent.log, errors.log, gateway.log) aren't mistaken for bare domains and linkified. Bare-domain matching is preserved for all other LinkifiedText callers. Adds regression tests covering explicitOnly (links only real URLs, keeps .log filenames as text) and the default bare-domain behavior. |
||
|
|
d65b513f23 |
feat(desktop): hover-reveal collapsed sidebars as fixed overlays (#41670)
* feat(desktop): hover-reveal collapsed chat sidebar as a fixed overlay When the sessions sidebar is collapsed, hovering the left edge now floats it back in as a fixed overlay over the main content instead of just being hidden. The collapsed grid track stays at 0px so the panel never reserves space — it slides over whatever's underneath and retracts on pointer-leave. - PaneShell: new hoverReveal prop. When a pane is collapsed + hoverReveal, render an edge hot-zone + a side-anchored floating panel (absolute, full height, honors any persisted resize width) that slides in on hover/focus. - ChatSidebar: force the (otherwise opacity-0 when collapsed) sidebar fully visible + interactive while the overlay is revealed, via an in-data-[pane-hover-reveal=open] variant. - desktop-controller: opt the chat-sidebar pane into hoverReveal. * feat(desktop): lower window minWidth 900→400 Lets the window shrink to a narrow rail (e.g. for the collapsed hover-reveal sidebar) instead of being floored at 900px. * fix(desktop): render full sidebar content in hover-reveal overlay The hover-reveal overlay showed only the nav rail — session rows, search, pinned/recents were gated behind `sidebarOpen` (false while collapsed), so they never mounted in the floated panel. Add a $sidebarRevealed store the PaneShell overlay drives via a new onHoverRevealChange callback, and gate ChatSidebar's content on `sidebarOpen || sidebarRevealed` (contentVisible) instead of raw open state. The overlay now shows the complete sidebar. * fix(desktop): drop shadow on hover-reveal sidebar overlay * feat(desktop): hover-reveal the file-browser sidebar too The reveal mechanism already lives in the shared Pane primitive — the right rail just opts in with hoverReveal. Its content renders unconditionally, so (unlike the chat sidebar) it needs no extra content-visibility gating. * clean(desktop): tighten hover-reveal pane code KISS pass — flatten the translate ternary, derive a single `revealed`, inline the edge style, drop the redundant set-guard, and trim comments to the house one-liner style. No behavior change. * fix(desktop): stop hiding sidebar nav labels on narrow windows The nav labels (New session, Skills, …) and the ⌘N hint were gated on a viewport breakpoint (max-[46.25rem]:hidden), so shrinking the window hid them even when the sidebar itself was wide — including in the hover-reveal overlay. Drop the gate; the label already truncates (min-w-0 flex-1) so it ellipsizes gracefully in a narrow rail, and contentVisible already hides it when collapsed to the icon rail. * feat(desktop): auto-collapse both sidebars below 600px into hover-reveal Add a Pane `forceCollapsed` prop — collapses the track without writing to the store (so the saved open state restores when the window widens) while keeping hoverReveal alive (unlike `disabled`, which suppresses it). desktop-controller watches (max-width: 600px) and force-collapses the chat sidebar + file browser, so on a narrow window both rails get out of the way and the hover-reveal overlay becomes the way in. * feat(desktop): hover-intent + refined easing for sidebar reveal - Gate the reveal on pointer velocity: the full-height edge hot-zone now only arms on a slow, deliberate pass (<=0.55 px/ms). Fast sweeps toward the titlebar/statusbar — or off the window — blow past the threshold and never trigger, so the wide hit area stops being a nuisance. - Swap the slide easing to cubic-bezier(0.32,0.72,0,1) at 260ms (snappy-out, soft-land) for a more serious-app feel. * fix(desktop): don't reveal sidebar during window resize Resizing the window parks the cursor on the screen edge and fires slow pointermoves over the hot-zone, reading as deliberate intent. Guard the reveal on (a) e.buttons !== 0 — any button-held drag, incl. edge-resize — and (b) a 250ms cooldown after any window resize event. * feat(desktop): hoverIntent-style poll gate + inert contents during slide Replace the single-sample velocity check (too eager — fired on any one slow move, incl. resize drift) with a port of Brian Cherne's hoverIntent: poll the pointer every 90ms and only arm once it has *settled* (moved <5px between two consecutive polls inside the edge zone). Fly-bys, pass-throughs, and resize drift never produce two close samples in a row, so they don't trigger. Also keep the revealed panel's CONTENTS pointer-events-none until the slide-in transition finishes (onTransitionEnd → settled), so you can't misclick a session row mid-animation. Resets on retract. * fix(desktop): no cursor/hit-test leak before reveal settles The edge hot-zone showed cursor:pointer the instant the pointer touched it — before the panel was armed or in view. And contents were inert but the panel itself still hit-tested, so the cursor could flip mid-slide. Fix: hot-zone is cursor-default (it's invisible), and the whole panel is pointer-events-none until revealed && settled, so the cursor never changes or lands on a row before the slide-in finishes. * fix(desktop): geometry-driven close so revealed panel always retracts The revealed panel relied on its own onPointerLeave to close — but a panel that slid in under a still cursor (or whose contents were inert during the slide) never fires enter/leave, so it got stuck open (esp. the file browser). onTransitionEnd also bubbled from the file-tree's own row transitions, tripping the settled flag wrongly. Replace with a document-level pointermove watcher that closes once the cursor leaves the panel's bounding rect + a 24px grace — independent of pointer-events state or what the contents do. Gate interactivity on a simple slide-duration timer (interactive) instead of the fragile transitionEnd, so the cursor still can't flip or land on a row before the panel is in view. * feat(desktop): make sidebar toggle shortcuts reveal when force-collapsed mod+b / mod+j were no-ops on a narrow (force-collapsed) window — they flipped the store but the pane ignores it. Now the toggle handlers also dispatch PANE_TOGGLE_REVEAL_EVENT; a force-collapsed Pane listens (only while overlayActive) and flips its hover-reveal, so the shortcut floats the rail in (and back out) at this responsive breakpoint. * refactor(desktop): name the 600px sidebar collapse breakpoint Hoist the inline '(max-width: 600px)' literal into SIDEBAR_COLLAPSE_BREAKPOINT_PX + SIDEBAR_COLLAPSE_MEDIA_QUERY in layout-constants, so the responsive collapse point is a single named source of truth instead of a magic string in the controller. * tweak(desktop): sidebar auto-collapse breakpoint 600px -> 768px 768 is the standard md breakpoint and a more honest 'no room to dock' point. * tweak(desktop): halve sidebar reveal slide duration 260ms -> 130ms * Revert "tweak(desktop): halve sidebar reveal slide duration 260ms -> 130ms" This reverts commit 6009a132008105f5f871370d554a5097be3090af. * perf(desktop): pre-mount hover-reveal contents to kill slide-in stall The reveal mounted the (heavy, virtualized) sidebar contents in the same frame the slide started, so the browser stalled painting the transform until the mount finished — a ~100-200ms beat before the panel moved, very visible on the instant keyboard toggle (hover masked it via the 90ms intent poll). Report overlayActive (collapsed-overlay mode) rather than the live reveal state to the mount consumer, so contents stay mounted off-screen while collapsed and reveal is a pure transform. Visibility is still driven separately by the data-pane-hover-reveal attr + the slide transform. * fix(desktop): make reveal hotkey spammable Two throttles on the reveal toggle: - The handler fired both the reveal event AND toggleSidebarOpen() per press; the store write hits localStorage synchronously every keystroke + recomputes the grid, janking rapid presses. When collapsed, only dispatch the reveal event (the store toggle was a no-op anyway). - The geometry close-watcher slammed a keyboard-opened panel shut on the first stray pointermove (trackpad jitter), fighting hotkey spam. Keyboard reveals now ignore geometry until the cursor actually enters the panel, then the mouse takes over. * fix(desktop): inset reveal hot-zone past the OS window-resize gutter The hot-zone sat flush at the window edge (left-0/right-0), overlapping the OS resize grab strip — reaching to drag-resize naturally slows the cursor there, which hoverIntent reads as settled and reveals before the resize drag even starts. Inset the hot-zone 8px so the outermost edge stays a pure resize/drag region and only an intentful move just inside it arms a reveal. * fix(desktop): keep reveal hot-zone at edge, gate arming past resize gutter Insetting the hot-zone made it unreachable when moving fast. Instead, anchor the zone flush at the edge (w-4, always captures the pointer) but only ARM the reveal when the cursor settles >=8px in from the edge — so a resize-reach that parks on the outermost OS grab strip never triggers, while a deliberate move into the zone still does. Keeps polling while in the gutter so moving inward still arms. * refactor(desktop): rebuild hover-reveal as pure CSS, delete the JS state machine The hand-rolled pointer state machine (hoverIntent poll, refs, timers, document pointermove geometry-close, interactive gate, resize cooldowns, keyboard-held suppression) was fragile and side/instance-specific — hover broke on the right rail, keyboard toggles triggered phantom animations, resize popped it open. Replace all of it with the native primitive: CSS group-hover drives the slide transform; a transition-delay on enter (instant on leave) is the hover-intent gate (a fast pass-by doesn't dwell long enough to open); a thin edge trigger inset past the OS resize grab strip arms it; and a single `forced` bool (data-forced, toggled by the keyboard event) pins it open. Side-agnostic by construction — group-hover doesn't care which edge or which pane. Net: ~200 lines of imperative pointer logic → ~40 lines of declarative CSS. * fix(desktop): don't animate hover-reveal panel across viewport on side flip Flipping panes changed the off-screen transform from -translateX (off the left) to +translateX (off the right). transition-transform interpolated between them, passing through translate-x-0 (fully on-screen) mid-way — so the hidden panel visibly slid across the window to reach its new hiding spot. Key the panel on side so it remounts off-screen on the new edge with no transition to play. * clean(desktop): tighten hover-reveal markup KISS pass on the CSS-driven reveal: reuse the existing `side` instead of a local `left`, move the static duration/ease to inline style (drop two single-use CSS vars + their arbitrary-value classes, keep only the state-dependent enter-delay var), and trim comments to the house one-liner density. No behavior change. * fix(desktop): inset titlebar past traffic lights when sidebar is force-collapsed The titlebar content inset (clearing the macOS traffic lights) keyed off the stored sidebarOpen/fileBrowserOpen, but below the collapse breakpoint both rails are force-collapsed so the left edge is uncovered while the store still says open — content (the intro wordmark) overflowed under the lights. Gate leftEdgePaneOpen on !narrowViewport using the shared SIDEBAR_COLLAPSE_MEDIA_QUERY. Also rename the now-misleading reveal plumbing to match what it actually does: onHoverRevealChange -> onOverlayActiveChange, $sidebarRevealed -> $sidebarOverlayMounted (+ setter/consumer). It reports/stores collapsed-overlay mode (mount gate), not live reveal state. * feat(desktop): small --nous-shadow lift on revealed hover-reveal panels Add a --nous-shadow token (white-based on light, black-based on dark) and apply it to the floating sidebar panel only while revealed (group-hover / data-forced) so it reads as lifted off the surface. No shadow on the off-screen panel. * feat(desktop): shadow-reveal lift on revealed hover-reveal panels Mirror the --shadow-nous layered falloff into a new --shadow-reveal token whose drop color flips per mode (white on light, black on dark) via --shadow-reveal-raw set in :root / :root.dark. Apply the generated shadow-reveal utility to the floated panel only while revealed (group-hover / data-forced). Leaves the shared --shadow-nous untouched. * feat(desktop): use tuned reveal shadow, drop per-mode token Replace the --shadow-reveal token machinery with Brooklyn's tuned literal (0 -18px 18px -5px #0000003b) inline per-panel via --reveal-shadow, y-offset sign flipped for the right side. Same color both modes. Reverts styles.css to pristine (token removed). * fix(desktop): use the reveal shadow verbatim, don't invert it per side Flipping the y-offset sign for the right side inverted the shadow's direction (cast-up -> cast-down), making it read heavier — not a mirror. The mirror axis for a left/right panel is offset-x, which is 0 here, so both sides take the tuned value as-is: 0 -18px 18px -5px #0000003b. * clean(desktop): hoist reveal shadow to a named const Move the inline reveal-shadow literal to HOVER_REVEAL_SHADOW alongside the other HOVER_REVEAL_* tuning consts; drop the now-stale per-side comment. * fix(desktop): truncate titlebar title before the right tool cluster The session title used a hardcoded max-w-[52vw] that's blind to where the right-side tools start, so it ran under them at narrow widths / with pane tools present. Bound the title container by the same vars the titlebar drag region uses (--titlebar-content-inset + --titlebar-tools-right + --titlebar-tools-width) so it truncates exactly at the cluster's left edge. * fix(desktop): responsive markdown tables — floor width + nowrap headers The wrapper had overflow-x-auto but the table was w-full with auto layout, so instead of scrolling it crushed columns until even header words broke mid-word (Tim/e, Nig/ht). Add a min-w-[18rem] floor so it scrolls horizontally when the column is narrower than readable, and whitespace-nowrap on th so headers never break mid-word. Above the floor it still wraps cells naturally. * fix intro |
||
|
|
c50fb560ef |
Merge pull request #40433 from xxxigm/fix/desktop-chat-autoscroll
fix(desktop): stop chat transcript from jumping/flickering while reading (#37549) |
||
|
|
e029b7597b |
feat(desktop): stop the chat viewport from following streaming output (#41414)
The desktop chat GUI pinned the viewport to the bottom on every content growth while a turn streamed, so the window chased tokens as they arrived. Remove that follow behavior: once a turn is running the viewport stays exactly where the user left it. - Delete the streaming ResizeObserver re-pin loop in useThreadScrollAnchor. - Delete the post-run bottom lock (kept pinning ~1.2s after completion). - Keep the one-time jump-to-bottom on user submit / new turn / session change so a freshly submitted message still lands in view. - Update streaming.test.tsx to assert the viewport no longer follows streaming growth or snaps down on final code-highlight remeasure. |
||
|
|
16786f3bb3 |
feat(desktop+gateway): remote media relay — attach images/PDFs and display gateway images over the network
Desktop connected to a remote gateway can now attach images and PDFs and
display agent-written images. Previously the desktop passed a LOCAL file path
to image.attach; on a remote gateway that path doesn't exist, so the image was
silently dropped ("skipped unreadable path") and the vision model never saw it.
The reverse direction was also broken — images the agent wrote on the gateway
rendered as dead links in the remote client.
Gateway (tui_gateway/server.py):
- image.attach_bytes: base64 byte upload written into the gateway's own images
dir and queued via the existing native-image-attach pipeline. Magic-byte
extension sniffing, data-URL prefix + whitespace tolerance, 25 MB cap,
structured error codes. Accepts content_base64/filename (canonical) and
data/ext (older-desktop aliases).
- pdf.attach: renders each page to PNG via pdftoppm (poppler-utils) at 150 DPI
and queues the pages as images; 50 MB / 25-page caps. Accepts host path or
base64 upload.
- Shared helpers (_decode_attach_base64, _sniff_image_ext, _queue_attached_image)
so the two methods and the existing image.attach don't duplicate logic.
Gateway (hermes_cli/web_server.py):
- GET /api/media: returns a gateway-local image as a base64 data URL so remote
clients can display it. Auth-gated like every /api route, extension
allowlist + size cap, AND confined to the gateway's own media roots
(images/screenshots/cache, resolved symlink-safe) so an authed caller can't
read image-extension files anywhere on disk.
Desktop (apps/desktop):
- syncImageAttachmentsForSubmit uploads bytes via image.attach_bytes when the
connection mode is 'remote'; the local fast path is unchanged.
- media.ts gains isRemoteGateway() + gatewayMediaDataUrl(); directive-text and
markdown-text fetch images over /api/media in remote mode.
Consolidates the competing remote-media PRs (#38876, #40317, #21908, #39437)
into one coherent implementation, taking the strongest parts of each and adding
shared-helper cleanup plus the /api/media root-confinement hardening on top.
The per-profile gateway switching from #38876 is intentionally left out as a
separable feature. TUI file uploads (#40492) remain a separate surface.
Tested: 11 new tui_gateway tests + 5 /api/media endpoint tests + desktop
media.remote unit tests; full tui_gateway + web_server suites green (472
passed); tsc -b clean; E2E verified the full attach→disk→queue and
gateway-path→data-URL display round-trip plus the out-of-root security block.
Co-authored-by: Max Mitcham <maxmitcham@mac.home>
Co-authored-by: Justlrnal4 <Justlrnal4@users.noreply.github.com>
Co-authored-by: Chris Cook <ccook@nvms.com>
Co-authored-by: Thomas Paquette <thomas.paquette@gmail.com>
|
||
|
|
349a3f601c |
fix(desktop): stop bare-URL autolinker swallowing trailing emphasis asterisks (#41093)
The desktop markdown preprocessor autolinks bare URLs by wrapping them in <...>. RAW_URL_RE allowed '*' in its character classes, so a bold line with a URL and no separating space — e.g. '**PR opened: https://.../pull/123**' — greedily pulled the closing '**' into the href, producing a broken link and an unterminated bold run. Exclude '*' from both URL character classes; '_' and '~' (which can appear in real paths) are preserved. |
||
|
|
846821d8c0 |
Merge pull request #40684 from NousResearch/bb/cron-sessions-sidebar
feat(desktop): first-class cron jobs in the sidebar + dashboard scheduler |
||
|
|
d165933c56 |
docs(desktop): add DESIGN.md design-system guide + close two consistency gaps (#40823)
Codify the desktop overlay/design conventions in apps/desktop/DESIGN.md: surfaces & elevation (shadow-nous + --stroke-nous), stroke/color tokens, the single Button (variants/sizes, no per-call overrides), shared form controls (controlVariants / SearchField / SegmentedControl / Switch), flat layout (PAGE_INSET_X, OverlaySplitLayout, ListRow, no card-in-card), feedback states (Loader / ErrorState / LogView / EmptyState), BrandMark, motion, i18n, and the nanostore state model. Ends with a pre-merge checklist. Two fixes so the doc isn't aspirational: - brand-mark: rounded-md + overflow-hidden (doc says "softly rounded") - i18n ja/zh/zh-hant: mirror en's "Begin" + drop trailing period on connectedProvider (doc says update all locales together) |
||
|
|
f491260365 |
Merge remote-tracking branch 'origin/main' into bb/cron-sessions-sidebar
# Conflicts: # apps/desktop/src/app/cron/index.tsx |
||
|
|
f033b7dbfb |
feat(desktop): unified overlay design system, BrandMark & onboarding redesign (#40708)
* fix(desktop): unify dialog/overlay buttons on shared Button component
Replace raw <button> action/text controls across the modal layer (boot
failure, install, update, onboarding, clarify, model-visibility,
notifications, gateway menu) with the shared Button + its variants
(text / ghost / icon-xs). Drops the bespoke square-cornered styling so
every dialog matches the app's slightly-rounded button system, and
swaps clarify-tool's hardcoded "Skip" for the existing i18n string.
* feat(desktop): add dev-only dialog gallery for auditing overlays
A code-split, DEV-gated harness (toggle ⌘/Ctrl+Alt+Shift+D) that triggers
every dialog/overlay so their buttons can be eyeballed in one place:
store-driven overlays (boot failure, updates, notifications, sudo/secret)
plus in-place dialogs (confirm, profile create/rename, attach-url, model
picker/visibility, clarify, tool approval). Never ships to production.
* fix(desktop): use Ctrl+Shift+D for dialog gallery (mac-friendly)
The Cmd/Ctrl+Alt+Shift+D chord is impractical on macOS (Option mangles
the keypress). Ctrl+Shift+D is the same chord on every platform and uses
neither Cmd nor Option.
* fix(desktop): stop overriding button icon size to size-4
Action buttons hardcoded size-4 icons, overriding the Button component's
built-in size-3.5. That extra 2px is why boot-failure / onboarding / gateway
buttons looked chunkier than the settings "Apply" (size-3.5 spinner) despite
being the same component+size. Drop the overrides so icons inherit 3.5.
* feat(desktop): add BrandMark, use it in the updates overlay hero
New BrandMark renders the white logo.png on a hardcoded brand-blue tile
(#0000F2 light / #222 dark), replacing the generic Sparkles hero glyph in
the "update available" overlay. Trying it here first to iterate on the look.
NOTE: apps/desktop/public/logo.png is currently a 1x1 placeholder — the tile
renders now; the glyph appears once the real white logo art is dropped in.
* feat(desktop): add real logo.png asset, render it white in BrandMark
logo.png is blue line-art on transparent, so force it white via filter to
read on both the brand-blue (#0000F2) and near-black (#222) tiles. Bump the
glyph to 62% of the tile for the portrait aspect.
* fix(desktop): BrandMark renders logo as-is, no light bg/radius/padding
Drop the white filter, the hardcoded light-mode blue tile, the radius, and
the inner padding. Logo now fills the tile over a transparent surface in
light mode; dark keeps the #222 tile.
* fix(desktop): bump updates-overlay BrandMark to size-16
* feat(desktop): use downscaled karb.webp in BrandMark
Swap the BrandMark glyph to karb.webp, downscaled from 1129x1418/888KB to
254x320/81KB for the hero badge.
* feat(desktop): use nous-girl mark in BrandMark, invert in dark
Key the white background to transparent so only the black line-art remains
(384px/20KB webp). Light mode shows black art; dark mode flips it white via
dark:invert on the #222 tile. Drop the now-unused karb.webp and logo.png.
* fix(desktop): BrandMark uses nous-girl as-is (no transparent/invert)
The dark-mode invert read as a creepy negative. Use the opaque black-on-white
mark unchanged in both themes; drop the white-key, dark:invert, and #222 tile.
* fix(desktop): give BrandMark an explicit white bg tile
* fix(desktop): use nous-girl.jpg directly in BrandMark
* perf(desktop): downscale nous-girl.jpg to 256x256 (466KB -> 19KB)
* style(desktop): bump nous light --theme-secondary to 14% blue
* fix(desktop): outline button is transparent, not chrome-filled
The outline variant used bg-background (the chrome color), so on cards/overlays
with a different surface it rendered as an odd gray-blue fill (visible on the
boot overlay's Repair install / Use local gateway). Make it bg-transparent so
it inherits the surface like a real outline. Reverts the unrelated
--theme-secondary tweak.
* fix(desktop): clean outline button — thin border, no shadow/fill
Drop shadow-xs and the resting fills (light chrome bg, dark bg-input/30) so
outline is just a thin clean border with a subtle hover, in both themes.
* fix(desktop): stop forcing tertiary bg on outline buttons
A global [data-variant='outline'] rule set background: var(--ui-bg-tertiary),
which (attribute-selector specificity) overrode the cva bg-transparent — so
outline buttons always showed the pale tertiary fill on cards/overlays
regardless of the variant classes. Scope that fill to secondary only; outline
is now a true transparent border.
* style(desktop): unified overlay design system + restore #38631 flat-UI
Overlays/dialogs/toasts share a custom shadow-nous (downward-weighted) and
--stroke-nous hairline instead of hard borders: boot-failure, install,
notifications, model-picker, onboarding, prompt-overlays, updates, Dialog.
- button: outline is a 1px inset ring (no fill/shadow); chrome lives in Button
- BrandMark: 256px nous-girl mark replaces sparkle glyphs (updates/onboarding/about)
- onboarding: conditional header, lemniscate-bloom loaders, OTP device-code boxes,
NOUS CONNECTED hero (ascii decode) + cuneiform easter egg, "Begin" matrix exit
- shared LogView + ErrorState; math/ascii loaders over "Loading..." text
- appearance-settings flattened to SegmentedControl/ListRow; keybind-panel on
shadow-nous + text-variant reset
- restore flat-UI clobbered by #38631's stale-squash (
|
||
|
|
471a5fc5c9 |
feat(desktop): make cron jobs the first-class sidebar entity
Redesign the cron surface around jobs (not run sessions), following
power-user patterns (GitHub Actions / Airflow / Dagu): master → detail → output.
Sidebar "Cron jobs" section:
- jobs with a state pip + live next-run countdown
- click toggles an inline run-history peek; a run opens its chat (active run highlighted)
- hover: trigger-now + manage (open the Cron page)
- capped at 50 with a "50+" badge
Cron page: de-nested from a collapse-in-row accordion to master/detail —
job list + the selected job's schedule, actions, and run history.
Backend: GET /api/cron/jobs/{id}/runs lists a job's run sessions.
Share STATE_DOT/jobState across both surfaces; drop dead code/keys.
|
||
|
|
e3ae035921 |
Merge pull request #40660 from NousResearch/bb/keybinds
feat(desktop): rebindable keyboard shortcuts panel |
||
|
|
5e2b83a8ad |
feat(desktop): rebindable keyboard shortcuts panel
Add a central keybind registry + nanostore so desktop hotkeys are discoverable and user-rebindable. A titlebar ⌨ button (and ⌘/) opens a collapsible map grouped by Composer (read-only) / Profiles / Session / Navigation / View; click any chip to capture a new combo. Overrides persist to localStorage as a delta against shipped defaults, so future default changes aren't shadowed by a stored snapshot. Migrates the previously scattered inline listeners (palette, command center, new session, sidebar, theme) into the registry, and adds profile switch/cycle/create + default-profile hotkeys. |
||
|
|
e8c837c921 |
feat(desktop): surface every provider + models from hermes model in the GUI menus (#40563)
* feat(desktop): surface every provider + models from `hermes model` in the GUI The desktop GUI's model/provider choices were starved relative to the `hermes model` CLI. Onboarding listed ~8 providers, Settings → Model only showed authenticated ones, because the global `/api/model/options` endpoint called build_models_payload() without the full-universe flags the TUI's model.options JSON-RPC already used. - web_server.py: `/api/model/options` now passes include_unconfigured + picker_hints + canonical_order (matching the TUI handler), so every GUI surface fed by it sees all 37 canonical providers with auth hints. - Settings → Model: provider dropdown lists every provider; picking an unconfigured api_key provider shows an inline 'paste key → Activate' flow (auto-selects the recommended default); OAuth/external route to onboarding. - Onboarding: the API-key form is now driven by the full provider catalog (curated five first, then the rest), not a hand-maintained list of five. - types/hermes.ts: ModelOptionProvider gains authenticated/auth_type/key_env. - Tests: model-settings covers the full-universe list + inline activation; fixed a pre-existing stale assertion (nous / hermes-4 was never rendered). * feat(desktop): /model in GUI chat opens the model picker instead of a dead-end notice Typing /model in a desktop chat session printed "/model uses the desktop model picker instead of a slash command" and did nothing — it never opened the picker. (The slash worker can't render the prompt_toolkit modal /model opens in the CLI, so the desktop just showed the unavailable-notice.) - use-prompt-actions.ts: intercept /model client-side. No args → open the desktop model picker overlay (setModelPickerOpen) — the same full provider+model picker as the status-bar button. With args (/model <name> [--provider ...]) → run the switch directly via slash.exec so power users can still type it. - desktop-slash-commands.ts: export isModelPickerCommand() so the hook can detect picker-owned commands without duplicating the PICKER_OWNED_COMMANDS set. - Test: covers isModelPickerCommand for /model (+ args) vs non-picker commands. * fix(desktop): make onboarding provider lists scrollable + clean up card styling The full-catalog onboarding picker could overflow the modal with no way to scroll — the OAuth provider list and the api-key grid both grew past the viewport, hiding the key input and the bottom action row (overflow-hidden card, no scroll container). - Scope a `max-h-[60dvh] overflow-y-auto` region to just the provider list / api-key card grid; the "other providers" disclosure, key input, and action row stay pinned and reachable. - Inner `p-1` so card borders / focus rings aren't clipped by the scroll viewport. - Flatter card styling: drop the persistent border, the redundant selected-state checkmark, and the modal shadow — selection now reads from the ring alone (the muted "already configured" check stays). - Remove the " — set up" suffix from the Settings → Model provider dropdown; the inline setup flow already signals unconfigured providers. * fix(desktop): identify api-key onboarding cards by env var, not id Selecting "Google Gemini" also highlighted "Google AI Studio": the curated catalog and the backend-derived providers can collide on `id` (a provider slug can equal a curated id like `gemini`), so `option.id === o.id` matched two cards at once. Key selection (and the React key + snap-back effect) on `envKey` instead, which the catalog dedups and is therefore unique per card. --------- Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com> |
||
|
|
3a46262c7c |
Merge remote-tracking branch 'origin/main' into bb/remove-composer-message-shadows
# Conflicts: # apps/desktop/src/components/assistant-ui/tool-fallback.tsx |
||
|
|
9d31577590 |
Tighten conversation rhythm, flatten the tool list, and smooth streaming text
Conversation rhythm: - Single `--paragraph-gap` knob drives paragraph spacing both inside a markdown block and between consecutive prose parts, out-specifying Tailwind Typography's prose margins. Code cards carry the same gap themselves so it holds at any Streamdown nesting depth. - Two-tier vertical rhythm: `--turn-block-gap` separates scaffolding (tools / thinking) from the reply; `--tool-row-gap` keeps a tool run tight. - Drop the prose indent so prose, tools, todos, and thinking share one left edge. `---` renders as quiet spacing, not a heavy rule. Flat tool list: - Tools always render as a standalone-row stack, never a "Tool actions · N steps" group. assistant-ui slices the tool range unstably (interleaved live vs. reconstructed-consecutive when settled), so grouping reshuffled the whole turn the instant it settled. Flat rows are pixel-identical either way. - Inline approvals can no longer be buried in a collapsed group body. - Remove the now-dead grouping helpers from tool-fallback-model. Empty thinking: - Suppress reasoning disclosures with no visible text (encrypted / spinner- coerced reasoning) instead of leaving an empty "Thinking" header. - Tail stall indicator returns "thinking" when a running turn goes quiet. Streaming cadence: - Smooth character-reveal decouples visible cadence from bursty arrival. - Flush queued text deltas before applying tool events so a tool row can't jump ahead of its preceding text. - Disable Nagle on the GUI WebSocket so per-token frames aren't coalesced. Polish: clarify/patch/vision_analyze tool meta, queue-panel + diff-lines spacing, sticky human bubble expands on focus (not hover). |
||
|
|
fbd423b94d |
feat(desktop): localize desktop chrome
Co-authored-by: Kiro 有点Yes <246816394+sdyckjq-lab@users.noreply.github.com> |
||
|
|
812dc6957e | Add searchable language picker | ||
|
|
b1b89f843e | Refactor desktop i18n field copy into nested structures | ||
|
|
f18a9dbefc | feat: Add desktop language switching for Japanese and Traditional Chinese | ||
|
|
6bbc5eefa0 |
Fix clarify icon alignment and spurious error-red on non-zero exit
- clarify-tool: top-align the help icon (items-start + mt-px) so it sits beside the first line of a multi-line question instead of floating centered against the whole block. - tool-fallback: a non-zero exit code alone no longer paints the whole terminal/execute_code card red. grep no-match, diff differences, and piped commands routinely exit non-zero while producing useful output; only flag an error when the command produced no output. Explicit error signals (error field, success=false, status=error, isError) still go red. - Add regression tests covering the exit-code -> status matrix. |
||
|
|
40386f33ec |
Remove drop shadows from composer and user message bubbles
Strip shadow-composer (and its focus/open-state variants) from the composer surface, composer fallback surface, and the shared user-bubble base class. Also drop the !important box-shadow override on [data-slot=composer-surface] that re-applied the shadow regardless of the utility class, so the flatter look actually takes effect. |
||
|
|
7cceead273 |
fix(desktop): render steer note as a codicon, not an emoji
The inline steer note used a ⏩ emoji. Emit a structured `steer:<text>`
system note and render it in SystemMessage as a codicon (compass) row —
same style as slash-status output. No emoji in the transcript.
|
||
|
|
0cbcc75935 |
fix(desktop): reliable composer message queue (#40221)
* fix(desktop): make composer message queue reliable The queue felt 'dumb' because of three real bugs: 1. Drained-after-interrupt sends went silent. cancelRun sets interrupted:true and nothing reset it; submitPromptText's optimistic seed preserved it, and the message stream drops every delta while interrupted. So Send-now-while-busy and any interrupt+drain submitted the next turn into a muted session. Fix: a fresh submit is a new turn — seed interrupted:false. 2. Back-to-back queue drains stalled. The drain fires on the busy->false settle edge, but busyRef (synced from the busy store by a separate effect) can still read true on that same edge, so the drained send hit the busy guard, returned false, and the entry was never removed. Fix: fromQueue sends bypass the busyRef guard (the queue drain lock serializes them); the user path keeps the guard. 3. Double-enter-to-interrupt killed single non-queue turns. The hidden 450ms timer meant a natural double-tap after sending stopped the agent. Fix: empty Enter while busy is a no-op; interrupting is explicit — Stop button or Esc. Also: clean stop (no [interrupted] marker), Send-now works while busy (promote + interrupt + auto-drain), settle on the interrupted completion path. Adds regression tests and unblocks the prompt-actions suite by completing its stale @/hermes mock. * fix(desktop): float the queue panel as an overlay so the chat doesn't resize The queue list rendered in-flow inside the composer root, so its height fed --composer-measured-height (the composer rect drives the thread's bottom padding + last-message clearance). Queuing a message grew that rect and the whole chat visibly resized. Anchor the panel out of flow above the composer (absolute bottom-full, capped at 40vh with internal scroll). It no longer contributes to the measured height, so the thread layout stays put and the list overlays the (already faded) chat. Still collapsible via the panel's own disclosure header. * fix(desktop): queue panel collapsed by default + shared border with composer - Default the queue disclosure to collapsed (compact 'N queued' pill) instead of expanded. - Drop the gap and merge the panel into the composer: square bottom corners, no bottom border/radius, and overlap down by the Root's pt-2 (-mb-2) so the panel's borderless bottom lands on the composer surface's top border — one continuous bordered shape. * style(desktop): tighten queue panel padding * style(desktop): trim queue-ux comments to house style * style(desktop): drop 'Cursor' references from comments |
||
|
|
1d9c3ebae0 | feat(desktop): persist i18n language in config | ||
|
|
1a3e608524 |
feat(desktop): per-profile remote gateway hosts (#39778)
* feat(desktop): per-profile remote gateway hosts Profile switching silently failed whenever the desktop was connected to a remote backend: the rail routed non-active profiles to a local pool backend, but spawnPoolBackend hard-threw "Profiles are unavailable when connected to a remote Hermes backend", and the renderer swallowed the error into an infinite reconnect backoff while still marking the profile active. Remote was also a single app-global setting, so there was no way to give a profile its own host. Add per-profile remote hosts so each profile can point at its own backend: - connection.json gains a validated `profiles` map; profileRemoteOverride() (pure, unit-tested) selects an explicit per-profile remote. - resolveRemoteBackend(profile) precedence: per-profile override → env override → global remote → local spawn. spawnPoolBackend now connects to a profile's remote (no local child) instead of throwing; startHermes resolves the primary profile's remote. - coerce/sanitize connection config are scope-aware (global vs named profile) and preserve each other's entries; IPC get/save/apply/test thread an optional profile. Per-profile apply drops only that profile's pool backend. - Settings → Gateway adds an "Applies to" scope selector reusing the existing URL/token/OAuth/test UX per profile. Tests: connection-config pure suite (+6) and desktop platform suite pass; tsc/eslint/vitest clean. * refactor(desktop): DRY per-profile remote helpers Share connectionScopeKey + normAuthMode from connection-config.cjs (drop the main.cjs copy), collapse the scope/auth ternaries, route the env remote through buildRemoteConnection, and fold the duplicated remote-block validation into buildRemoteBlock. No behavior change; pure suite + live E2E still green. |
||
|
|
9cc47b20cb |
feat(desktop): add 'choose provider later' skip to first-run onboarding (#39483)
The first-run provider picker was a hard gate — the only way out was connecting a provider. Add an 'I'll choose a provider later' link that dismisses the overlay and persists the skip to localStorage so it never re-nags on subsequent launches. Users connect a provider any time from Settings -> Providers (manual onboarding already bypasses the skip gate). - onboarding.ts: firstRunSkipped state seeded from localStorage (hermes-onboarding-skipped-v1) + dismissFirstRunOnboarding() action; completeDesktopOnboarding clears the flag once a provider connects. - overlay: skip gate (firstRunSkipped && !manual returns null); ChooseLaterLink rendered in both the OAuth picker footer and the API-key fallback, first-run only. - tests: skip persists + hidden in manual mode; full-state fixtures updated. |
||
|
|
4891f9ae78 |
feat(desktop): concurrent multi-profile gateway sockets
Keep one persistent socket per profile with live work instead of closing the single socket on every profile swap, so background sessions across profiles keep streaming at once. A gateway registry owns the primary (window) socket plus lazy secondaries (own backoff/reconnect); all feed the same session-keyed event handler. Secondaries are pruned to profiles with a working/needs-input session, the keepalive pings every open backend, and LRU eviction spares freshly-touched backends so the soft cap can't abort a running agent. Approval/sudo/secret prompts are parked per-session (surfaced via the needs-input badge) so a background turn can block without hijacking the foreground. Single-profile users only ever have the primary, so their path is unchanged. |
||
|
|
89baf02919 |
Merge origin/main into bb/desktop-profile-support
Resolve conflicts in desktop settings/cron/messaging/sidebar: adopt main's ListRow + actions-menu refactors for credential rows; keep our profileColor import on the sidebar. Drop the now-orphaned Tip-based helpers. |
||
|
|
1b01fa3acf |
feat(desktop): long-press a rail profile to pick its color
Hold (~450ms) a profile square — or right-click → Color… — to open a shadcn Popover of swatches and override its rail color, with Auto to fall back to the deterministic hue. The hold timer rides alongside the dnd pointer listener (a real drag cancels it, the trailing click is suppressed), so reorder/select/recolor stay distinct gestures. Overrides persist in localStorage ($profileColors), resolved via resolveProfileColor (override wins, else the name-hashed hue). Cosmetic and gated on the multi-profile rail, so single-profile users are unaffected. Adds a reusable ui/popover.tsx (radix-ui umbrella). |
||
|
|
ea44011d15 |
fix(desktop): prevent thinking block from closing mid-streaming
When reasoning text grows during streaming, new parts can be appended beyond endIndex. The pending check used slice(startIndex, endIndex) which excluded these new parts — if the original part completed, the block would close while new reasoning was still streaming. Fix: remove the endIndex cap from slice() so all parts from startIndex onward are checked. During non-streaming, the array is stable and all parts are within range anyway. |
||
|
|
9dbd3c57d7 |
feat(desktop): drag sessions into chat as @session links + spawn loader
Drag a sidebar session into the composer to drop an @session:<profile>/<id> chip the agent resolves via session_search. New READ shape dumps a whole session by id (head+tail when large); a `profile` param reads another profile's DB read-only, and a cross-profile locate scan resolves bare ids when the model drops the owning profile from the link. Also: ASCII "waking up <profile>" overlay during lazy gateway swaps, global haptic rate-limit to kill the reconnect-storm "clickity" buzz, and reauth toasts surfaced once per disconnect instead of every backoff tick. |
||
|
|
2c98dc0a96 |
fix(desktop): offer remote sign-in on a gated-gateway boot failure (#39402)
When a remote gateway with username/password (or OAuth) auth restarts, its session cookie lapses and Desktop boots into the recovery overlay with a session-expired error. That overlay only exposed local-recovery actions — Retry (resets the local bootstrap latch) and Repair (re-runs the installer) — neither of which can re-establish a remote session, so the user is stuck in a no-op Retry loop with no way to sign in again. The overlay now detects a remote-reauth boot failure from the saved connection config (remote + gated + not currently connected + has a URL) and surfaces a primary 'Sign in to remote gateway' button that opens the gateway login window (the username/password form for a basic gateway, the OAuth redirect otherwise) and reloads on success. Button copy is driven by a best-effort provider probe, matching the gateway-settings page. Detection and copy logic live in a pure helper module with unit coverage. |
||
|
|
a40e20e136 |
feat(desktop): profile rail rename/delete + context-switch polish
- right-click a profile square to rename or delete it, via shared self-contained dialogs (also reused by the profiles page) - switching or creating a profile now resets to a fresh new-session draft so the prior session doesn't stay sticky across contexts - deleting the profile you're currently in falls back to default instead of stranding the gateway on a dead profile - shared ConfirmDialog: Enter/Space confirm from anywhere in the dialog; profile-delete and cron-delete both route through it |
||
|
|
5df732a355 |
feat(desktop): quick-create profile from rail + pin rail on empty sidebar
- Add a "+" in the profile rail that opens a self-contained CreateProfileDialog (name + clone toggle + optional SOUL.md); extract it and ActionStatus from the profiles view so both surfaces share one flow. - Keep the profile rail pinned to the bottom when a profile has no sessions by rendering a flex-1 spacer (previously the rail floated up to the nav). |
||
|
|
b94b3622b5 |
feat(desktop): per-session profile switching + cross-profile sessions
Add first-class profile support to the desktop app without app reloads. - Swap the single live gateway onto a session's profile lazily (spawned on demand by the Electron backend pool), so one backend serves the active profile and others stay cold — no OOM with many profiles. - Aggregate sessions across profiles by reading each profile's state.db read-only; unified "All profiles" view groups sessions per profile with per-profile pagination, while the default view stays scoped to one profile. - Add an Arc-style profile rail at the sidebar foot: a default<->all toggle pinned left, colored named-profile squares scrolling between, Manage pinned right. Profile identity is a deterministic per-name color. - Route profile-scoped REST (config/env/skills/tools/model) to the active gateway profile and invalidate React Query caches on swap. Single-profile users never trigger a swap, so their path is unchanged. Backend: - web_server: profile-aware active/list endpoints + per-profile session totals; hermes_state: session_count(exclude_children); main.py: honor --profile over HERMES_HOME env for pooled backends. UI primitives: - Add a position-aware Tip tooltip (instant, themed) as a drop-in for native title=, and strip redundant tooltips from self-descriptive chrome. |
||
|
|
e003c53b06 |
chore(desktop): zero eslint/typecheck debt + prettier pass (#39100)
- eslint --fix across src/ and electron/ (unused imports, import/prop sort, padding) - flatten empty catch blocks in electron CJS; drop unused applyUpdatesPosixInApp arg - add setMutableRef helper for imperative ref writes (react-compiler clean) - move sidebar cookie persistence into an effect; extract scrollElementToBottom helper |
||
|
|
b1b0f4b668 |
fix(desktop): surface command approval even when its tool is in a collapsed group (#38829)
The desktop command-approval ApprovalBar renders inline inside ToolEntry, which lives inside ToolGroupSlot. When 2+ tools group, the group body is hidden until expanded, so an approval raised by a pending terminal/ execute_code call was buried behind "Tool actions · N steps" and required manual expansion to act on (sudo/secret were unaffected — they use modal overlays). ToolGroupSlot now subscribes to $approvalRequest and force-opens its body while an approval targeting one of its pending approval-eligible tools is in flight, so the inline controls surface with nothing expanded. The group reverts to the user's stored collapse state once the approval resolves. |
||
|
|
9cbc37e25b |
feat(desktop): dedicated Providers settings + polished Accounts/API-keys UX (#38551)
* feat(desktop): dedicated Providers settings with Accounts/API-keys subnav Rework provider configuration in the desktop app into its own Providers page that mirrors the first-run onboarding picker, instead of burying provider keys in the generic Tools & Keys list. - Add a Providers settings page (providers-settings.tsx) reusing the onboarding picker cards/ApiKeyForm so the two surfaces stay identical - Add a sidebar subnav (Accounts vs API keys) backed by a deep-linkable `pview` URL param; nested OverlayNavItem variant for a lighter active state so children don't compete with the parent item - Scope provider search to the active sub-view in its native card format (no more accordion fallback); collapse the API-key grid to the top providers behind a "Show all" toggle to cut scrolling - Launch real in-app OAuth from settings via startManualProviderOAuth; fix the misleading red "reason" banner that showed during an active connect (neutral style, hidden during a flow, omitted for direct per-provider launches) - Expand PROVIDER_GROUPS and add longest-prefix matching so providers like xAI/Ollama group correctly instead of landing under "Other" - Drop redundant messaging API keys from Tools & Keys (channel_managed) Co-authored-by: Cursor <cursoragent@cursor.com> * feat(desktop): Cursor-style provider key list with inline inputs Replace the card-grid API-key form on the Providers page with a per-provider list (mirrors Cursor's API keys section): - One row per vendor with its primary key input inline; rows with extra vars (base URL, region, alt tokens) expand to reveal those on focus - Set keys show their redacted value as the placeholder; Save appears on edit, Remove on a set key - Hide redundant alias key fields (e.g. ANTHROPIC_TOKEN vs ANTHROPIC_API_KEY) unless already set, and label set aliases by env var name so they're unambiguous - Smaller mono input text + compact height Co-authored-by: Cursor <cursoragent@cursor.com> * style(desktop): flatten providers settings UI chrome Tighten the providers settings surface to match the newer desktop style: remove extra card rails/borders in API-key rows, reduce visual noise in the providers subnav, replace bespoke link-like controls with shared text-button variants, and improve key input readability. * feat(desktop): rework providers settings UI - Flatten the shared OAuth picker rows (accounts + onboarding): drop the rounded-2xl/border cards for flat hover-bg rows; Nous hero keeps a subtle tint plus an animated blue→purple arc border. - Key fields collapse to a single input: a set key reads read-only (redacted) and edits in place on focus/click — no Replace/Cancel chrome. Save on type, Esc cancels (without closing the overlay), "Remove or esc to cancel" hint. - Non-key overrides render boxless, content-sized (field-sizing) and right-anchored; advanced fields align under the primary key column. - Add `xs` control size; size fields via padding (no fixed heights). - Cards expand on key-input focus; chevron shows on hover/expanded; expanded state uses a ring + softer bg tier so hover ≠ focus. - Relocate "Get a key" to the bottom-right of the expanded panel; drop the redundant provider description. - Cmd+K: add Providers (accounts) and Provider API keys deep-links. * fix(desktop): flatten provider fields, drop input shadows, fix Cmd+K provider rank - KeyField: collapse to one stacked label-above-input form field (drop the bespoke `naked`/inline/column branches); empty advanced overrides fade until hover/focus/set - styles: kill the resting + focus drop shadow on shared input chrome so form inputs sit flat (composer keeps its own shadow) - Cmd+K: drop stray `providers` keyword from Skills & Tools so the Providers settings entry ranks first for "provider" * fix(desktop): nous portal arc blue → orange * fix(desktop): rank appearance above settings in Cmd+K --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com> |
||
|
|
3c163cb035 |
feat(desktop): background needs-input indicator, clarify redesign, Cmd+K palette & UI consistency pass (#38631)
* fix(desktop): surface background-session clarify prompts instead of hanging
clarify.request is a one-shot blocking event: the gateway turn blocks on
clarify.respond. The desktop handler dropped it for any non-focused session
(`if (!isActiveEvent) return`) and stored at most one request in a single
global atom, so a background session that asked a clarifying question hung
forever and re-focusing it could never recover (the event was already gone).
- store/clarify.ts: key pending requests by runtime session id; expose the
active session's request via a focus-scoped computed view (ClarifyTool is
unchanged). clearClarifyRequest takes an optional session id for targeted
clears, with a request-id fallback.
- use-message-stream.ts: park every session's clarify (drop the isActiveEvent
early return); toast when one lands for a background session since the row
otherwise just keeps spinning like normal work.
- clarify-tool.tsx: clear by session id so answering one chat can't wipe
another's pending request.
- store/clarify.test.ts: concurrent independence, focus-scoped view,
targeted/stale/fallback clears.
* feat(desktop): persistent needs-input indicator + icon button consolidation
Replace the background-clarify toast (expired on alt-tab, easy to miss) with a
persistent, glowing amber "needs input" dot on the session's sidebar row,
driven off a new ClientSessionState.needsInput flag mirrored into a
$attentionSessionIds store. The flag is set on clarify.request and cleared the
moment the turn resumes (tool.complete) or ends.
Also: redesign the clarify tool UI (borderless choices, pseudo-radio dots,
right-aligned checkmark, arc border, tighter padding), make Button the single
source of icon-button styling (4px radius, new icon-titlebar variant, titlebar
buttons rendered polymorphically via asChild, Codicons throughout), put the
file-tree refresh action first, and .trim() pasted composer text.
* style(desktop): padding-driven, square non-icon buttons
Default button sizing was vanilla-shadcn chunky (fixed h-9, 16px padding) and
inconsistent with the icon-button radius pass. Size text variants by
padding + line-height instead of fixed heights so they stay snug and scale
with content, and drop the radius on non-icon buttons (icon buttons keep the
shared 4px). Move the update-overlay CTAs off a hardcoded h-10 onto the
padding-based lg variant. Composer and the inline approval strip are untouched.
* style(desktop): shrink button scale, flush overlay sidebar, variant-ize stray buttons
- Buttons: smaller default font (14px -> 13px) and tighter padding-driven sizes
across every variant; the chunky shadcn scale read as oversized in a dense
desktop UI.
- Overlay split layout (settings / command center): the shared OverlayView top
padding left the card surface showing as a gap above the sidebar. Move the
titlebar clearance into each column so the sidebar background runs flush to
the card's top edge.
- Consolidate buttons that hardcoded size/radius/font onto the proper size
variants (tooltip-icon-button, overlay close, cron IconAction, SidebarTrigger,
gateway system button, session-row actions radius, title chip radius, release
notes link) so styling flows from variant props, not per-call overrides.
Composer and the inline approval strip are intentionally left as-is.
* style(desktop): 12px button text, drop sparkle decoration + redundant settings titles
- Button base font down to 12px (text-xs) for the dense desktop scale.
- Remove the decorative Sparkles glyph from the model "Apply" button (keep the
spinner while applying).
- Drop the page-level section titles that just restate the left nav ("Main
model", "Appearance", "MCP servers") — the sidebar already labels the pane.
Sub-section headings (Auxiliary models, LLM providers, etc.) stay.
* feat(desktop): add boxless `text` button variant; use for aux-model actions
New reusable `text` variant renders a button as inline label text (no
bg/border, muted -> foreground, underline-on-hover affordance). Emphasize the
actionable word by adding `font-semibold`/`underline` at the call site. Applied
to the auxiliary-model "Set to main" (plain), "Change" and "Reset all to main"
(bold + underlined) actions, replacing the boxed ghost/outline buttons.
* style(desktop): nudge button scale up + 2.5px radius on non-icon buttons
Bump default/sm vertical padding a step (the 12px pass read too small) and give
non-icon buttons a subtle 2.5px radius instead of square corners. Icon buttons
keep their 4px.
* style(desktop): unify Input/Textarea/SelectTrigger on shared controlVariants
Mirror the buttonVariants exercise for non-composer form controls: add a
single controlVariants source of truth (2.5px radius, 12px text,
padding-driven sizing, chrome via desktop-input-chrome) and consume it from
Input, Textarea, and SelectTrigger. Drop per-call radius/height/font
overrides that fought the shared look.
* style(desktop): flatten appearance settings — drop card-in-card sections
Remove the outer card chrome (border/bg/shadow/rounded) wrapping each
appearance section so they're flat headings + option grids instead of
boxes nested inside boxes, matching the other settings pages.
* style(desktop): de-box appearance options into flat rows + bare theme swatches
Color Mode and Tool Call Display become flat radio-style rows (no tile
border/fill, no inner icon box, no filled check badge — just a subtle active
bg and a check). Theme drops its outer card wrapper so only the preview
swatch shows, with a primary ring marking the active palette.
* style(desktop): primitive-level pointer cursor + borderless settings lists
Add a base-layer rule giving every interactive control (button, select,
menu item, switch, tab, summary) cursor:pointer, and strip the now-redundant
hardcoded cursor-pointer from those elements (plain clickable divs/labels
keep theirs). Remove the divide-y separators from settings list sections so
they breathe.
* style(desktop): Color Mode + Tool Call Display as one-row segmented controls
Replace the vertical option-row lists with a compact SegmentedControl
(grouped pill buttons on a single track), dropping the per-option
descriptions since the section subtitle already covers the context.
* style(desktop): drop redundant On/Off label next to boolean config switches
The switch already communicates state, so the text label was noise.
* style(desktop): add Switch xs size; move appearance controls inline-right
Add an xs size variant to the Switch primitive and use it for the provider
edit submenu toggles. In appearance settings, drop the redundant selection
Pills (the UI already shows the active choice), move the Color Mode and Tool
Call Display segmented controls into the section header's right side
(responsive: stacks under the heading on narrow widths), and shrink the
segmented control.
* feat(desktop): titlebar toggle to flip sidebar sides
Adds a top-left swap button (replacing the search icon) that mirrors the
layout: sessions sidebar ↔ file browser + preview rail. Persisted via
$panesFlipped. The left/right sidebar toggles, content inset, and pane
borders all follow the active side so the buttons stay accurate after a flip.
* feat(desktop): global Cmd+K palette + UI consistency overhaul
Builds on the clarify/needs-input work with a cross-cutting pass to make
the desktop surfaces feel like one app.
- Global Cmd+K command palette (cmdk): nav, settings deep-links, async
API-key / MCP-server / archived-session groups, reusable theme sub-page
(light/dark groups, stays open on pick), loop nav, fuzzy match. Replaces
per-page settings search.
- Shared SearchField: borderless, underline-on-focus, `field-sizing`
auto-width. Unifies sessions sidebar, pages, overlays, command center,
cron; drops bespoke OverlaySearchInput.
- Cron & Profiles converted to OverlayView; flat token-driven panels
(no card-in-card / divider borders) matching command center.
- `r` refresh hotkey via useRefreshHotkey; drop the visible refresh buttons.
- Button text/textStrong link variants applied across settings & views;
shared PAGE_INSET_X content gutters.
- Math/ascii loaders replace "Loading…" text placeholders; x-icon close
over text "Close"; cursor-pointer at the dropdown/select primitive level.
* style(desktop): tidy root error-boundary actions
Reload window → text link, Open logs pushed right (ml-auto), and the
error message box drops the oversized rounded-2xl for rounded-md.
* style(desktop): fix profiles sidebar — header + add-icon, drop text-link
The full-width `text` New-profile button drew an underline under the +
glyph on hover (text-decoration spans the icon). Replace with a proper
"PROFILES" section header + ghost add-icon button, matching the chat
sidebar's header/new-item pattern.
* style(desktop): kill focus rings globally
Tab/focus showed Tailwind's `focus-visible:ring-*` (a box-shadow) plus the
native outline. Drop both via an unlayered reset that nulls --tw-ring-*;
the composer / input soft-glow is untouched (those use direct box-shadows).
* style(desktop): shared Badge component; tidy profile metadata
Add a proper shadcn-style Badge (CVA tones, app radius — not a full pill)
and use it for the Default/.env tags instead of bespoke rounded-full spans.
Drop the oversized text-sm metadata values to text-xs.
* style(desktop): migrate bespoke pills to shared Badge; tidy cron/titlebar
- Sidebar toggles in the titlebar no longer carry an active highlight —
they're plain show/hide affordances now.
- Replace every bespoke rounded-full status pill (cron, messaging,
settings, skills) with the shared Badge (adds a `warn` tone). App radius,
one component.
- Cron row actions use Codicons (play/debug-pause/zap/edit/trash) to match
the rest of the chrome instead of stray lucide glyphs.
* style(desktop): drop active background on titlebar actions
Mute/haptics state reads from the icon glyph (and aria-pressed) — no
background highlight on any titlebar action.
* style(desktop): tighten error-boundary action gap
gap-4 → gap-2.5 between Try again / Reload window.
* style(desktop): hide search when there's nothing to search
Empty datasets no longer render a search field. Adds a `searchHidden` prop
to PageSearchShell (artifacts/skills/messaging) and gates cron + command
center sessions search on a non-empty list. The chat sidebar already did
this via showSessionSections.
* fix(desktop): composer wraps long text & expands at the real wrap point
Long unbroken input ran off horizontally and the stacked layout flipped
on a char-count guess (too early). Add wrap rules to the contentEditable
and drive expansion off the editor's actual rendered height via the
resize observer, so it stacks exactly when the text wraps to a 2nd line.
* feat(desktop): composer/intro polish + shared ErrorState
- Composer single-line row centers (was bottom-aligned); placeholder
randomizes per session (starter vs follow-up) without mid-stream flip.
- Drop chat header on brand-new sessions (dead label + border).
- ⌘N flashes its sidebar hint; ⌘. toggles the command center.
- Intro wordmark fills width (drop 8rem fit cap).
- Unify error states on a shared ErrorState component (boundary + updates).
* style(desktop): satisfy lint across PR-touched files
* refactor(desktop): DRY/elegance pass over PR-touched files
- Shared useDeepLinkHighlight hook collapses 3 near-identical settings
deep-link effects (keys/mcp); config kept inline (distinct bail-clear).
- command-center: table-driven SECTION_ICONS + single errorText helper.
- clarify-tool: OPTION_ROW_CLASS + RadioDot extracted from option rows.
- desktop-controller: merge Cmd+K / Cmd+. into one keydown handler.
- statusbar-controls: hoist shared action class.
- Misc: drop redundant cn()/cursor-pointer/dead fields; tidy switch.
* feat(desktop): Cmd+K jumps to sessions; drop API-key entries
Add active sessions to the palette (fuzzy jump-to-chat), remove the
low-value per-API-key entries, and move the lazy palette sources
(config/sessions/archived) to react-query instead of hand-rolled
useState + effect fetching. Hoist the shared nav helper.
|
||
|
|
bc9e33d66b |
refactor(desktop): DRY/elegance pass over PR-touched files
- Shared useDeepLinkHighlight hook collapses 3 near-identical settings deep-link effects (keys/mcp); config kept inline (distinct bail-clear). - command-center: table-driven SECTION_ICONS + single errorText helper. - clarify-tool: OPTION_ROW_CLASS + RadioDot extracted from option rows. - desktop-controller: merge Cmd+K / Cmd+. into one keydown handler. - statusbar-controls: hoist shared action class. - Misc: drop redundant cn()/cursor-pointer/dead fields; tidy switch. |
||
|
|
38acced687 | style(desktop): satisfy lint across PR-touched files | ||
|
|
5bb7156949 |
feat(desktop): composer/intro polish + shared ErrorState
- Composer single-line row centers (was bottom-aligned); placeholder randomizes per session (starter vs follow-up) without mid-stream flip. - Drop chat header on brand-new sessions (dead label + border). - ⌘N flashes its sidebar hint; ⌘. toggles the command center. - Intro wordmark fills width (drop 8rem fit cap). - Unify error states on a shared ErrorState component (boundary + updates). |
||
|
|
9e02b18828 |
style(desktop): tighten error-boundary action gap
gap-4 → gap-2.5 between Try again / Reload window. |
||
|
|
e026fd88cd |
style(desktop): migrate bespoke pills to shared Badge; tidy cron/titlebar
- Sidebar toggles in the titlebar no longer carry an active highlight — they're plain show/hide affordances now. - Replace every bespoke rounded-full status pill (cron, messaging, settings, skills) with the shared Badge (adds a `warn` tone). App radius, one component. - Cron row actions use Codicons (play/debug-pause/zap/edit/trash) to match the rest of the chrome instead of stray lucide glyphs. |
||
|
|
fd88d527af |
style(desktop): shared Badge component; tidy profile metadata
Add a proper shadcn-style Badge (CVA tones, app radius — not a full pill) and use it for the Default/.env tags instead of bespoke rounded-full spans. Drop the oversized text-sm metadata values to text-xs. |
||
|
|
311e80809f |
style(desktop): tidy root error-boundary actions
Reload window → text link, Open logs pushed right (ml-auto), and the error message box drops the oversized rounded-2xl for rounded-md. |
||
|
|
ac9de2e80c |
feat(desktop): global Cmd+K palette + UI consistency overhaul
Builds on the clarify/needs-input work with a cross-cutting pass to make the desktop surfaces feel like one app. - Global Cmd+K command palette (cmdk): nav, settings deep-links, async API-key / MCP-server / archived-session groups, reusable theme sub-page (light/dark groups, stays open on pick), loop nav, fuzzy match. Replaces per-page settings search. - Shared SearchField: borderless, underline-on-focus, `field-sizing` auto-width. Unifies sessions sidebar, pages, overlays, command center, cron; drops bespoke OverlaySearchInput. - Cron & Profiles converted to OverlayView; flat token-driven panels (no card-in-card / divider borders) matching command center. - `r` refresh hotkey via useRefreshHotkey; drop the visible refresh buttons. - Button text/textStrong link variants applied across settings & views; shared PAGE_INSET_X content gutters. - Math/ascii loaders replace "Loading…" text placeholders; x-icon close over text "Close"; cursor-pointer at the dropdown/select primitive level. |
||
|
|
40420a619b |
fix(desktop): attachments on Enter, IME composition, scroll, fetchJson resets (salvage #38502) (#38677)
* fix(desktop): critical fixes — attachments, IME composition, scroll, fetchJson
DC2: Pass attachments to onSubmit() on direct Enter submit and call
clearComposerAttachments(). Previously attachments were silently
dropped — only text was sent while attachment pills remained visible.
DH1: Add 'open' to ThinkingDisclosure ResizeObserver effect deps.
When the disclosure toggles, refs point to new DOM but the observer
wasn't reattached, breaking live-scroll preview after expand/collapse
and leaking detached DOM nodes.
DH3+DH4: Add composition tracking via composingRef (set by
compositionstart/compositionend). Guards handleEditorInput (skip
preedit state writes), handleEditorKeyDown (prefer composingRef over
unreliable isComposing), and form onSubmit (prevent IME Enter from
triggering submission). Fixes IME Enter message splitting and preedit
text leaking into app state on CJK input.
DH6: Add res.on('error', reject) to fetchJson response stream.
Without this, a TCP reset mid-transfer left the promise hanging forever,
freezing the desktop UI.
All TypeScript compiles cleanly.
* chore: add copii.list@gmail.com to AUTHOR_MAP (stremtec)
* fix(desktop): prevent scroll snap-back during streaming, atomic config writes
DH2: Defer pinToBottom() in useLayoutEffect to rAF so that browser
scroll/wheel events from the current frame are processed first.
Previously an immediate pinToBottom() could snap the viewport back
to bottom against the user's trackpad scroll-up intent during
streaming — the wheel event hadn't fired yet so stickyBottomRef was
still true.
DH7: Add writeFileAtomic() helper (write to .tmp then rename) and
use it in writeDesktopConnectionConfig, writeDesktopUpdateConfig,
and writeBootstrapMarker. Prevents partial writes on crash/power
loss that would corrupt JSON config files, requiring manual repair.
* fix(desktop): guard nativeTheme listener from duplicates, invalidate connection config cache
DM9: Guard nativeTheme.on('updated') with a one-shot flag so that
multiple createWindow() calls (e.g. macOS activate after all windows
closed) don't accumulate duplicate listeners on the process-wide
singleton.
DM3: Add mtime-based cache invalidation to readDesktopConnectionConfig.
Previously the cache was populated once and never invalidated — if an
external tool modified connection.json, the desktop ignored the change
until restart. Now re-reads when the file's mtime differs.
* fix(desktop): widen fetchJson res.on('error') to sibling fetch + sort JSX props
Follow-up to salvaged #38502:
- resourceBufferFromUrl had the same mid-stream-reset hang class as
fetchJson (req.on('error') present, res.on('error') missing). Add the
response-stream error handler so a TCP reset during body read rejects
instead of leaving the promise unsettled.
- Sort the new onComposition* JSX props to satisfy perfectionist/sort-jsx-props
(was an introduced eslint error in the composer).
---------
Co-authored-by: asill-livestream <copii.list@gmail.com>
|
||
|
|
75e29f97ee |
style(desktop): add Switch xs size; move appearance controls inline-right
Add an xs size variant to the Switch primitive and use it for the provider edit submenu toggles. In appearance settings, drop the redundant selection Pills (the UI already shows the active choice), move the Color Mode and Tool Call Display segmented controls into the section header's right side (responsive: stacks under the heading on narrow widths), and shrink the segmented control. |