Skip to content
Merged
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
22 changes: 22 additions & 0 deletions apps/web/src/components/Icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,28 @@ export const GrokIcon: Icon = ({ className, ...props }) => (
</svg>
);

/** Official four-colour Slack "pinwheel" mark (brand asset, viewBox 122.8). */
export const SlackIcon: Icon = (props) => (
<svg {...props} viewBox="0 0 122.8 122.8" fill="none">
<path
fill="#E01E5A"
d="M25.8 77.6c0 7.1-5.8 12.9-12.9 12.9S0 84.7 0 77.6s5.8-12.9 12.9-12.9h12.9v12.9zm6.5 0c0-7.1 5.8-12.9 12.9-12.9s12.9 5.8 12.9 12.9v32.3c0 7.1-5.8 12.9-12.9 12.9s-12.9-5.8-12.9-12.9V77.6z"
/>
<path
fill="#36C5F0"
d="M45.2 25.8c-7.1 0-12.9-5.8-12.9-12.9S38.1 0 45.2 0s12.9 5.8 12.9 12.9v12.9H45.2zm0 6.5c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9H12.9C5.8 58.1 0 52.3 0 45.2s5.8-12.9 12.9-12.9h32.3z"
/>
<path
fill="#2EB67D"
d="M97 45.2c0-7.1 5.8-12.9 12.9-12.9s12.9 5.8 12.9 12.9-5.8 12.9-12.9 12.9H97V45.2zm-6.5 0c0 7.1-5.8 12.9-12.9 12.9s-12.9-5.8-12.9-12.9V12.9C64.7 5.8 70.5 0 77.6 0s12.9 5.8 12.9 12.9v32.3z"
/>
<path
fill="#ECB22E"
d="M77.6 97c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9-12.9-5.8-12.9-12.9V97h12.9zm0-6.5c-7.1 0-12.9-5.8-12.9-12.9s5.8-12.9 12.9-12.9h32.3c7.1 0 12.9 5.8 12.9 12.9s-5.8 12.9-12.9 12.9H77.6z"
/>
</svg>
);

export const TraeIcon: Icon = (props) => (
<svg {...props} viewBox="0 0 24 24" fill="currentColor">
{/* Back rectangle: left strip + bottom strip drawn separately — empty bottom-left corner is the gap between them */}
Expand Down
8 changes: 7 additions & 1 deletion apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import {
import { isDesktopLocalConnectionTarget } from "../connection/desktopLocal";
import { useDesktopLocalBootstraps } from "../connection/useDesktopLocalBootstraps";
import { isElectron } from "../env";
import { SlackThreadBadge, useThreadDisplayTitle } from "./SlackThreadBadge";
import { useOpenPrLink } from "../lib/openPullRequestLink";
import { isTerminalFocused } from "../lib/terminalFocus";
import { isMacPlatform } from "../lib/utils";
Expand Down Expand Up @@ -456,6 +457,8 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
});
const prStatus = prStatusIndicator(pr, gitStatus.data?.sourceControlProvider);
const terminalStatus = terminalStatusFromRunningIds(runningTerminalIds);
// Threads bridged from Slack (`Slack: …` titles) show the Slack mark in place of the prefix.
const { isSlack, displayTitle } = useThreadDisplayTitle(thread.title);
const isConfirmingArchive = confirmingArchiveThreadKey === threadKey && !isThreadRunning;
const threadMetaClassName = isConfirmingArchive
? "pointer-events-none opacity-0"
Expand Down Expand Up @@ -718,7 +721,10 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr
className="min-w-0 flex-1 truncate text-sm"
data-testid={`thread-title-${thread.id}`}
>
{thread.title}
{isSlack ? (
<SlackThreadBadge density="v1" isActive={isActive} isMuted={false} />
) : null}
{displayTitle}
</span>
}
/>
Expand Down
10 changes: 9 additions & 1 deletion apps/web/src/components/SidebarV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
squashAtomCommandFailure,
} from "@t3tools/client-runtime/state/runtime";
import { isElectron } from "../env";
import { SlackThreadBadge, useThreadDisplayTitle } from "./SlackThreadBadge";
import {
resolveShortcutCommand,
shortcutLabelForCommand,
Expand Down Expand Up @@ -526,6 +527,10 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
const isRemote =
props.currentEnvironmentId !== null && thread.environmentId !== props.currentEnvironmentId;

// Threads bridged from Slack (`Slack: …` titles) show the Slack mark in
// place of the prefix so they read differently from local conversations.
const { isSlack, displayTitle } = useThreadDisplayTitle(thread.title);

const detailsTooltip = (
<SidebarV2ThreadTooltip
thread={thread}
Expand Down Expand Up @@ -707,7 +712,10 @@ const SidebarV2Row = memo(function SidebarV2Row(props: {
),
)}
>
{thread.title}
{isSlack ? (
<SlackThreadBadge density={variant} isActive={props.isActive} isMuted={shouldRecede} />
) : null}
{displayTitle}
</span>
);

Expand Down
67 changes: 67 additions & 0 deletions apps/web/src/components/SlackThreadBadge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { useMemo } from "react";

import { SlackIcon } from "./Icons";
import { getThreadChannel, stripThreadChannelPrefix } from "~/lib/threadChannel";
import { cn } from "~/lib/utils";

/**
* Sidebar row title for a thread, with channel awareness. Threads bridged in
* from Slack (`Slack: …` titles) render the Slack mark in place of the
* literal prefix — the channel is read where the eye already starts, and the
* row gets those characters back for the actual sentence. Rename still edits
* the full stored title.
*/
export function useThreadDisplayTitle(title: string): { isSlack: boolean; displayTitle: string } {
return useMemo(() => {
const isSlack = getThreadChannel(title) === "slack";
return { isSlack, displayTitle: isSlack ? stripThreadChannelPrefix(title) : title };
}, [title]);
}

export type SlackThreadBadgeDensity = "card" | "slim" | "v1";

/**
* The mark is 14px everywhere (all three sidebar row layouts set the title at
* `text-sm`, and the pinwheel's negative space collapses below that); density
* only tunes the gap to the title. Receded rows desaturate and step back so the
* settled tail keeps its hierarchy, and come back to full colour on row hover.
* A visually-hidden `Slack:` keeps the channel in the accessible name, since
* the visible mark replaces the prefix text.
*/
export function SlackThreadBadge({
density,
isActive,
isMuted,
className,
}: {
density: SlackThreadBadgeDensity;
isActive: boolean;
isMuted: boolean;
className?: string | undefined;
}) {
return (
<>
<span
role="img"
aria-label="Slack conversation"
title="Bridged from Slack"
className={cn(
"inline-flex size-3.5 align-[-0.15em] transition-[opacity,filter] duration-150",
density === "v1" ? "mr-1" : "mr-1.5",
isMuted
? "opacity-65 saturate-[0.8] dark:opacity-55"
: isActive
? "opacity-100"
: "opacity-90",
isMuted &&
density !== "v1" &&
"group-hover/v2-row:opacity-100 group-hover/v2-row:saturate-100",
className,
)}
>
<SlackIcon aria-hidden focusable="false" className="size-full shrink-0" />
</span>
<span className="sr-only">Slack: </span>
</>
);
}
32 changes: 32 additions & 0 deletions apps/web/src/lib/threadChannel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, expect, it } from "vite-plus/test";

