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
89 changes: 89 additions & 0 deletions apps/code/src/renderer/features/auth/utils/userInitials.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { describe, expect, it } from "vitest";
import { getUserInitials } from "./userInitials";

describe("getUserInitials", () => {
it("returns uppercased first+last initials when both are set", () => {
expect(getUserInitials({ first_name: "Charles", last_name: "Vien" })).toBe(
"CV",
);
});

it("uppercases lowercase names", () => {
expect(getUserInitials({ first_name: "alice", last_name: "smith" })).toBe(
"AS",
);
});

it("returns the first initial when only first_name is set", () => {
expect(getUserInitials({ first_name: "Charles" })).toBe("C");
});

it("returns the last initial when only last_name is set", () => {
expect(getUserInitials({ last_name: "Vien" })).toBe("V");
});

it("falls back to the first two letters of the email local part", () => {
expect(getUserInitials({ email: "charles.v@posthog.com" })).toBe("CH");
});

it("never pulls letters from the email domain", () => {
expect(getUserInitials({ email: "1234@example.com" })).toBe("U");
});

it("skips non-letter chars when extracting from names", () => {
expect(getUserInitials({ first_name: " 123Alice" })).toBe("A");
});

it("skips non-letter chars when extracting from email local part", () => {
expect(getUserInitials({ email: "1.2_charles@posthog.com" })).toBe("CH");
});

it("handles astral-plane characters without producing lone surrogates", () => {
// U+20BB7 ("𠮷") is encoded as a UTF-16 surrogate pair. The old
// implementation used string[0], which returned only the high surrogate
// and rendered as a garbled tofu char.
expect(getUserInitials({ first_name: "𠮷田", last_name: "Smith" })).toBe(
"𠮷S",
);
});

it("handles accented characters", () => {
expect(getUserInitials({ first_name: "Émile", last_name: "Über" })).toBe(
"ÉÜ",
);
});

it("returns 'U' for a null user", () => {
expect(getUserInitials(null)).toBe("U");
});

it("returns 'U' for an undefined user", () => {
expect(getUserInitials(undefined)).toBe("U");
});

it("returns 'U' when every field is an empty string", () => {
expect(getUserInitials({ first_name: "", last_name: "", email: "" })).toBe(
"U",
);
});

it("returns 'U' when names have no letters and there is no email", () => {
expect(getUserInitials({ first_name: "123" })).toBe("U");
});

it("returns 'U' when names have no letters and the email local part has no letters", () => {
expect(
getUserInitials({ first_name: "123", email: "456@example.com" }),
).toBe("U");
});

it("ignores null name fields and uses the email fallback", () => {
expect(
getUserInitials({
first_name: null,
last_name: null,
email: "charles.v@posthog.com",
}),
).toBe("CH");
});
});
31 changes: 31 additions & 0 deletions apps/code/src/renderer/features/auth/utils/userInitials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
interface UserLike {
first_name?: string | null;
last_name?: string | null;
email?: string | null;
}

function firstLetter(value: string | null | undefined): string | null {
if (!value) return null;
const match = value.match(/\p{L}/u);
return match ? match[0] : null;
}

