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
2 changes: 2 additions & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const clientSettings: ClientSettings = {
dismissedProviderUpdateNotificationKeys: [],
diffIgnoreWhitespace: true,
environmentIdentificationMode: "artwork",
environmentIconColors: {},
favorites: [],
fontFamilyCode: "",
fontFamilyComposer: "",
Expand All @@ -37,6 +38,7 @@ const clientSettings: ClientSettings = {
glassOpacity: 80,
planModeEnabled: false,
showSkillsInSlashMenu: false,
showLocalEnvironmentIcon: false,
providerModelPreferences: {},
roundedProjectIcons: false,
sidebarAutoSettleAfterDays: 3,
Expand Down
7 changes: 7 additions & 0 deletions apps/desktop/src/settings/LastCodeSettingsImport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ describe("LastCodeSettingsImport", () => {
...DEFAULT_CLIENT_SETTINGS,
legacySidebarScale: 75,
roundedProjectIcons: true,
environmentIconColors: { primary: "#2563eb", remote: "#7c3aed" },
showLocalEnvironmentIcon: true,
favorites: [{ provider: lastCodeCustom, model: "lastcode-model" }],
providerModelPreferences: {
[lastCodeCustom]: { hiddenModels: [], modelOrder: ["lastcode-model"] },
Expand Down Expand Up @@ -215,6 +217,11 @@ describe("LastCodeSettingsImport", () => {
assert.equal(importedClient.fontSizeInterface, 17);
assert.equal(importedClient.legacySidebarScale, 75);
assert.equal(importedClient.roundedProjectIcons, true);
assert.deepEqual(importedClient.environmentIconColors, {
primary: "#2563eb",
remote: "#7c3aed",
});
assert.equal(importedClient.showLocalEnvironmentIcon, true);
assert.deepEqual(importedClient.favorites, [
{ provider: "lastcode_custom", model: "lastcode-model" },
{ provider: "codex", model: "gpt-source" },
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/settings/LastCodeSettingsImport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,8 +236,10 @@ function mergeClientSettings(sourceRaw: string, destinationRaw: string | null):
...source,
favorites,
providerModelPreferences,
environmentIconColors: destination.environmentIconColors,
legacySidebarScale: destination.legacySidebarScale,
roundedProjectIcons: destination.roundedProjectIcons,
showLocalEnvironmentIcon: destination.showLocalEnvironmentIcon,
})}\n`;
}

Expand Down
199 changes: 146 additions & 53 deletions apps/web/src/components/LegacySidebar.tsx

Large diffs are not rendered by default.

46 changes: 41 additions & 5 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import {
scopedThreadKey,
} from "@t3tools/client-runtime/environment";
import type { ScopedThreadRef, ThreadId } from "@t3tools/contracts";
import type { TimestampFormat } from "@t3tools/contracts/settings";
import type { EnvironmentIconColor, TimestampFormat } from "@t3tools/contracts/settings";
import {
AlarmClockIcon,
AlarmClockOffIcon,
Expand All @@ -46,7 +46,6 @@ import {
PinIcon,
PlusIcon,
SearchIcon,
ServerIcon,
SettingsIcon,
SquarePenIcon,
TerminalIcon,
Expand Down Expand Up @@ -169,6 +168,11 @@ import {
} from "./sidebar/SidebarThreadHoverContent";
import { WorktreeCleanupFailureDialog } from "./WorktreeCleanupFailureDialog";
import { getTriggerDisplayModelLabel } from "./chat/providerIconUtils";
import {
EnvironmentIcon,
resolveEnvironmentIconColor,
showV2ThreadCardEnvironmentIcon,
} from "../environmentIcons";
import {
deriveProviderEntriesByEnvironment,
shouldShowInstanceBadge,
Expand Down Expand Up @@ -621,6 +625,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
jumpLabel: string | null;
currentEnvironmentId: string | null;
environmentLabel: string | null;
environmentKnown: boolean;
showLocalEnvironmentIcon: boolean;
configuredEnvironmentIconColor: EnvironmentIconColor | undefined;
projectCwd: string | null;
projectFaviconPath: string | null;
projectTitle: string | null;
Expand Down Expand Up @@ -899,6 +906,11 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {

const isRemote =
props.currentEnvironmentId !== null && thread.environmentId !== props.currentEnvironmentId;
const environmentIconColor = resolveEnvironmentIconColor(
props.configuredEnvironmentIconColor,
props.environmentKnown,
);
const environmentIconKind = isRemote ? "server" : "monitor";
const cleanupBlockerTitle =
cleanup?.status === "queued"
? (readThreadShell(scopeThreadRef(thread.environmentId, cleanup.blockedByThreadId))?.title ??
Expand All @@ -912,6 +924,8 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
projectCwd={props.projectCwd}
projectFaviconPath={props.projectFaviconPath}
environmentLabel={props.environmentLabel}
environmentIconKind={environmentIconKind}
environmentIconColor={environmentIconColor}
providerEntry={providerEntry}
showInstanceBadge={showInstanceBadge}
modelInstanceId={modelInstanceId}
Expand Down Expand Up @@ -1610,9 +1624,15 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
aria-hidden
className="pointer-events-none ml-auto inline-flex shrink-0 items-center gap-1"
>
{isRemote ? (
<span className="inline-flex shrink-0 items-center text-sidebar-muted-foreground/70">
<ServerIcon aria-hidden className="size-3.5" />
{showV2ThreadCardEnvironmentIcon(!isRemote, props.showLocalEnvironmentIcon) ? (
<span className="inline-flex shrink-0 items-center">
<EnvironmentIcon
aria-hidden
kind={environmentIconKind}
context="v2-row"
color={environmentIconColor}
className="size-3.5"
/>
</span>
) : null}
{driverKind ? (
Expand Down Expand Up @@ -1663,6 +1683,8 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
projectFaviconPath: string | null;
projectTitle: string | null;
environmentLabel: string | null;
environmentKnown: boolean;
configuredEnvironmentIconColor: EnvironmentIconColor | undefined;
providerEntryByInstanceId: ReadonlyMap<string, ProviderInstanceEntry>;
isHighlighted: boolean;
isRouteActive: boolean;
Expand All @@ -1671,6 +1693,11 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
onSelect: () => void;
}) {
const { thread } = props;
const primaryEnvironmentId = usePrimaryEnvironmentId();
const environmentIconColor = resolveEnvironmentIconColor(
props.configuredEnvironmentIconColor,
props.environmentKnown,
);
// Same details tooltip as the regular rows: a search hit is still a thread,
// and the hover card is how you disambiguate identically-titled results.
const gitCwd = thread.worktreePath ?? props.projectCwd;
Expand Down Expand Up @@ -1750,6 +1777,8 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
projectCwd={props.projectCwd}
projectFaviconPath={props.projectFaviconPath}
environmentLabel={props.environmentLabel}
environmentIconKind={thread.environmentId === primaryEnvironmentId ? "monitor" : "server"}
environmentIconColor={environmentIconColor}
providerEntry={providerEntry}
showInstanceBadge={showInstanceBadge}
modelInstanceId={modelInstanceId}
Expand Down Expand Up @@ -1777,6 +1806,8 @@ export default function Sidebar() {
const sidebarProjectSortOrder = useClientSettings((s) => s.sidebarProjectSortOrder);
const timestampFormat = useClientSettings((s) => s.timestampFormat);
const projectGroupingSettings = useClientSettings(selectProjectGroupingSettings);
const environmentIconColors = useClientSettings((s) => s.environmentIconColors);
const showLocalEnvironmentIcon = useClientSettings((s) => s.showLocalEnvironmentIcon);
const {
settleThread,
unsettleThread,
Expand Down Expand Up @@ -3725,6 +3756,8 @@ export default function Sidebar() {
) ?? null
}
environmentLabel={environmentLabelById.get(thread.environmentId) ?? null}
environmentKnown={environmentLabelById.has(thread.environmentId)}
configuredEnvironmentIconColor={environmentIconColors[thread.environmentId]}
providerEntryByInstanceId={
providerEntriesByEnvironment.get(thread.environmentId) ??
EMPTY_PROVIDER_ENTRIES
Expand Down Expand Up @@ -3824,6 +3857,9 @@ export default function Sidebar() {
jumpLabel={showJumpHints ? (jumpLabelByKey.get(threadKey) ?? null) : null}
currentEnvironmentId={primaryEnvironmentId}
environmentLabel={environmentLabelById.get(thread.environmentId) ?? null}
environmentKnown={environmentLabelById.has(thread.environmentId)}
showLocalEnvironmentIcon={showLocalEnvironmentIcon}
configuredEnvironmentIconColor={environmentIconColors[thread.environmentId]}
projectCwd={
projectCwdByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null
}
Expand Down
92 changes: 92 additions & 0 deletions apps/web/src/components/settings/LastCodeSettings.logic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {
BearerConnectionTarget,
PrimaryConnectionTarget,
RelayConnectionTarget,
SshConnectionTarget,
type ConnectionCatalogEntry,
} from "@t3tools/client-runtime/connection";
import { EnvironmentId } from "@t3tools/contracts";
import * as Option from "effect/Option";
import { describe, expect, it } from "vite-plus/test";

import { desktopLocalConnectionId } from "../../connection/desktopLocal";
import { deriveLastCodeEnvironmentSettingEntries } from "./LastCodeSettings.logic";

const primary = EnvironmentId.make("primary");
const buildbox = EnvironmentId.make("buildbox");
const production = EnvironmentId.make("production");
const ssh = EnvironmentId.make("ssh");
const wsl = EnvironmentId.make("wsl");

function entry(target: ConnectionCatalogEntry["target"]): ConnectionCatalogEntry {
return { target, profile: Option.none() };
}

describe("deriveLastCodeEnvironmentSettingEntries", () => {
it("lists local first and includes every saved remote in catalog order", () => {
const entries = new Map<EnvironmentId, ConnectionCatalogEntry>([
[
production,
entry(new RelayConnectionTarget({ environmentId: production, label: "Production" })),
],
[
primary,
entry(
new PrimaryConnectionTarget({
environmentId: primary,
label: "Airy",
httpBaseUrl: "http://localhost",
wsBaseUrl: "ws://localhost/ws",
}),
),
],
[
buildbox,
entry(
new BearerConnectionTarget({
environmentId: buildbox,
label: "Buildbox",
connectionId: "buildbox",
}),
),
],
[
ssh,
entry(
new SshConnectionTarget({
environmentId: ssh,
label: "SSH Lab",
connectionId: "ssh-lab",
}),
),
],
]);

expect(
deriveLastCodeEnvironmentSettingEntries({ entries, primaryEnvironmentId: primary }),
).toEqual([
{ environmentId: primary, kind: "local", label: "Airy (local)" },
{ environmentId: production, kind: "remote", label: "Production" },
{ environmentId: buildbox, kind: "remote", label: "Buildbox" },
{ environmentId: ssh, kind: "remote", label: "SSH Lab" },
]);
});

it("omits desktop-local secondary environments", () => {
const entries = new Map<EnvironmentId, ConnectionCatalogEntry>([
[
wsl,
entry(
new BearerConnectionTarget({
environmentId: wsl,
label: "Ubuntu",
connectionId: desktopLocalConnectionId("wsl:ubuntu"),
}),
),
],
]);
expect(
deriveLastCodeEnvironmentSettingEntries({ entries, primaryEnvironmentId: primary }),
).toEqual([{ environmentId: primary, kind: "local", label: "Local (local)" }]);
});
});
42 changes: 42 additions & 0 deletions apps/web/src/components/settings/LastCodeSettings.logic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { ConnectionCatalogEntry } from "@t3tools/client-runtime/connection";
import type { EnvironmentId } from "@t3tools/contracts";

import { isDesktopLocalConnectionTarget } from "../../connection/desktopLocal";
import { formatLocalEnvironmentLabel } from "../../environmentIcons";

export interface LastCodeEnvironmentSettingEntry {
readonly environmentId: EnvironmentId;
readonly kind: "local" | "remote";
readonly label: string;
}

export function deriveLastCodeEnvironmentSettingEntries(input: {
readonly entries: ReadonlyMap<EnvironmentId, ConnectionCatalogEntry>;
readonly primaryEnvironmentId: EnvironmentId | null;
}): LastCodeEnvironmentSettingEntry[] {
const primaryEntry =
input.primaryEnvironmentId === null ? undefined : input.entries.get(input.primaryEnvironmentId);
const local =
input.primaryEnvironmentId === null
? []
: [
{
environmentId: input.primaryEnvironmentId,
kind: "local" as const,
label: formatLocalEnvironmentLabel(primaryEntry?.target.label),
},
];
const remotes = [...input.entries.entries()].flatMap(
([environmentId, entry]): LastCodeEnvironmentSettingEntry[] => {
if (
environmentId === input.primaryEnvironmentId ||
entry.target._tag === "PrimaryConnectionTarget" ||
isDesktopLocalConnectionTarget(entry.target)
) {
return [];
}
return [{ environmentId, kind: "remote", label: entry.target.label }];
},
);
return [...local, ...remotes];
}
Loading