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
122 changes: 122 additions & 0 deletions apps/web/src/components/chat/DraftHeroHeadline.logic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { describe, expect, it } from "vite-plus/test";

import type { SidebarProjectPickerEntry } from "~/sidebarProjectGrouping";
import { resolveProjectPickerEnvironmentHints } from "./DraftHeroHeadline.logic";

function entry(input: {
projectKey: string;
displayName: string;
environmentId: string;
environmentLabel: string | null;
workspaceRoot: string;
}): SidebarProjectPickerEntry {
return {
group: { projectKey: input.projectKey, displayName: input.displayName },
targetProject: {
environmentId: input.environmentId,
environmentLabel: input.environmentLabel,
workspaceRoot: input.workspaceRoot,
},
isPreferred: false,
} as unknown as SidebarProjectPickerEntry;
}

describe("resolveProjectPickerEnvironmentHints", () => {
it("hides environment labels when every entry lives in one environment", () => {
const hints = resolveProjectPickerEnvironmentHints([
entry({
projectKey: "a",
displayName: "quasar",
environmentId: "local",
environmentLabel: "laptop",
workspaceRoot: "/a/quasar",
}),
entry({
projectKey: "b",
displayName: "pulsar",
environmentId: "local",
environmentLabel: "laptop",
workspaceRoot: "/a/pulsar",
}),
]);
expect(hints.get("a")).toEqual({ environmentLabel: null, text: null });
expect(hints.get("b")).toEqual({ environmentLabel: null, text: null });
});

it("shows the environment label once entries span two environments", () => {
const hints = resolveProjectPickerEnvironmentHints([
entry({
projectKey: "a",
displayName: "quasar",
environmentId: "local",
environmentLabel: "laptop",
workspaceRoot: "/a/quasar",
}),
entry({
projectKey: "b",
displayName: "quasar",
environmentId: "remote",
environmentLabel: "build-box",
workspaceRoot: "/srv/quasar",
}),
]);
expect(hints.get("a")).toEqual({ environmentLabel: "laptop", text: "laptop" });
expect(hints.get("b")).toEqual({ environmentLabel: "build-box", text: "build-box" });
});

it("falls back to the workspace path when name and label both collide", () => {
const hints = resolveProjectPickerEnvironmentHints([
entry({
projectKey: "a",
displayName: "quasar",
environmentId: "local",
environmentLabel: "laptop",
workspaceRoot: "/a/quasar",
}),
entry({
projectKey: "b",
displayName: "quasar",
environmentId: "local",
environmentLabel: "laptop",
workspaceRoot: "/b/quasar",
}),
entry({
projectKey: "c",
displayName: "quasar",
environmentId: "remote",
environmentLabel: "build-box",
workspaceRoot: "/srv/quasar",
}),
]);
expect(hints.get("a")).toEqual({
environmentLabel: "laptop",
text: "laptop · /a/quasar",
});
expect(hints.get("b")).toEqual({
environmentLabel: "laptop",
text: "laptop · /b/quasar",
});
expect(hints.get("c")).toEqual({ environmentLabel: "build-box", text: "build-box" });
});

it("uses the bare workspace path for same-name checkouts in a single environment", () => {
const hints = resolveProjectPickerEnvironmentHints([
entry({
projectKey: "a",
displayName: "quasar",
environmentId: "local",
environmentLabel: null,
workspaceRoot: "/a/quasar",
}),
entry({
projectKey: "b",
displayName: "quasar",
environmentId: "local",
environmentLabel: null,
workspaceRoot: "/b/quasar",
}),
]);
expect(hints.get("a")).toEqual({ environmentLabel: null, text: "/a/quasar" });
expect(hints.get("b")).toEqual({ environmentLabel: null, text: "/b/quasar" });
});
});
42 changes: 42 additions & 0 deletions apps/web/src/components/chat/DraftHeroHeadline.logic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { SidebarProjectPickerEntry } from "~/sidebarProjectGrouping";

export interface ProjectPickerEnvironmentHint {
/** Environment label for the row, or null when the row has no environment to show. */
readonly environmentLabel: string | null;
/**
* Secondary text rendered next to the project name. Falls back to the
* workspace path when another row would otherwise read identically.
*/
readonly text: string | null;
}