export function getUserInitials(user: UserLike | null | undefined): string {
const first = firstLetter(user?.first_name);
const last = firstLetter(user?.last_name);
if (first && last) {
return `${first}${last}`.toUpperCase();
}
if (first) {
return first.toUpperCase();
}
if (last) {
return last.toUpperCase();
}
const emailLocal = user?.email?.split("@")[0];
const emailLetters = emailLocal?.match(/\p{L}/gu)?.slice(0, 2).join("");
if (emailLetters) {
return emailLetters.toUpperCase();
}
return "U";
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ import { CodeMirrorEditor } from "@features/code-editor/components/CodeMirrorEdi
import { EnrichmentPopover } from "@features/code-editor/components/EnrichmentPopover";
import { useCloudFileContent } from "@features/code-editor/hooks/useCloudFileContent";
import { useFileEnrichment } from "@features/code-editor/hooks/useFileEnrichment";
import { useMarkdownViewerStore } from "@features/code-editor/stores/markdownViewerStore";
import { isMarkdownFile } from "@features/code-editor/utils/markdownUtils";
import { getRelativePath } from "@features/code-editor/utils/pathUtils";
import { usePanelLayoutStore } from "@features/panels";
import { useFileTreeStore } from "@features/right-sidebar/stores/fileTreeStore";
import { useCwd } from "@features/sidebar/hooks/useCwd";
import { useIsWorkspaceCloudRun } from "@features/workspace/hooks/useWorkspace";
import { Check, Code, Copy, Eye } from "@phosphor-icons/react";
import { Check, Copy } from "@phosphor-icons/react";
import { Box, Flex, IconButton, Text } from "@radix-ui/themes";
import { trpcClient, useTRPC } from "@renderer/trpc/client";
import { getImageMimeType, isImageFile } from "@shared/constants/image";
Expand Down Expand Up @@ -76,10 +75,6 @@ export function CodeEditorPanel({
const filePath = getRelativePath(absolutePath, repoPath);
const isImage = isImageFile(absolutePath);
const isMarkdown = isMarkdownFile(absolutePath);
const preferRendered = useMarkdownViewerStore((s) => s.preferRendered);
const togglePreferRendered = useMarkdownViewerStore(
(s) => s.togglePreferRendered,
);
const openFileInSplit = usePanelLayoutStore((s) => s.openFileInSplit);
const expandToFile = useFileTreeStore((s) => s.expandToFile);
const [copied, setCopied] = useState(false);
Expand Down Expand Up @@ -272,36 +267,17 @@ export function CodeEditorPanel({
{copied ? <Check size={14} /> : <Copy size={14} />}
</IconButton>
</Tooltip>
<Tooltip content={preferRendered ? "View source" : "View rendered"}>
<IconButton
size="1"
variant="ghost"
color="gray"
className="cursor-pointer"
onClick={togglePreferRendered}
>
{preferRendered ? <Code size={14} /> : <Eye size={14} />}
</IconButton>
</Tooltip>
</Flex>
</Flex>
<Box className="flex-1 overflow-auto">
{preferRendered ? (
<Box className="plan-markdown max-w-[750px]" p="5">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={markdownComponents}
>
{fileContent}
</ReactMarkdown>
</Box>
) : (
<CodeMirrorEditor
content={fileContent}
filePath={absolutePath}
readOnly
/>
)}
<Box className="plan-markdown max-w-[750px]" p="5">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={markdownComponents}
>
{fileContent}
</ReactMarkdown>
</Box>
</Box>
</Flex>
);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ExpandedContentBox,
getContentText,
StatusIndicators,
stripCodeFences,
ToolTitle,
type ToolViewProps,
truncateText,
Expand Down Expand Up @@ -40,7 +41,10 @@ export function ExecuteToolView({
const description =
executeInput?.description ?? (command ? undefined : title);

const output = (getContentText(content) ?? "").replace(ANSI_REGEX, "");
const output = stripCodeFences(getContentText(content) ?? "").replace(
ANSI_REGEX,
"",
);
const hasOutput = output.trim().length > 0;
const isExpandable = hasOutput;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
useAuthStateValue,
useCurrentUser,
} from "@features/auth/hooks/authQueries";
import { getUserInitials } from "@features/auth/utils/userInitials";
import {
type SettingsCategory,
useSettingsDialogStore,
Expand Down Expand Up @@ -162,11 +163,7 @@ export function SettingsDialog() {

const ActiveComponent = CATEGORY_COMPONENTS[activeCategory];

const initials = user
? user.first_name && user.last_name
? `${user.first_name[0]}${user.last_name[0]}`.toUpperCase()
: (user.email?.substring(0, 2).toUpperCase() ?? "U")
: null;
const initials = getUserInitials(user);

return (
<div
Expand All @@ -176,7 +173,7 @@ export function SettingsDialog() {
<div className="flex h-full w-[256px] shrink-0 flex-col border-gray-6 border-r">
<div className="drag h-[36px] shrink-0 border-b border-b-(--gray-6)" />

{isAuthenticated && user && initials && (
{isAuthenticated && user && (
<Flex
align="center"
gap="3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
useAuthStateValue,
useCurrentUser,
} from "@features/auth/hooks/authQueries";
import { getUserInitials } from "@features/auth/utils/userInitials";
import { useSeat } from "@hooks/useSeat";
import { SignOut } from "@phosphor-icons/react";
import { Avatar, Badge, Button, Flex, Spinner, Text } from "@radix-ui/themes";
Expand Down Expand Up @@ -45,10 +46,7 @@ export function AccountSettings() {
);
}

const initials =
user.first_name && user.last_name
? `${user.first_name[0]}${user.last_name[0]}`.toUpperCase()
: (user.email?.substring(0, 2).toUpperCase() ?? "U");
const initials = getUserInitials(user);

return (
<Flex direction="column">
Expand Down
12 changes: 10 additions & 2 deletions apps/code/src/renderer/utils/toast.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { CheckIcon, InfoIcon, WarningIcon, XIcon } from "@phosphor-icons/react";
import {
CheckIcon,
InfoIcon,
WarningCircleIcon,
WarningIcon,
XIcon,
} from "@phosphor-icons/react";
import { Card, Flex, IconButton, Spinner, Text } from "@radix-ui/themes";
import type { ReactNode } from "react";
import { toast as sonnerToast } from "sonner";
Expand Down Expand Up @@ -26,7 +32,9 @@ function ToastComponent(props: ToastProps) {
case "success":
return <CheckIcon size={16} weight="bold" color="var(--green-9)" />;
case "error":
return <XIcon size={16} weight="bold" color="var(--red-9)" />;
return (
<WarningCircleIcon size={16} weight="bold" color="var(--red-9)" />
);
case "info":
return <InfoIcon size={16} weight="bold" color="var(--blue-9)" />;
case "warning":
Expand Down
Loading