import { getThreadChannel, stripThreadChannelPrefix } from "./threadChannel";

describe("getThreadChannel", () => {
it("detects a Slack: prefix case-insensitively", () => {
expect(getThreadChannel("Slack: Can you look at the errors?")).toBe("slack");
expect(getThreadChannel("slack: lower")).toBe("slack");
expect(getThreadChannel(" SLACK : spaced")).toBe("slack");
});

it("ignores titles that merely mention Slack", () => {
expect(getThreadChannel("Add Slack: integration")).toBeNull();
expect(getThreadChannel("Slackline tricks")).toBeNull();
expect(getThreadChannel("")).toBeNull();
expect(getThreadChannel(null)).toBeNull();
});
});

describe("stripThreadChannelPrefix", () => {
it("removes the prefix and leading whitespace", () => {
expect(stripThreadChannelPrefix("Slack: Can you look?")).toBe("Can you look?");
});

it("keeps the original title when only the prefix is present", () => {
expect(stripThreadChannelPrefix("Slack:")).toBe("Slack:");
});

it("leaves non-channel titles untouched", () => {
expect(stripThreadChannelPrefix("Fix booking status")).toBe("Fix booking status");
});
});
23 changes: 23 additions & 0 deletions apps/web/src/lib/threadChannel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Thread "channel" detection.
*
* Threads bridged in from an external service (the conversation gateway —
* Slack first) are titled with a channel prefix such as `Slack: …`. The
* sidebar uses this to decorate the row with a channel symbol so bridged
* conversations are recognisable at a glance.
*/
export type ThreadChannel = "slack";

const SLACK_TITLE_PREFIX = /^\s*slack\s*:/iu;

export function getThreadChannel(title: string | null | undefined): ThreadChannel | null {
if (!title) return null;
if (SLACK_TITLE_PREFIX.test(title)) return "slack";
return null;
}

/** Title with the channel prefix removed (for variants that render the symbol instead of the word). */
export function stripThreadChannelPrefix(title: string): string {
const stripped = title.replace(SLACK_TITLE_PREFIX, "").trimStart();
return stripped.length > 0 ? stripped : title;
}
Loading