Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ apps/*/tooling/db/dev.db-shm
apps/*/tooling/node-deps/node_modules/
.pi/extensions/emdash-hook.ts
.opencode/plugins/emdash-notifications.js
.amp/plugins/emdash-hook.ts

# Vitest / Browser testing
.vitest-attachments/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,32 @@
import { ExternalLink } from '@renderer/lib/components/external-link';
import { Alert, AlertDescription } from '@renderer/lib/ui/alert';

export function SkillsInfoBox() {
return (
<Alert>
<AlertDescription>
Skills from the{' '}
<a
<ExternalLink
href="https://github.com/openai/skills"
target="_blank"
rel="noopener noreferrer"
className="decoration-muted-foreground/40 font-medium text-foreground underline underline-offset-2 hover:decoration-foreground"
>
OpenAI
</a>{' '}
</ExternalLink>{' '}
and{' '}
<a
<ExternalLink
href="https://github.com/anthropics/skills"
target="_blank"
rel="noopener noreferrer"
className="decoration-muted-foreground/40 font-medium text-foreground underline underline-offset-2 hover:decoration-foreground"
>
Anthropic
</a>{' '}
</ExternalLink>{' '}
catalogs. Install a skill to make it available across all your coding agents. Skills follow
the open{' '}
<a
<ExternalLink
href="https://agentskills.io"
target="_blank"
rel="noopener noreferrer"
className="decoration-muted-foreground/40 font-medium text-foreground underline underline-offset-2 hover:decoration-foreground"
>
Agent Skills
</a>{' '}
</ExternalLink>{' '}
standard. If you want to use skills from another library, feel free to let us know through
the feedback modal.
</AlertDescription>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import { ExternalLink, Link, Loader2 } from 'lucide-react';
import { ExternalLink as ExternalLinkIcon, Link, Loader2 } from 'lucide-react';
import { observer } from 'mobx-react-lite';
import { type ReactNode, useCallback, useRef, useState } from 'react';
import {
ISSUE_PROVIDER_META,
ISSUE_PROVIDER_ORDER,
} from '@renderer/features/integrations/issue-provider-meta';
import { PROVIDER_ICON_COMPONENTS } from '@renderer/features/integrations/provider-icons';
import { ExternalLink } from '@renderer/lib/components/external-link';
import { InlineMarkdown } from '@renderer/lib/components/inline-markdown';
import {
IssueStatusIndicator,
toIssueStatus,
} from '@renderer/lib/components/issue-status-indicator';
import { rpc } from '@renderer/lib/ipc';
import { useNavigate } from '@renderer/lib/layout/navigation-provider';
import { Button } from '@renderer/lib/ui/button';
import {
Expand Down Expand Up @@ -324,13 +324,15 @@ export function SelectedIssueValue({ issue }: { issue: LinkedIssue }) {
<span className="mt-0.5 flex items-center justify-between gap-2">
<span className="group flex min-w-0 items-center gap-1">
<div className="text-muted-foreground min-w-0 truncate">{issue.title}</div>
<button
<ExternalLink
href={issue.url ?? '#'}
className="opacity-0 group-hover:opacity-100"
disabled={!issue.url}
onClick={() => issue.url && rpc.app.openExternal(issue.url)}
onClick={(event) => {
if (!issue.url) event.preventDefault();
}}
>
<ExternalLink className="size-3" />
</button>
<ExternalLinkIcon className="size-3" />
</ExternalLink>
</span>
<span className="flex items-center gap-1">
<ProviderLogo provider={issue.provider} className="size-3 opacity-40" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { CheckCircle2, ExternalLink, Loader2, MinusCircle, XCircle } from 'lucide-react';
import {
CheckCircle2,
ExternalLink as ExternalLinkIcon,
Loader2,
MinusCircle,
XCircle,
} from 'lucide-react';
import { observer } from 'mobx-react-lite';
import { useMemo } from 'react';
import { useSyncCheckRuns } from '@renderer/features/tasks/diff-view/state/use-check-runs';
import { rpc } from '@renderer/lib/ipc';
import { ExternalLink } from '@renderer/lib/components/external-link';
import { EmptyState } from '@renderer/lib/ui/empty-state';
import {
computeCheckBucket,
Expand Down Expand Up @@ -70,14 +76,13 @@ export function CheckRunItem({ check }: { check: CheckRun }) {
<div className="flex shrink-0 items-center gap-2">
{duration && <span className="text-xs text-foreground-passive">{duration}</span>}
{detailsUrl && (
<button
type="button"
<ExternalLink
href={detailsUrl}
aria-label={`Open ${check.name} check details`}
className="absolute top-1/2 right-3 hidden -translate-y-1/2 items-center justify-center rounded bg-background-1 px-1 py-0.5 text-foreground-muted group-hover:flex hover:text-foreground"
onClick={() => void rpc.app.openExternal(detailsUrl)}
>
<ExternalLink className="size-3.5" />
</button>
<ExternalLinkIcon className="size-3.5" />
</ExternalLink>
)}
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ExternalLink, MessageSquare } from 'lucide-react';
import { ExternalLink as ExternalLinkIcon, MessageSquare } from 'lucide-react';
import { useMemo } from 'react';
import { rpc } from '@renderer/lib/ipc';
import { ExternalLink } from '@renderer/lib/components/external-link';
import { MarkdownRenderer } from '@renderer/lib/ui/markdown-renderer';
import { RelativeTime } from '@renderer/lib/ui/relative-time';
import { cn } from '@renderer/utils/utils';
Expand Down Expand Up @@ -71,12 +71,12 @@ function CommentItem({ comment }: { comment: PullRequestConversationItem }) {
<MarkdownRenderer content={comment.body} variant="compact" allowHtml />
</div>
</div>
<button
<ExternalLink
href={comment.url}
className="absolute top-2 right-3 hidden items-center justify-center rounded bg-background-1 px-1 py-0.5 text-foreground-muted group-hover:flex hover:text-foreground"
onClick={() => void rpc.app.openExternal(comment.url)}
>
<ExternalLink className="size-3.5" />
</button>
<ExternalLinkIcon className="size-3.5" />
</ExternalLink>
</div>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { ExternalLink } from 'lucide-react';
import { ExternalLink as ExternalLinkIcon } from 'lucide-react';
import { observer } from 'mobx-react-lite';
import { useState } from 'react';
import {
useTaskViewContext,
useWorkspaceViewModel,
} from '@renderer/features/tasks/task-view-context';
import { ExternalLink } from '@renderer/lib/components/external-link';
import { PrMergeLine } from '@renderer/lib/components/pr-merge-line';
import { PrNumberBadge } from '@renderer/lib/components/pr-number-badge';
import { StatusIcon } from '@renderer/lib/components/pr-status-icon';
import { toast } from '@renderer/lib/hooks/use-toast';
import { rpc } from '@renderer/lib/ipc';
import { type SplitButtonAction } from '@renderer/lib/ui/split-button';
import { ToggleGroup, ToggleGroupItem } from '@renderer/lib/ui/toggle-group';
import { cn } from '@renderer/utils/utils';
Expand Down Expand Up @@ -106,17 +106,14 @@ export const PullRequestEntry = observer(function PullRequestEntry({ pr }: { pr:
<div className={cn('flex min-h-0 flex-1 flex-col border-t border-border')}>
<div className="flex w-full flex-col gap-2 p-2.5">
<div className="flex items-center justify-between gap-2">
<button
className="group relative flex min-w-0 items-center gap-2"
onClick={() => rpc.app.openExternal(pr.url)}
>
<ExternalLink href={pr.url} className="group relative flex min-w-0 items-center gap-2">
<StatusIcon className="size-4" pr={pr} />
<span className="min-w-0 flex-1 truncate text-sm font-normal">{pr.title}</span>
<PrNumberBadge number={getPrNumber(pr) ?? 0} />
<span className="absolute right-0 flex items-center bg-linear-to-r from-transparent to-background pr-0.5 pl-4 opacity-0 transition-opacity group-hover:opacity-100">
<ExternalLink className="size-3.5 text-foreground-muted" />
<ExternalLinkIcon className="size-3.5 text-foreground-muted" />
</span>
</button>
</ExternalLink>
</div>
<PrMergeLine pr={pr} />
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ import {
useWorkspaceViewModel,
} from '@renderer/features/tasks/task-view-context';
import { ConnectionStatusDot } from '@renderer/lib/components/connection-status-dot';
import { ExternalLink } from '@renderer/lib/components/external-link';
import { OpenInMenu } from '@renderer/lib/components/titlebar/open-in-menu';
import { Titlebar } from '@renderer/lib/components/titlebar/Titlebar';
import { rpc } from '@renderer/lib/ipc';
import { useNavigate } from '@renderer/lib/layout/navigation-provider';
import { Badge } from '@renderer/lib/ui/badge';
import { Button } from '@renderer/lib/ui/button';
Expand Down Expand Up @@ -420,11 +420,10 @@ function LinkedIssueBadge({ issue }: { issue: LinkedIssue }) {
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
disabled={!issue.url}
onClick={() => {
if (issue.url) void rpc.app.openExternal(issue.url);
<ExternalLink
href={issue.url ?? '#'}
onClick={(event) => {
if (!issue.url) event.preventDefault();
}}
className="hover:bg-muted/30 flex items-center gap-1 rounded-md border border-border px-1.5 py-0.5 text-xs text-foreground-muted disabled:cursor-default disabled:opacity-60"
>
Expand All @@ -434,7 +433,7 @@ function LinkedIssueBadge({ issue }: { issue: LinkedIssue }) {
) : (
<span className="font-mono">{issue.identifier}</span>
)}
</button>
</ExternalLink>
}
/>
<TooltipContent>{issue.title || issue.identifier}</TooltipContent>
Expand Down
20 changes: 20 additions & 0 deletions apps/emdash-desktop/src/renderer/lib/components/external-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { type ComponentPropsWithoutRef, type MouseEvent } from 'react';
import { confirmOpenExternalLink } from '@renderer/lib/open-external-link';

type ExternalLinkProps = Omit<ComponentPropsWithoutRef<'a'>, 'href'> & {
href: string;
onOpenError?: (error: unknown) => void;
};

export function ExternalLink({ href, onClick, onOpenError, ...props }: ExternalLinkProps) {
const handleClick = (event: MouseEvent<HTMLAnchorElement>) => {
onClick?.(event);
if (event.defaultPrevented) return;
if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey) return;

event.preventDefault();
confirmOpenExternalLink(href, onOpenError);
};

return <a href={href} rel="noopener noreferrer" onClick={handleClick} {...props} />;
}
8 changes: 4 additions & 4 deletions apps/emdash-desktop/src/renderer/lib/components/pr-badge.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ExternalLink } from 'lucide-react';
import { ExternalLink as ExternalLinkIcon } from 'lucide-react';
import { ExternalLink } from '@renderer/lib/components/external-link';
import { PrMergeLine } from '@renderer/lib/components/pr-merge-line';
import { Popover, PopoverContent, PopoverTrigger } from '@renderer/lib/ui/popover';
import { cn } from '@renderer/utils/utils';
import { getPrNumber, type PullRequest } from '@shared/core/pull-requests/pull-requests';
import { rpc } from '../ipc';
import { Button } from '../ui/button';
import { RelativeTime } from '../ui/relative-time';
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
Expand Down Expand Up @@ -59,12 +59,12 @@ export function PrBadge({ variant = 'default', pr, className, hoverDelay }: PrBa
<Tooltip>
<TooltipTrigger>
<Button
render={<ExternalLink href={pr.url} />}
variant="ghost"
size="icon-xs"
className="cursor-pointer"
onClick={() => rpc.app.openExternal(pr.url)}
>
<ExternalLink className="size-3.5" />
<ExternalLinkIcon className="size-3.5" />
</Button>
</TooltipTrigger>
<TooltipContent>Open PR on GitHub</TooltipContent>
Expand Down
9 changes: 7 additions & 2 deletions apps/emdash-desktop/src/renderer/lib/ui/markdown-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,11 @@ const ResolvedImage: React.FC<{
type WithChildren = { children?: React.ReactNode };
type WithChildrenAndClass = { children?: React.ReactNode; className?: string };
type AnchorProps = { href?: string; children?: React.ReactNode };

function shouldConfirmExternalLinkClick(event: React.MouseEvent): boolean {
return event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey;
}

type ImgProps = React.ComponentPropsWithoutRef<'img'> & ExtraProps;

function getCodeBlock(children: React.ReactNode, className?: string) {
Expand Down Expand Up @@ -207,7 +212,7 @@ function useFullComponents(
a: ({ href, children }: AnchorProps) => {
const isHttp = typeof href === 'string' && /^https?:\/\//i.test(href);
const handleClick = (e: React.MouseEvent) => {
if (isHttp) {
if (isHttp && shouldConfirmExternalLinkClick(e)) {
e.preventDefault();
confirmOpenExternalLink(href);
}
Expand Down Expand Up @@ -354,7 +359,7 @@ function useCompactComponents(isDark: boolean) {
a: ({ href, children }: AnchorProps) => {
const isHttp = typeof href === 'string' && /^https?:\/\//i.test(href);
const handleClick = (e: React.MouseEvent) => {
if (isHttp) {
if (isHttp && shouldConfirmExternalLinkClick(e)) {
e.preventDefault();
confirmOpenExternalLink(href);
}
Expand Down
Loading