/**
* The same project name can exist in several environments, so the environment
* label is only surfaced when the picker spans two or more environments.
* Labels are not unique either: two checkouts of one project on the same
* machine collide on both name and label, so those rows fall back to the
* workspace path (mirroring ProjectSettingsPanel's checkout labels).
*/
export function resolveProjectPickerEnvironmentHints(
entries: ReadonlyArray<SidebarProjectPickerEntry>,
): ReadonlyMap<string, ProjectPickerEnvironmentHint> {
const environmentIds = new Set(entries.map((entry) => entry.targetProject.environmentId));
const showEnvironmentLabels = environmentIds.size >= 2;
const hints = new Map<string, ProjectPickerEnvironmentHint>();
for (const entry of entries) {
const environmentLabel = showEnvironmentLabels ? entry.targetProject.environmentLabel : null;
const collides = entries.some(
(other) =>
other.group.projectKey !== entry.group.projectKey &&
other.group.displayName === entry.group.displayName &&
(showEnvironmentLabels ? other.targetProject.environmentLabel : null) === environmentLabel,
);
const text = collides
? environmentLabel === null
? entry.targetProject.workspaceRoot
: `${environmentLabel} · ${entry.targetProject.workspaceRoot}`
: environmentLabel;
hints.set(entry.group.projectKey, { environmentLabel, text });
}
return hints;
}
40 changes: 38 additions & 2 deletions apps/web/src/components/chat/DraftHeroHeadline.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { DraftId } from "~/composerDraftStore";
import { useComposerDraftStore } from "~/composerDraftStore";
import type { ScopedProjectRef } from "@t3tools/contracts";
import { resolveEnvironmentMachineKind } from "@t3tools/contracts";
import { scopedProjectKey, scopeProjectRef } from "@t3tools/client-runtime/environment";
import { FolderPlusIcon } from "lucide-react";
import { useCallback, useMemo } from "react";
Expand All @@ -15,6 +16,7 @@ import {
} from "~/sidebarProjectGrouping";
import { useProjects, useThreadShells } from "~/state/entities";
import { useEnvironments, usePrimaryEnvironmentId } from "~/state/environments";
import { EnvironmentMachineIcon } from "../EnvironmentMachineIcon";
import { ProjectFavicon } from "../ProjectFavicon";
import { sortLogicalProjectsForSidebar } from "../Sidebar.logic";
import {
Expand All @@ -27,6 +29,7 @@ import {
MenuTrigger,
} from "../ui/menu";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
import { resolveProjectPickerEnvironmentHints } from "./DraftHeroHeadline.logic";

interface DraftHeroHeadlineProps {
readonly draftId: DraftId | null;
Expand Down Expand Up @@ -60,6 +63,19 @@ export function DraftHeroHeadline({
),
[environments],
);
const environmentMachineById = useMemo(
() =>
new Map(
environments.map(
(environment) =>
[
environment.environmentId,
resolveEnvironmentMachineKind(environment.serverConfig),
] as const,
),
),
[environments],
);
const projectGroups = useMemo(
() =>
sortLogicalProjectsForSidebar(
Expand Down Expand Up @@ -94,6 +110,10 @@ export function DraftHeroHeadline({
() => new Map(projectPickerEntries.map((entry) => [entry.group.projectKey, entry] as const)),
[projectPickerEntries],
);
const environmentHintByKey = useMemo(
() => resolveProjectPickerEnvironmentHints(projectPickerEntries),
[projectPickerEntries],
);
const activeProjectGroup =
activeProjectRef === null
? null
Expand Down Expand Up @@ -163,7 +183,11 @@ export function DraftHeroHeadline({
}
}}
>
{projectPickerEntries.map(({ group }) => {
{projectPickerEntries.map(({ group, targetProject }) => {
const hint = environmentHintByKey.get(group.projectKey) ?? {
environmentLabel: null,
text: null,
};
return (
<MenuRadioItem
key={group.projectKey}
Expand All @@ -177,9 +201,21 @@ export function DraftHeroHeadline({
{group.displayName}
</TooltipTrigger>
<TooltipPopup side="top" className="max-w-80">
{group.displayName}
{hint.text === null ? group.displayName : `${group.displayName} (${hint.text})`}
</TooltipPopup>
</Tooltip>
{hint.text === null ? null : (
<span className="ml-auto inline-flex min-w-0 max-w-32 items-center gap-1 pl-3 text-muted-foreground text-xs">
{hint.environmentLabel === null ? null : (
<EnvironmentMachineIcon
aria-hidden
kind={environmentMachineById.get(targetProject.environmentId) ?? "server"}
className="size-3 shrink-0"
/>
)}
<span className="min-w-0 truncate">{hint.text}</span>
</span>
)}
</MenuRadioItem>
);
})}
Expand Down
Loading