From f8fe5d3a89fd48bcd7b5791532e88d0f259ca261 Mon Sep 17 00:00:00 2001 From: Kiet <31864905+Kitenite@users.noreply.github.com> Date: Sat, 25 Apr 2026 12:43:03 -0700 Subject: [PATCH 1/3] fix(desktop): drop branch row from v2 sidebar workspace item (#3733) Workspace name already falls back to branch when missing, so the dedicated branch row was redundant. Slightly bumps row padding for breathing room. --- .../DashboardSidebarExpandedWorkspaceRow.tsx | 142 +++++++++--------- 1 file changed, 68 insertions(+), 74 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx index 77496cadfc0..c2cddefe1eb 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/components/DashboardSidebarWorkspaceItem/components/DashboardSidebarExpandedWorkspaceRow/DashboardSidebarExpandedWorkspaceRow.tsx @@ -122,7 +122,7 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< "relative flex w-full items-center pl-3 pr-2 text-left text-sm", onClick && "cursor-pointer hover:bg-muted/50", "group", - "py-1.5", + "py-2", isActive && "bg-muted", className, )} @@ -211,86 +211,80 @@ export const DashboardSidebarExpandedWorkspaceRow = forwardRef< -
-
- {isRenaming ? ( - - ) : ( +
+ {isRenaming ? ( + + ) : ( + + {name || branch} + + )} + +
+ {creationStatusText ? ( - {name || branch} + {creationStatusText} - )} - -
- {creationStatusText ? ( - + {diffStats && + (diffStats.additions > 0 || diffStats.deletions > 0) && ( + )} - > - {creationStatusText} - - ) : ( - <> - {diffStats && - (diffStats.additions > 0 || diffStats.deletions > 0) && ( - + {shortcutLabel && ( + + {shortcutLabel} + + )} + + + + + + - )} -
- {shortcutLabel && ( - - {shortcutLabel} - - )} - - - - - - - - -
- - )} -
- - - {branch} - + + +
+ + )}
From 2640352cdeecfc946c5b782604813b4b0d9eefce Mon Sep 17 00:00:00 2001 From: Kiet <31864905+Kitenite@users.noreply.github.com> Date: Sat, 25 Apr 2026 13:13:27 -0700 Subject: [PATCH 2/3] feat(desktop): add fade-edge mask utilities (#3735) * feat(desktop): add fade-edge mask utilities Add composable `.fade-edge-{t,r,b,l}` utility classes that fade out one or more edges of an element via a linear-gradient mask. Useful for indicating horizontal/vertical scroll affordance. Lives in `apps/desktop/src/renderer/styles/fade-edge.css` (single import in `globals.css`) so the file can be promoted to `packages/ui` once a second consumer needs it. * style: blank line before mask-image declaration --- apps/desktop/src/renderer/globals.css | 1 + .../desktop/src/renderer/styles/fade-edge.css | 70 +++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 apps/desktop/src/renderer/styles/fade-edge.css diff --git a/apps/desktop/src/renderer/globals.css b/apps/desktop/src/renderer/globals.css index a541e0d7b31..c841b9c1b16 100644 --- a/apps/desktop/src/renderer/globals.css +++ b/apps/desktop/src/renderer/globals.css @@ -1,6 +1,7 @@ @import "tailwindcss"; @import "tw-animate-css"; @import "streamdown/styles.css"; +@import "./styles/fade-edge.css"; @source "./**/*.{ts,tsx}"; @source "../../../../packages/ui/src/**/*.{ts,tsx}"; diff --git a/apps/desktop/src/renderer/styles/fade-edge.css b/apps/desktop/src/renderer/styles/fade-edge.css new file mode 100644 index 00000000000..801718a17d8 --- /dev/null +++ b/apps/desktop/src/renderer/styles/fade-edge.css @@ -0,0 +1,70 @@ +/** + * Edge-fade mask utilities. + * + * Fades out one or more edges of an element via a linear-gradient mask. + * Composable: `class="fade-edge-r fade-edge-b"` fades both edges. + * Override the fade size per element with `[--fade-edge-size:2rem]`. + * + * NOTE: This is a generic, app-agnostic utility. When a second consumer + * outside `apps/desktop` needs it, promote this file to + * `packages/ui/src/styles/fade-edge.css` and update consumers' imports. + */ + +@layer utilities { + .fade-edge-t, + .fade-edge-r, + .fade-edge-b, + .fade-edge-l { + --fade-edge-size: 1.5rem; + --fade-edge-t-size: 0px; + --fade-edge-r-size: 0px; + --fade-edge-b-size: 0px; + --fade-edge-l-size: 0px; + + mask-image: + linear-gradient( + to bottom, + transparent, + black var(--fade-edge-t-size), + black calc(100% - var(--fade-edge-b-size)), + transparent + ), + linear-gradient( + to right, + transparent, + black var(--fade-edge-l-size), + black calc(100% - var(--fade-edge-r-size)), + transparent + ); + -webkit-mask-image: + linear-gradient( + to bottom, + transparent, + black var(--fade-edge-t-size), + black calc(100% - var(--fade-edge-b-size)), + transparent + ), + linear-gradient( + to right, + transparent, + black var(--fade-edge-l-size), + black calc(100% - var(--fade-edge-r-size)), + transparent + ); + mask-composite: intersect; + -webkit-mask-composite: source-in; + } + + .fade-edge-t { + --fade-edge-t-size: var(--fade-edge-size); + } + .fade-edge-r { + --fade-edge-r-size: var(--fade-edge-size); + } + .fade-edge-b { + --fade-edge-b-size: var(--fade-edge-size); + } + .fade-edge-l { + --fade-edge-l-size: var(--fade-edge-size); + } +} From ba0bacac9acf3324e65647052618943d667b5314 Mon Sep 17 00:00:00 2001 From: Kiet <31864905+Kitenite@users.noreply.github.com> Date: Sat, 25 Apr 2026 13:17:29 -0700 Subject: [PATCH 3/3] fix v2 terminal osc links (#3736) --- .../terminal/terminal-link-manager.test.ts | 105 ++++++++++++++++++ .../lib/terminal/terminal-link-manager.ts | 27 ++++- 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 apps/desktop/src/renderer/lib/terminal/terminal-link-manager.test.ts diff --git a/apps/desktop/src/renderer/lib/terminal/terminal-link-manager.test.ts b/apps/desktop/src/renderer/lib/terminal/terminal-link-manager.test.ts new file mode 100644 index 00000000000..950f51793ea --- /dev/null +++ b/apps/desktop/src/renderer/lib/terminal/terminal-link-manager.test.ts @@ -0,0 +1,105 @@ +import { describe, expect, it, mock } from "bun:test"; +import type { ILinkProvider, Terminal as XTerm } from "@xterm/xterm"; +import { TerminalLinkManager } from "./terminal-link-manager"; + +function createMockTerminal() { + const registeredProviders: ILinkProvider[] = []; + const disposedProviders: ILinkProvider[] = []; + const terminal = { + options: { + linkHandler: null, + }, + registerLinkProvider: (provider: ILinkProvider) => { + registeredProviders.push(provider); + return { + dispose: () => { + disposedProviders.push(provider); + }, + }; + }, + buffer: { + active: { + getLine: () => null, + }, + }, + cols: 80, + } as unknown as XTerm; + + return { terminal, registeredProviders, disposedProviders }; +} + +describe("TerminalLinkManager", () => { + it("routes OSC 8 hyperlinks through the terminal URL handler", () => { + const { terminal } = createMockTerminal(); + const manager = new TerminalLinkManager(terminal); + const onUrlClick = mock(); + const onLinkHover = mock(); + const onLinkLeave = mock(); + + manager.setHandlers({ + stat: async () => null, + onUrlClick, + onLinkHover, + onLinkLeave, + }); + + const linkHandler = terminal.options.linkHandler; + expect(linkHandler).toBeTruthy(); + expect(linkHandler?.allowNonHttpProtocols).toBe(false); + + const event = {} as MouseEvent; + linkHandler?.activate(event, "https://example.com", { + start: { x: 1, y: 1 }, + end: { x: 20, y: 1 }, + }); + linkHandler?.hover?.(event, "https://example.com", { + start: { x: 1, y: 1 }, + end: { x: 20, y: 1 }, + }); + linkHandler?.leave?.(event, "https://example.com", { + start: { x: 1, y: 1 }, + end: { x: 20, y: 1 }, + }); + + expect(onUrlClick).toHaveBeenCalledWith(event, "https://example.com"); + expect(onLinkHover).toHaveBeenCalledWith(event, { kind: "url" }); + expect(onLinkLeave).toHaveBeenCalled(); + }); + + it("clears only the OSC link handler it installed", () => { + const { terminal, disposedProviders } = createMockTerminal(); + const manager = new TerminalLinkManager(terminal); + + manager.setHandlers({ + stat: async () => null, + onUrlClick: mock(), + }); + + const installedHandler = terminal.options.linkHandler; + expect(installedHandler).toBeTruthy(); + + manager.dispose(); + + expect(terminal.options.linkHandler).toBeNull(); + expect(disposedProviders.length).toBe(2); + }); + + it("does not clear a link handler installed by another owner", () => { + const { terminal } = createMockTerminal(); + const manager = new TerminalLinkManager(terminal); + + manager.setHandlers({ + stat: async () => null, + onUrlClick: mock(), + }); + + const replacementHandler = { + activate: mock(), + }; + terminal.options.linkHandler = replacementHandler; + + manager.dispose(); + + expect(terminal.options.linkHandler).toBe(replacementHandler); + }); +}); diff --git a/apps/desktop/src/renderer/lib/terminal/terminal-link-manager.ts b/apps/desktop/src/renderer/lib/terminal/terminal-link-manager.ts index ea1ca56fb91..07d265713d8 100644 --- a/apps/desktop/src/renderer/lib/terminal/terminal-link-manager.ts +++ b/apps/desktop/src/renderer/lib/terminal/terminal-link-manager.ts @@ -7,7 +7,7 @@ * resolver caching, and priority ordering. *--------------------------------------------------------------------------------------------*/ -import type { Terminal as XTerm } from "@xterm/xterm"; +import type { ILinkHandler, Terminal as XTerm } from "@xterm/xterm"; import { UrlLinkProvider } from "../../screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/link-providers"; import type { DetectedLink } from "./links"; import { @@ -57,6 +57,7 @@ export class TerminalLinkManager { private _disposables: LinkProviderDisposable[] = []; private _resolver: TerminalLinkResolver | null = null; private _handlers: TerminalLinkHandlers | null = null; + private _oscLinkHandler: ILinkHandler | null = null; constructor(private readonly _terminal: XTerm) {} @@ -83,11 +84,19 @@ export class TerminalLinkManager { dispose(): void { for (const d of this._disposables) d.dispose(); this._disposables = []; + this._clearOscLinkHandler(); this._resolver?.clearCache(); this._resolver = null; this._handlers = null; } + private _clearOscLinkHandler(): void { + if (this._terminal.options.linkHandler === this._oscLinkHandler) { + this._terminal.options.linkHandler = null; + } + this._oscLinkHandler = null; + } + private _register(): void { const handlers = this._handlers; if (!handlers?.stat) return; @@ -95,6 +104,7 @@ export class TerminalLinkManager { // Dispose old providers to prevent duplicates for (const d of this._disposables) d.dispose(); this._disposables = []; + this._clearOscLinkHandler(); // Reuse resolver to preserve stat cache across re-registrations. if (!this._resolver) { @@ -135,6 +145,21 @@ export class TerminalLinkManager { onLinkLeave, ); this._disposables.push(this._terminal.registerLinkProvider(urlProvider)); + + // xterm always registers its own OSC 8 hyperlink provider first. Without + // this, OSC 8 links use xterm's default confirm() + window.open() path, + // which is blocked in Electron and also bypasses our link preferences. + this._oscLinkHandler = { + allowNonHttpProtocols: false, + activate: (event, uri) => { + onUrlClick(event, uri); + }, + hover: onLinkHover + ? (event) => onLinkHover(event, { kind: "url" }) + : undefined, + leave: onLinkLeave ? () => onLinkLeave() : undefined, + }; + this._terminal.options.linkHandler = this._oscLinkHandler; } // 3. SUPERSET ADDITION: Word link detector (lowest priority).