Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export function useConnectionDiagnosis(
const relayOnly =
connectionRoutes(presentation.entry).length === 1 && hasRelayRoute(presentation.entry);
const diagnosis = presentConnectionDiagnosis({
hostLabel: presentation.entry.target.label,
hostLabel: presentation.label,
state,
via: relayOnly ? "relay" : "direct",
clientVersion: Constants.expoConfig?.version,
Expand Down
11 changes: 7 additions & 4 deletions apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,18 @@ export function ConnectionEnvironmentRow(props: {
updates: { readonly label: string; readonly displayUrl: string },
) => Promise<AtomCommandResult<unknown, unknown>>;
}) {
const [label, setLabel] = useState(props.environment.environmentLabel);
const presentation = useAtomValue(
environmentPresentations.presentationAtom(props.environment.environmentId),
);
// Edit the saved label; the displayed one may be the environment's own custom name.
const [label, setLabel] = useState(
presentation?.entry.target.label ?? props.environment.environmentLabel,
);
const [url, setUrl] = useState(props.environment.displayUrl);
const serverConfig = useAtomValue(
serverEnvironment.configValueAtom(props.environment.environmentId),
);
const unsupported = props.environment.connectionState === "unsupported";
const presentation = useAtomValue(
environmentPresentations.presentationAtom(props.environment.environmentId),
);
const canRetry = presentation !== null && hasSavedBearerRoute(presentation.entry);
const { diagnosis, state: connectionState } = useConnectionDiagnosis(
props.environment.environmentId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Pressable, View } from "react-native";
import { AppText as Text } from "../../components/ui/AppText";
import { SymbolView } from "../../components/ui/AppSymbol";
import { environmentCatalog } from "../../connection/catalog";
import { environmentPresentations } from "../../state/presentation";
import { useAtomCommand } from "../../state/use-atom-command";
import { SettingsSection } from "../settings/components/SettingsSection";

Expand All @@ -38,6 +39,7 @@ const options: ReadonlyArray<{
export function PullRequestRoutingSettings() {
const catalog = useAtomValue(environmentCatalog.catalogValueAtom);
const permissions = useAtomValue(environmentCatalog.pullRequestRoutingPermissionsValueAtom);
const presentations = useAtomValue(environmentPresentations.presentationsAtom);
const update = useAtomCommand(environmentCatalog.setPullRequestRoutingPermission);
const [expanded, setExpanded] = useState<string | null>(null);
const [saving, setSaving] = useState(false);
Expand All @@ -48,22 +50,21 @@ export function PullRequestRoutingSettings() {
<SettingsSection title="Pull request routing">
{[...catalog.entries.values()].map((entry) => {
const environmentId = entry.target.environmentId;
const label = presentations.get(environmentId)?.label ?? entry.target.label;
const selected = pullRequestRoutingPermissionFor(entry, permissions);
const disabled =
!catalog.isReady || saving || pullRequestRoutingConnectionKey(entry) === null;
return (
<View key={environmentId}>
<Pressable
accessibilityRole="button"
accessibilityLabel={`${entry.target.label} pull request routing`}
accessibilityLabel={`${label} pull request routing`}
accessibilityState={{ expanded: expanded === environmentId }}
className="flex-row items-center gap-3 p-4"
onPress={() => setExpanded(expanded === environmentId ? null : environmentId)}
>
<View className="min-w-0 flex-1 gap-0.5">
<Text className="text-base font-t3-bold text-foreground">
{entry.target.label}
</Text>
<Text className="text-base font-t3-bold text-foreground">{label}</Text>
<Text className="text-xs text-foreground-muted" numberOfLines={1}>
{connectionCatalogDisplayUrl(entry) ?? "T3 Connect"}
</Text>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function WorkspaceFilePreviewError(props: {
return (
<View className="flex-1 bg-sheet">
<EnvironmentConnectionNotice
environmentLabel={environment.presentation?.entry.target.label ?? "Environment"}
environmentLabel={environment.presentation?.label ?? "Environment"}
connection={
environment.presentation?.connection ?? {
phase: "available",
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/features/review/ReviewSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,7 @@ export function ReviewSheet(props: ReviewSheetProps) {
{showConnectionNotice ? (
<View className="flex-1" style={{ paddingTop: topContentInset }}>
<EnvironmentConnectionNotice
environmentLabel={environment.presentation?.entry.target.label ?? "Environment"}
environmentLabel={environment.presentation?.label ?? "Environment"}
connection={
environment.presentation?.connection ?? {
phase: "available",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -890,7 +890,7 @@ function ThreadTerminal(props: {
{!isEnvironmentReady ? (
<EnvironmentConnectionNotice
environmentLabel={
environment.presentation?.entry.target.label ??
environment.presentation?.label ??
selectedEnvironmentConnection?.environmentLabel ??
"Environment"
}
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/features/usage/UsageLimitsSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,7 @@ export function useRefreshLimits(
);
const failedLabels = selected
.filter(([environmentId]) => failedIds.has(environmentId))
.map(([, presentation]) => presentation.entry.target.label);
.map(([, presentation]) => presentation.label);

const refresh = (afterPending: boolean) =>
Promise.all(
Expand Down
2 changes: 0 additions & 2 deletions apps/mobile/src/state/environments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import { useEnvironmentQuery } from "./query";

export interface EnvironmentPresentation extends BaseEnvironmentPresentation {
readonly environmentId: EnvironmentId;
readonly label: string;
readonly displayUrl: string | null;
readonly relayManaged: boolean;
}
Expand All @@ -25,7 +24,6 @@ export function projectEnvironmentPresentation(
return {
...presentation,
environmentId,
label: presentation.entry.target.label,
displayUrl: connectionCatalogDisplayUrl(presentation.entry),
relayManaged: hasRelayRoute(presentation.entry),
};
Expand Down
4 changes: 3 additions & 1 deletion apps/mobile/src/state/remote-environment-projections.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ function presentation(
endpoint: string = environmentId,
serverConfig: ServerConfig | null = null,
): EnvironmentPresentation {
const entry = { target: target(environmentId, endpoint), profile: Option.none(), enabled: true };
return {
entry: { target: target(environmentId, endpoint), profile: Option.none(), enabled: true },
entry,
label: entry.target.label,
connection: { phase: "connected", error: null, traceId: null },
serverConfig,
};
Expand Down
6 changes: 5 additions & 1 deletion apps/mobile/src/state/remote-environment-projections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@ export function createRemoteEnvironmentProjectionAtoms(input: {
}) {
const savedConnectionAtom = Atom.family((environmentId: EnvironmentId) => {
let previousEntry: EnvironmentPresentation["entry"] | null = null;
let previousLabel: string | null = null;
let previousPrepared: PreparedConnection | null = null;
let previous: SavedRemoteConnection | null = null;

return Atom.make((get) => {
const presentation = get(input.presentationAtom(environmentId));
if (presentation === null) {
previousEntry = null;
previousLabel = null;
previousPrepared = null;
previous = null;
return null;
Expand All @@ -37,6 +39,7 @@ export function createRemoteEnvironmentProjectionAtoms(input: {
if (
previous !== null &&
presentation.entry === previousEntry &&
presentation.label === previousLabel &&
prepared === previousPrepared
) {
return previous;
Expand All @@ -58,10 +61,11 @@ export function createRemoteEnvironmentProjectionAtoms(input: {
: presentation.entry.target._tag === "RelayConnectionTarget";

previousEntry = presentation.entry;
previousLabel = presentation.label;
previousPrepared = prepared;
previous = {
environmentId,
environmentLabel: presentation.entry.target.label,
environmentLabel: presentation.label,
pairingUrl: displayUrl,
displayUrl,
httpBaseUrl,
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/state/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const usageByWindowAtom = Atom.family((windowKey: string) =>
if (!access.canReadDiagnostics) {
statuses.push({
environmentId,
label: presentation.entry.target.label,
label: presentation.label,
isConnected: presentation.connection.phase === "connected",
...access,
summary: null,
Expand All @@ -57,7 +57,7 @@ const usageByWindowAtom = Atom.family((windowKey: string) =>
const summary = Option.getOrNull(AsyncResult.value(result));
statuses.push({
environmentId,
label: presentation.entry.target.label,
label: presentation.label,
isPending: result.waiting,
isConnected: presentation.connection.phase === "connected",
canReadDiagnostics: true,
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/state/workspace-connection-atoms.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ function presentation(): EnvironmentPresentation {
enabled: true,
profile: Option.none(),
},
label: "Mac",
connection: { phase: "connected", error: null, traceId: null },
serverConfig: null,
};
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ export const make = Effect.gen(function* () {
serverResolvedCommandContext: true,
threadStop: true,
environmentIcon: true,
environmentLabel: true,
projectCloneTracking: true,
ignoredFileListing: true,
...(serverSelfUpdate === null ? {} : { serverSelfUpdate }),
Expand Down
45 changes: 45 additions & 0 deletions apps/server/src/mcp/toolkits/environment/handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,48 @@ it.effect("refuses a preferences update when the caller's turn ends while it wai
);
}),
);

it.effect("reads the environment by its custom name", () =>
Effect.gen(function* () {
const layerDependencies = Layer.mergeAll(
ThreadCommandExecutor.layer,
Layer.succeed(McpInvocationContext.McpInvocationContext, {
environmentId,
requestNamespace: "provider:preferences",
thread: undefined,
client: undefined,
capabilities: new Set(["orchestration" as const]),
issuedAt: 0,
}),
Layer.mock(ThreadManagement.ThreadManagementService)({}),
Layer.mock(Environment.ServerEnvironment)({
getDescriptor: Effect.succeed({
environmentId,
label: "devbox",
platform: { os: "linux", arch: "x64" },
serverVersion: "0.0.0",
capabilities: { repositoryIdentity: false },
}),
}),
Layer.mock(Settings.ServerSettingsService)({
getSettings: Effect.succeed({
...DEFAULT_SERVER_SETTINGS,
environmentLabel: "MacBook Pro · user2",
}),
}),
);
const result = yield* Effect.gen(function* () {
const toolkit = yield* EnvironmentToolkit;
return yield* toolkit
.handle("t3_environment_read", {})
.pipe(Stream.unwrap, Stream.runCollect);
}).pipe(
Effect.provide(
McpToolAccess.HandlersLayer.layer(EnvironmentHandlers.layer).pipe(
Layer.provideMerge(layerDependencies),
),
),
);
expect(result.at(-1)?.result).toMatchObject({ label: "MacBook Pro · user2" });
}),
);
2 changes: 1 addition & 1 deletion apps/server/src/mcp/toolkits/environment/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export const layer = McpToolAccess.toLayer(EnvironmentToolkit, {
const current = yield* settings.getSettings.pipe(Effect.mapError(unavailable));
return {
environmentId: descriptor.environmentId,
label: descriptor.label,
label: current.environmentLabel ?? descriptor.label,
serverVersion: descriptor.serverVersion,
platform: descriptor.platform,
preferences: preferences(current),
Expand Down
3 changes: 1 addition & 2 deletions apps/web/src/components/chat/useComposerSend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,8 +545,7 @@ export function useComposerSend(input: ComposerSendInput) {
})[0];
if (strandedThread) {
const owner =
readEnvironmentPresentation(strandedThread.environmentId)?.entry.target.label ??
"its machine";
readEnvironmentPresentation(strandedThread.environmentId)?.label ?? "its machine";
toastManager.add(
stackedThreadToast({
type: "warning",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function ChatGptWelcomeCoordinator() {
provider.instanceId,
provider.auth.profileId ?? provider.auth.email ?? "default",
]),
environmentLabel: presentation.entry.target.label,
environmentLabel: presentation.label,
providerName: provider.displayName ?? "Codex",
})),
);
Expand Down
57 changes: 36 additions & 21 deletions apps/web/src/components/settings/ConnectionsSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { SessionPermissions } from "./SessionPermissions";
import { ChevronRightIcon, EllipsisIcon } from "lucide-react";
import { ChevronRightIcon, EllipsisIcon, PencilIcon } from "lucide-react";
import { useAtomValue } from "@effect/atom-react";
import { useState } from "react";
import {
Expand Down Expand Up @@ -35,6 +35,7 @@ import { SettingsPageContainer, SettingsRow, SettingsSection } from "./settingsL
import { LocalEnvironmentSetting } from "./LocalEnvironmentSetting";
import { searchableSetting } from "./settingsSearch";
import { EnvironmentIconMenu } from "./EnvironmentIconPicker";
import { EnvironmentRenameDialog } from "./EnvironmentRenameDialog";
import { FoldedSettingsSection } from "./FoldedSettingsSection";
import { LoadBalancingSettings } from "./LoadBalancingSettings";
import { PullRequestRoutingSettings } from "./PullRequestRoutingSettings";
Expand All @@ -61,7 +62,7 @@ import {
} from "../ui/alert-dialog";
import { Spinner } from "../ui/spinner";
import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select";
import { Menu, MenuPopup, MenuTrigger } from "../ui/menu";
import { Menu, MenuItem, MenuPopup, MenuTrigger } from "../ui/menu";
import { Switch } from "../ui/switch";
import { stackedThreadToast, toastManager } from "../ui/toast";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
Expand Down Expand Up @@ -235,6 +236,7 @@ export function ConnectionsSettings() {
>(null);
const [isRevokingOtherDesktopClients, setIsRevokingOtherDesktopClients] = useState(false);
const [addBackendDialogOpen, setAddBackendDialogOpen] = useState(false);
const [renamePrimaryOpen, setRenamePrimaryOpen] = useState(false);
const [routeTarget, setRouteTarget] = useState<EnvironmentPresentation | null>(null);
const [savedBackendError, setSavedBackendError] = useState<string | null>(null);
const handleAddBackendDialogOpenChange = (open: boolean) => {
Expand Down Expand Up @@ -1046,26 +1048,39 @@ export function ConnectionsSettings() {
}
headerAction={
primaryEnvironmentId !== null ? (
<Menu>
<MenuTrigger
render={
<Button
type="button"
variant="ghost-muted"
size="icon-xs"
aria-label="More actions for this machine"
<>
<Menu>
<MenuTrigger
render={
<Button
type="button"
variant="ghost-muted"
size="icon-xs"
aria-label="More actions for this machine"
/>
}
>
<EllipsisIcon className="size-3.5" />
</MenuTrigger>
<MenuPopup align="end">
<EnvironmentIconMenu
environmentId={primaryEnvironmentId}
serverConfig={primaryServerConfig}
/>
}
>
<EllipsisIcon className="size-3.5" />
</MenuTrigger>
<MenuPopup align="end">
<EnvironmentIconMenu
environmentId={primaryEnvironmentId}
serverConfig={primaryServerConfig}
/>
</MenuPopup>
</Menu>
<MenuItem onClick={() => setRenamePrimaryOpen(true)}>
<PencilIcon />
Rename…
</MenuItem>
</MenuPopup>
</Menu>
<EnvironmentRenameDialog
environmentId={primaryEnvironmentId}
serverConfig={primaryServerConfig}
label={primaryEnvironment?.label ?? ""}
open={renamePrimaryOpen}
onOpenChange={setRenamePrimaryOpen}
/>
</>
) : null
}
>
Expand Down
Loading
Loading