# Files changed · PR \#16731 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/16731/diffs)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

[feat\(server\): t3_thread_import, conversation only](https://git.cafe/pingdotgg/t3code/pull/16731.md)

Version: 14

Head commit: 48dea790a657af09a50d188a5092ada32bab8542

Base commit: a0c623abd846b5bfc99a23234dda490b7b949862

Comparison base: 5d27ecd79c33a642efcdf259e89e3eba5a560247

Showing up to 20 entries from this revision. If the pull request changes, reopen the first page before continuing.

## apps/mobile/modules/t3-composer-editor/android/src/main/java/expo/modules/t3composereditor/T3ContextChip.kt

Status: modified; +7; −0

```diff
@@ -160,6 +160,13 @@
           line(7f, 8f, 17f, 8f)
           line(7f, 12f, 14f, 12f)
         }
+        // Two stacked rack units, each with a status light, like the SF Symbol iOS draws.
+        "server.rack" -> {
+          addRoundRect(3f, 3f, 21f, 11f, 2f, 2f, Path.Direction.CW)
+          addRoundRect(3f, 13f, 21f, 21f, 2f, 2f, Path.Direction.CW)
+          line(7f, 7f, 7.01f, 7f)
+          line(7f, 17f, 7.01f, 17f)
+        }
         "terminal", "play.rectangle", "photo" -> {
           addRoundRect(2f, 4f, 22f, 20f, 2f, 2f, Path.Direction.CW)
           when (symbol) {
```

## apps/mobile/src/components/ComposerContextSheet.tsx

Status: modified; +3; −0

```diff
@@ -314,6 +314,9 @@
                 ) : null}
               </View>
             ) : null}
+            {record.kind === "environment" ? (
+              <ContextField label="Machine" value={record.label} />
+            ) : null}
             {record.kind === "skill" ? (
               <View className="gap-3">
                 <ContextField label="Skill" value={record.name} />
```

## apps/mobile/src/features/connection/ConnectionsRouteScreen.tsx

Status: modified; +10; −0

```diff
@@ -11,6 +11,7 @@
 import { useRemoteConnections } from "../../state/use-remote-environment-registry";
 import { LocalEnvironmentList } from "./LocalEnvironmentList";
 import { GitHubRoutingSettings } from "./GitHubRoutingSettings";
+import { LinkedEnvironmentsSection } from "./LinkedEnvironmentsSection";
 
 export function ConnectionsRouteScreen() {
   const {
@@ -69,6 +70,15 @@
           onSetEnabled={onSetEnvironmentEnabled}
           onUpdate={onUpdateEnvironment}
         />
+        {connectedEnvironments
+          .filter((environment) => environment.connectionState === "connected")
+          .map((environment) => (
+            <LinkedEnvironmentsSection
+              key={environment.environmentId}
+              environmentId={environment.environmentId}
+              environmentLabel={environment.environmentLabel}
+            />
+          ))}
         <GitHubRoutingSettings />
       </ScrollView>
     </View>
```

## apps/mobile/src/features/connection/LinkedEnvironmentsSection.tsx

Status: added; +78; −0

```diff
@@ -1,0 +1,78 @@
+import { peerLinkHealth } from "@t3tools/client-runtime/state/peerLinks";
+import { useFocusEffect } from "@react-navigation/native";
+import { AuthAccessReadScope, type EnvironmentId } from "@t3tools/contracts";
+import * as DateTime from "effect/DateTime";
+import { useCallback, useState } from "react";
+import { AppState, View } from "react-native";
+
+import { AppText as Text } from "../../components/ui/AppText";
+import { useEnvironmentServerConfig } from "../../state/entities";
+import { peerLinkEnvironment } from "../../state/peerLinks";
+import { useEnvironmentQuery } from "../../state/query";
+import { useEnvironmentScope } from "../../state/session";
+import { SettingsSection } from "../settings/components/SettingsSection";
+
+export function LinkedEnvironmentsSection(props: {
+  readonly environmentId: EnvironmentId;
+  readonly environmentLabel: string;
+}) {
+  const config = useEnvironmentServerConfig(props.environmentId);
+  const canRead = useEnvironmentScope(props.environmentId, AuthAccessReadScope);
+  const supported = canRead && config?.environment.capabilities.peerLinks === true;
+  const links = useEnvironmentQuery(
+    supported ? peerLinkEnvironment.list({ environmentId: props.environmentId, input: {} }) : null,
+  );
+  const [nowMs, setNowMs] = useState(Date.now);
+  const hasLinks = supported && (links.data?.links.length ?? 0) > 0;
+  useFocusEffect(
+    useCallback(() => {
+      if (!hasLinks) return;
+      const updateNow = () => setNowMs(Date.now());
+      updateNow();
+      const timer = setInterval(updateNow, 60_000);
+      const subscription = AppState.addEventListener("change", (state) => {
+        if (state === "active") updateNow();
+      });
+      return () => {
+        clearInterval(timer);
+        subscription.remove();
+      };
+    }, [hasLinks]),
+  );
+  if (!supported || links.error || !links.data || links.data.links.length === 0) return null;
+  const now = DateTime.makeUnsafe(nowMs);
+  return (
+    <View className="mt-5 gap-3">
+      <SettingsSection title={`Linked from ${props.environmentLabel}`}>
+        {links.data.links.map((link) => {
+          const health = peerLinkHealth(link, now);
+          const status =
+            health.kind === "expired"
+              ? "Expired. Link it again with a new pairing code."
+              : health.kind === "unreachable"
+                ? (health.detail ?? "Not answering right now.")
+                : health.kind === "failing"
+                  ? health.detail
+                  : health.expiresInDays !== null
+                    ? `Expires in ${health.expiresInDays} ${health.expiresInDays === 1 ? "day" : "days"}`
+                    : "Reachable";
+          return (
+            <View key={link.environmentId} className="gap-0.5 p-4">
+              <Text className="text-base font-t3-bold text-foreground">{link.label}</Text>
+              <Text
+                className={
+                  health.kind === "reachable" && health.expiresInDays === null
+                    ? "text-xs text-foreground-muted"
+                    : "text-xs text-danger-foreground"
+                }
+                numberOfLines={2}
+              >
+                {status}
+              </Text>
+            </View>
+          );
+        })}
+      </SettingsSection>
+    </View>
+  );
+}
```

## apps/mobile/src/features/home/useThreadListActions.test.ts

Status: modified; +4; −1

```diff
@@ -109,7 +109,10 @@
   },
 }));
 vi.mock("../../state/threads", () => ({
+  environmentThreadShells: {
+    threadShellsAtom: "thread-shells",
+    navigationThreadShellsAtom: "thread-shells",
-  environmentThreadShells: { threadShellsAtom: "thread-shells" },
+  },
   threadEnvironment: Object.fromEntries(
     [
       "archive",
```

## apps/mobile/src/features/home/useThreadListActions.ts

Status: modified; +1; −1

```diff
@@ -517,7 +517,7 @@
   const moveThread = async (thread: EnvironmentThreadShell, direction: ThreadMoveDestination) => {
     if (!checkThreadOperationPermission(thread, "Could not move thread")) return false;
     if (getPendingThreadOrder() !== null || appAtomRegistry.get(threadDropBusyAtom)) return false;
-    const shells = appAtomRegistry.get(environmentThreadShells.threadShellsAtom);
+    const shells = appAtomRegistry.get(environmentThreadShells.navigationThreadShellsAtom);
     const current = shells.find(
       (row) => row.id === thread.id && row.environmentId === thread.environmentId,
     );
```

## apps/mobile/src/features/threads/ComposerCommandPopover.tsx

Status: modified; +13; −0

```diff
@@ -3,6 +3,8 @@
   type ProviderSkillSourceKind,
 } from "@t3tools/client-runtime/providerSkills";
 import type {
+  EnvironmentId,
+  EnvironmentMachineKind,
   PullRequestContextMetadata,
   ScopedThreadRef,
   ServerProviderSkill,
@@ -14,6 +16,7 @@
 
 import { SymbolView, type AppSymbolName } from "../../components/ui/AppSymbol";
 import { AppText as Text } from "../../components/ui/AppText";
+import { ENVIRONMENT_MACHINE_SYMBOLS } from "../../components/EnvironmentMachineSymbol";
 import { GlassSurface } from "../../components/ui/GlassSurface";
 import { PierreEntryIcon } from "../../components/PierreEntryIcon";
 import { pullRequestStatus } from "../pull-requests/pullRequestStatus";
@@ -42,6 +45,14 @@
     }
   | {
       readonly id: string;
+      readonly type: "environment";
+      readonly environmentId: EnvironmentId;
+      readonly machine: EnvironmentMachineKind;
+      readonly label: string;
+      readonly description: string;
+    }
+  | {
+      readonly id: string;
       readonly type: "slash-command";
       readonly command: string;
       readonly label: string;
@@ -110,6 +121,8 @@
       return null;
     case "thread":
       return "text.bubble";
+    case "environment":
+      return ENVIRONMENT_MACHINE_SYMBOLS[item.machine];
   }
 }
 
```

## apps/mobile/src/features/threads/LinkRequestCard.tsx

Status: added; +514; −0

```diff
@@ -1,0 +1,514 @@
+import { useAtomValue } from "@effect/atom-react";
+import {
+  type LinkableMachine,
+  linkRequestAnswerInput,
+  linkRequestDefaultAccess,
+  linkRequestDisplay,
+  linkRequestFailureMessage,
+  linkRequestInitialMachine,
+  type LinkRequestItem,
+  linkRequestTargetName,
+  rankMachinesByHint,
+} from "@t3tools/client-runtime/link-request";
+import {
+  isAtomCommandInterrupted,
+  squashAtomCommandFailure,
+} from "@t3tools/client-runtime/state/runtime";
+import {
+  AuthAccessWriteScope,
+  type AuthMcpClientAccess,
+  type EnvironmentId,
+  type OrchestrationV2ProjectedTurnItem,
+} from "@t3tools/contracts";
+import * as Option from "effect/Option";
+import * as Result from "effect/Result";
+import { useMemo, useRef, useState } from "react";
+import { Modal, Platform, Pressable, ScrollView, View, type ColorValue } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+
+import { SymbolView, type AppSymbolName } from "../../components/ui/AppSymbol";
+import { AppText as Text, AppTextInput as TextInput } from "../../components/ui/AppText";
+import { EnvironmentMachineSymbol } from "../../components/EnvironmentMachineSymbol";
+import { useLinkableMachines } from "../../state/peerLinks";
+import { mintPairingCodeOn } from "../../state/peerPairingCode";
+import { serverEnvironment } from "../../state/server";
+import { useEnvironmentScope, usePreparedConnection } from "../../state/session";
+import { useAtomCommand } from "../../state/use-atom-command";
+import { SettingsChoiceRow } from "../settings/components/SettingsChoiceRow";
+import { SettingsSection } from "../settings/components/SettingsSection";
+import { Button } from "../../components/ui/Button";
+import { RUNTIME_MODE_CHOICES } from "./thread-settings-options";
+
+/** The same five levels as web's access picker: read-only, then the runtime modes. */
+const ACCESS_CHOICES: ReadonlyArray<{
+  readonly access: AuthMcpClientAccess;
+  readonly label: string;
+  readonly description: string;
+}> = [
+  {
+    access: "read-only",
+    label: "Read only",
+    description: "Read projects and threads. Cannot start, message or change anything.",
+  },
+  ...RUNTIME_MODE_CHOICES.map((choice) => ({ ...choice, access: choice.mode })),
+];
+const accessLabel = (access: AuthMcpClientAccess) =>
+  ACCESS_CHOICES.find((choice) => choice.access === access)?.label ?? access;
+
+const LINK_SYMBOL: AppSymbolName = { ios: "link", android: "desktop_windows" };
+
+/** The machine list's row for linking by an address typed in. */
+const ANOTHER_ADDRESS = "another-address";
+
+/** Whether the agent left the machine for the user to pick. */
+const picksMachine = (item: LinkRequestItem) =>
+  item.environmentId === undefined && item.url === undefined;
+
+/**
+ * Feed card for a link an agent asked the user to make. The user picks the
+ * access in a sheet; the pairing code lives only in that sheet's state and
+ * the RPC payload, and is never logged, alerted, or persisted.
+ */
+export function LinkRequestCard(props: {
+  readonly environmentId: EnvironmentId;
+  readonly projectedItem: OrchestrationV2ProjectedTurnItem;
+  readonly iconColor: ColorValue;
+}) {
+  const { item, visibility } = props.projectedItem;
+  if (item.type !== "link_request") return null;
+  const display = linkRequestDisplay(item, visibility, accessLabel);
+  if (display.kind === "pending") {
+    return <PendingLinkRequest environmentId={props.environmentId} item={item} />;
+  }
+  const icon: AppSymbolName =
+    display.kind === "pending-elsewhere"
+      ? LINK_SYMBOL
+      : display.outcome === "linked"
+        ? "checkmark"
+        : display.outcome === "failed"
+          ? "xmark"
+          : "minus";
+  return (
+    <View className="mb-3 min-h-9 flex-row items-center gap-2 px-1">
+      <SymbolView name={icon} size={13} tintColor={props.iconColor} type="monochrome" />
+      <Text className="flex-1 font-sans text-sm text-foreground-muted" numberOfLines={2}>
+        {display.label}
+      </Text>
+    </View>
+  );
+}
+
+function PendingLinkRequest(props: {
+  readonly environmentId: EnvironmentId;
+  readonly item: LinkRequestItem;
+}) {
+  const { item } = props;
+  const [open, setOpen] = useState(false);
+  const [declining, setDeclining] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const answer = useAtomCommand(serverEnvironment.answerLinkRequest, {
+    label: "decline link request",
+    reportFailure: false,
+    reportDefect: false,
+  });
+  const canAnswer = useAtomValue(
+    serverEnvironment.answerLinkRequest.permissionAtom(props.environmentId),
+  );
+  const target = linkRequestTargetName(item);
+
+  const decline = async () => {
+    const input = linkRequestAnswerInput(item, { type: "decline" });
+    if (input === null) return;
+    setDeclining(true);
+    setError(null);
+    const result = await answer({ environmentId: props.environmentId, input }).finally(() =>
+      setDeclining(false),
+    );
+    if (result._tag === "Failure" && !isAtomCommandInterrupted(result)) {
+      setError(linkRequestFailureMessage(squashAtomCommandFailure(result)));
+    }
+  };
+
+  return (
+    <View className="mb-3 gap-3 rounded-[20px] border border-border bg-card-alt p-4">
+      <View className="gap-1">
+        <Text className="font-t3-bold text-base text-foreground">
+          {picksMachine(item)
+            ? "Link a machine to this environment"
+            : `Link ${target} to this environment`}
+        </Text>
+        {item.reason.trim() ? (
+          <Text className="font-sans text-sm leading-5 text-foreground-muted">{item.reason}</Text>
+        ) : null}
+        {item.label !== undefined && item.url !== undefined ? (
+          <Text className="font-sans text-xs text-foreground-muted" numberOfLines={1}>
+            {item.url}
+          </Text>
+        ) : null}
+      </View>
+      {error !== null ? (
+        <Text
+          accessibilityRole="alert"
+          accessibilityLiveRegion="polite"
+          className="font-sans text-sm text-danger-foreground"
+        >
+          {error}
+        </Text>
+      ) : null}
+      {canAnswer ? (
+        <>
+          <Button
+            label={picksMachine(item) ? "Choose machine…" : "Review link…"}
+            disabled={declining}
+            onPress={() => setOpen(true)}
+            tone="primary"
+            fullWidth
+          />
+          <View className="flex-row justify-end">
+            <Pressable
+              accessibilityRole="button"
+              accessibilityState={{ disabled: declining }}
+              disabled={declining}
+              hitSlop={8}
+              className="px-1 py-1 active:opacity-60 disabled:opacity-50"
+              onPress={() => void decline()}
+            >
+              <Text className="font-sans text-xs text-foreground-muted">Decline</Text>
+            </Pressable>
+          </View>
+        </>
+      ) : (
+        <Text className="font-sans text-xs text-foreground-muted">
+          Only a session that manages access here can answer this.
+        </Text>
+      )}
+      {open ? (
+        <LinkRequestSheet
+          environmentId={props.environmentId}
+          item={item}
+          onClose={() => setOpen(false)}
+        />
+      ) : null}
+    </View>
+  );
+}
+
+/** What the sheet links; see web's LinkRequestDialog for the same rules. */
+type SheetTarget =
+  | { readonly kind: "none" }
+  | {
+      readonly kind: "machine";
+      readonly environmentId: EnvironmentId;
+      readonly label: string;
+      readonly urls: ReadonlyArray<string>;
+      readonly linked: boolean;
+    }
+  | { readonly kind: "address" };
+
+function resolveTarget(
+  item: LinkRequestItem,
+  machines: ReadonlyArray<LinkableMachine>,
+  choice: string | null,
+): SheetTarget {
+  if (item.environmentId === undefined && item.url !== undefined) return { kind: "address" };
+  const chosen = item.environmentId ?? choice;
+  if (chosen === null) return { kind: "none" };
+  if (chosen === ANOTHER_ADDRESS) return { kind: "address" };
+  const machine = machines.find((candidate) => candidate.environmentId === chosen);
+  return {
+    kind: "machine",
+    environmentId: machine?.environmentId ?? (chosen as EnvironmentId),
+    label: machine?.label ?? linkRequestTargetName(item),
+    urls: [...new Set([...(machine?.urls ?? []), ...(item.url === undefined ? [] : [item.url])])],
+    linked: machine?.linked ?? false,
+  };
+}
+
+function LinkRequestSheet(props: {
+  readonly environmentId: EnvironmentId;
+  readonly item: LinkRequestItem;
+  readonly onClose: () => void;
+}) {
+  const { item } = props;
+  const insets = useSafeAreaInsets();
+  const pickMachine = picksMachine(item);
+  const listed = useLinkableMachines(props.environmentId, true);
+  const machines = useMemo(() => rankMachinesByHint(listed, item.hint), [listed, item.hint]);
+  const [choice, setChoice] = useState<string | null>(null);
+  const [address, setAddress] = useState(item.url ?? "");
+  const [access, setAccess] = useState<AuthMcpClientAccess>(() => linkRequestDefaultAccess(item));
+  const [pairingCode, setPairingCode] = useState("");
+  const [busy, setBusy] = useState(false);
+  const [error, setError] = useState<string | null>(null);
+  const [mintFailed, setMintFailed] = useState<ReadonlySet<EnvironmentId>>(new Set());
+  // Submit then a tap can both run before a re-render; this guard is synchronous.
+  const inFlight = useRef(false);
+  const answer = useAtomCommand(serverEnvironment.answerLinkRequest, {
+    label: "answer link request",
+    // The failure cause holds the request, and with it the code; keep it out of the console.
+    reportFailure: false,
+    reportDefect: false,
+  });
+  const selected = choice ?? linkRequestInitialMachine(item, machines);
+  const target = resolveTarget(item, machines, selected);
+  const targetName = target.kind === "machine" ? target.label : linkRequestTargetName(item);
+  // This phone can mint a code there when it is connected to that environment
+  // with a session that may create pairing links.
+  const targetId = target.kind === "machine" ? target.environmentId : null;
+  const connected = usePreparedConnection(targetId);
+  const mayMint = useEnvironmentScope(targetId, AuthAccessWriteScope);
+  const canMint =
+    targetId !== null && Option.isSome(connected) && mayMint && !mintFailed.has(targetId);
+  const needsAddress =
+    target.kind === "address" || (target.kind === "machine" && target.urls.length === 0);
+  const useExisting = target.kind === "machine" && target.linked;
+  const ready =
+    target.kind !== "none" &&
+    (useExisting ||
+      ((!needsAddress || address.trim().length > 0) && (canMint || pairingCode.trim().length > 0)));
+
+  const prepareAnswer = async () => {
+    if (target.kind === "none") return null;
+    if (target.kind === "machine" && target.linked) {
+      return linkRequestAnswerInput(item, {
+        type: "use-existing",
+        environmentId: target.environmentId,
+      });
+    }
+    let code = pairingCode;
+    if (canMint && targetId !== null) {
+      const minted = await mintPairingCodeOn(targetId, access);
+      if (Result.isFailure(minted)) {
+        setMintFailed((failed) => new Set(failed).add(targetId));
+        setError(linkRequestFailureMessage(minted.failure));
+        return null;
+      }
+      code = minted.success;
+    }
+    return linkRequestAnswerInput(item, {
+      type: "link",
+      target:
+        target.kind === "address"
+          ? { url: address }
+          : { ...target, urls: needsAddress ? [address.trim()] : target.urls },
+      access,
+      pairingCode: code,
+    });
+  };
+
+  const submit = async () => {
+    if (inFlight.current || target.kind === "none") return;
+    inFlight.current = true;
+    setBusy(true);
+    setError(null);
+    const input = await prepareAnswer();
+    const result =
+      input === null ? null : await answer({ environmentId: props.environmentId, input });
+    inFlight.current = false;
+    setBusy(false);
+    if (result === null) return;
+    if (result._tag === "Success") {
+      setPairingCode("");
+      props.onClose();
+    } else if (!isAtomCommandInterrupted(result)) {
+      setError(linkRequestFailureMessage(squashAtomCommandFailure(result)));
+    }
+  };
+
+  return (
+    <Modal
+      animationType="slide"
+      presentationStyle={Platform.OS === "ios" ? "pageSheet" : "fullScreen"}
+      // An answer that lands after a close would still link, and would close a reopened sheet.
+      onRequestClose={() => {
+        if (!busy) props.onClose();
+      }}
+    >
+      <ScrollView
+        className="flex-1 bg-sheet-solid"
+        contentContainerClassName="gap-5 p-5"
+        contentContainerStyle={{ paddingBottom: insets.bottom + 20 }}
+        keyboardShouldPersistTaps="handled"
+      >
+        <View className="gap-1">
+          <Text className="font-t3-bold text-lg text-foreground">
+            {pickMachine ? "Link a machine" : `Link ${targetName}`}
+          </Text>
+          <Text className="font-sans text-sm leading-5 text-foreground-muted">
+            Agents here can then launch, message and wait on threads there. It appears in that
+            machine's Connections, where it can be revoked.
+          </Text>
+          {!pickMachine && item.url !== undefined ? (
+            <Text className="font-sans text-xs text-foreground-muted" numberOfLines={1}>
+              {item.url}
+            </Text>
+          ) : null}
+        </View>
+        {pickMachine ? (
+          <SettingsSection title="Machine">
+            {machines.map((machine, index) => (
+              <MachineRow
+                key={machine.environmentId}
+                machine={machine}
+                selected={selected === machine.environmentId}
+                separated={index > 0}
+                disabled={busy}
+                onPress={() => {
+                  setChoice(machine.environmentId);
+                  setError(null);
+                }}
+              />
+            ))}
+            <SettingsChoiceRow
+              label="Another address…"
+              description="Link a machine this app does not know, by its https or Tailscale address."
+              selected={selected === ANOTHER_ADDRESS}
+              separated={machines.length > 0}
+              disabled={busy}
+              onPress={() => {
+                setChoice(ANOTHER_ADDRESS);
+                setError(null);
+              }}
+            />
+          </SettingsSection>
+        ) : null}
+        {needsAddress && !(item.url !== undefined && item.environmentId === undefined) ? (
+          <View className="gap-2">
+            <TextInput
+              accessibilityLabel="Address"
+              placeholder="https://machine.tailnet.ts.net"
+              value={address}
+              onChangeText={setAddress}
+              editable={!busy}
+              autoCorrect={false}
+              autoCapitalize="none"
+              keyboardType="url"
+              spellCheck={false}
+            />
+            {target.kind === "machine" ? (
+              <Text className="font-sans text-xs text-foreground-muted">
+                This app has no address for {target.label} that another machine can reach. Enter its
+                https or Tailscale address.
+              </Text>
+            ) : null}
+          </View>
+        ) : null}
+        {useExisting ? (
+          <Text className="font-sans text-sm leading-5 text-foreground-muted">
+            This environment is already linked to {targetName}. The agent can use that link as it
+            is; change its access in Settings → Connections.
+          </Text>
+        ) : target.kind === "none" ? null : (
+          <>
+            {canMint ? null : (
+              <View className="gap-2">
+                <TextInput
+                  accessibilityLabel="Pairing code"
+                  placeholder="Paste the code"
+                  value={pairingCode}
+                  onChangeText={setPairingCode}
+                  editable={!busy}
+                  autoCorrect={false}
+                  autoCapitalize="none"
+                  autoComplete="one-time-code"
+                  textContentType="oneTimeCode"
+                  spellCheck={false}
+                />
+                <Text className="font-sans text-xs text-foreground-muted">
+                  Create a link on {targetName}: Settings → Connections → Create link, then paste
+                  its code here.
+                </Text>
+              </View>
+            )}
+            <SettingsSection title="What agents here may do there">
+              {ACCESS_CHOICES.map((choice, index) => (
+                <SettingsChoiceRow
+                  key={choice.access}
+                  label={choice.label}
+                  description={choice.description}
+                  selected={choice.access === access}
+                  separated={index > 0}
+                  disabled={busy}
+                  onPress={() => setAccess(choice.access)}
+                />
+              ))}
+            </SettingsSection>
+            <Text className="font-sans text-xs text-foreground-muted">
+              An agent here never gets more there than its own mode here either. Threads it starts
+              there cannot change that environment's own threads, projects or settings.
+            </Text>
+          </>
+        )}
+        {error !== null ? (
+          <Text
+            accessibilityRole="alert"
+            accessibilityLiveRegion="polite"
+            className="font-sans text-sm text-danger-foreground"
+          >
+            {error}
+          </Text>
+        ) : null}
+        <Button
+          label={busy ? "Linking…" : useExisting ? "Use this link" : "Link"}
+          disabled={busy || !ready}
+          onPress={() => void submit()}
+          tone="primary"
+          fullWidth
+        />
+        <Button label="Cancel" tone="secondary" disabled={busy} onPress={props.onClose} fullWidth />
+      </ScrollView>
+    </Modal>
+  );
+}
+
+/** One machine in the sheet's list: its glyph, name, and whether it is linked or reachable. */
+function MachineRow(props: {
+  readonly machine: LinkableMachine;
+  readonly selected: boolean;
+  readonly separated: boolean;
+  readonly disabled: boolean;
+  readonly onPress: () => void;
+}) {
+  const { machine } = props;
+  const status = machine.linked
+    ? "Linked"
+    : machine.connected
+      ? null
+      : machine.connectOnly
+        ? "T3 Connect"
+        : "Not connected";
+  return (
+    <Pressable
+      accessibilityRole="radio"
+      accessibilityState={{ checked: props.selected, disabled: props.disabled }}
+      className={
+        props.separated
+          ? "flex-row items-center gap-3 border-t border-border-subtle p-4 active:opacity-70"
+          : "flex-row items-center gap-3 p-4 active:opacity-70"
+      }
+      disabled={props.disabled}
+      onPress={props.onPress}
+    >
+      <EnvironmentMachineSymbol
+        kind={machine.machine}
+        size={18}
+        tintColorClassName="accent-foreground-muted"
+      />
+      <View className="min-w-0 flex-1 gap-0.5">
+        <Text className="text-lg text-foreground android:text-base" numberOfLines={1}>
+          {machine.label}
+        </Text>
+        {status === null ? null : <Text className="text-sm text-foreground-muted">{status}</Text>}
+      </View>
+      {props.selected ? (
+        <SymbolView
+          name="checkmark"
+          size={18}
+          tintColorClassName="accent-icon"
+          type="monochrome"
+          weight="semibold"
+        />
+      ) : null}
+    </Pressable>
+  );
+}
```

## apps/mobile/src/features/threads/NewTaskDraftScreen.tsx

Status: modified; +2; −0

```diff
@@ -111,6 +111,7 @@
   waitForComposerDraftsLoaded,
 } from "../../state/use-composer-drafts";
 import { useEnvironmentServerConfig, useProjects, useThreadShells } from "../../state/entities";
+import { useMentionableEnvironments } from "../../state/environments";
 import { useProjectClone } from "../../state/projectClones";
 import { projectEnvironment } from "../../state/projects";
 import { sourceControlEnvironment } from "../../state/sourceControl";
@@ -475,6 +476,7 @@
     ownerKey: flow.draftKey,
     environmentId: selectedProject?.environmentId ?? null,
     threadShells: useThreadShells(),
+    environments: useMentionableEnvironments(),
     pullRequestProjectId: selectedEnvironmentServerConfig?.environment.capabilities.pullRequests
       ? (selectedProject?.id ?? null)
       : null,
```

## apps/mobile/src/features/threads/SubagentRow.tsx

Status: modified; +11; −2

```diff
@@ -26,14 +26,17 @@
   | "status"
   | "progress"
   | "result"
+  | "remoteChild"
 >;
 
 export function SubagentRow(props: {
   readonly environmentId: EnvironmentId;
   readonly subagent: SubagentRowSubagent;
   readonly elapsed: ReactNode;
+  /** The task runs in a linked environment this app is connected to. */
+  readonly canOpenRemote?: boolean;
 }) {
+  const presentation = resolveSubagentRowPresentation(props.subagent, props.canOpenRemote);
-  const presentation = resolveSubagentRowPresentation(props.subagent);
   const detail = subagentCardDetail(presentation.detail);
   return (
     <View className="flex-row gap-3">
@@ -64,6 +67,11 @@
             >
               {presentation.statusLabel}
             </Text>
+            {presentation.runsOn !== null ? (
+              <Text numberOfLines={1} className="shrink text-xs text-foreground-muted">
+                on {presentation.runsOn}
+              </Text>
+            ) : null}
           </View>
           {props.elapsed}
           {presentation.canOpenThread ? (
@@ -92,7 +100,8 @@
   readonly subagent: SubagentRowSubagent;
 }) {
   const { environmentId, subagent } = props;
+  // A task in a linked environment runs a provider instance of that environment.
+  const config = useEnvironmentServerConfig(subagent.remoteChild?.environmentId ?? environmentId);
-  const config = useEnvironmentServerConfig(environmentId);
   const provider = config?.providers.find(
     (candidate) => candidate.instanceId === subagent.providerInstanceId,
   );
```

## apps/mobile/src/features/threads/ThreadAgentsSheet.tsx

Status: modified; +18; −8

```diff
@@ -21,9 +21,11 @@
 import { environmentThreadDetails } from "../../state/threads";
 import { NativeStackScreenOptions, nativeHeaderScrollEdgeEffects } from "../../native/StackHeader";
 import { NativeHeader, NativeScreen } from "../../native/ThemedScreenStack";
+import { useEnvironments } from "../../state/environments";
 import { SubagentRow } from "./SubagentRow";
 import { ScreenContent } from "../../components/ui/ScreenContent";
 import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
+import { subagentThreadTarget } from "./threadAgentsPresentation";
 
 const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version);
 
@@ -50,14 +52,14 @@
   const subagents = turn?.subagents ?? [];
   const hasLiveAgent = (turn?.liveCount ?? 0) > 0;
 
-  const openChildThread = (childThreadId: ThreadId) => {
+  const openChildThread = (child: { environmentId: EnvironmentId; threadId: ThreadId }) => {
     void Haptics.selectionAsync();
     // Replace rather than push: the sheet is a leaf, and the child thread
     // belongs in the workspace stack where Home's back button expects it.
     navigation.dispatch(
       StackActions.replace("Thread", {
+        environmentId: child.environmentId,
+        threadId: child.threadId,
-        environmentId: target.environmentId,
-        threadId: childThreadId,
       }),
     );
   };
@@ -135,26 +137,34 @@
   readonly environmentId: EnvironmentId;
   readonly subagent: OrchestrationV2Subagent;
   readonly tickSeconds: boolean;
+  readonly onOpen: (child: { environmentId: EnvironmentId; threadId: ThreadId }) => void;
-  readonly onOpen: (childThreadId: ThreadId) => void;
 }) {
   const { subagent } = props;
+  const { presentationById } = useEnvironments();
+  const target = subagentThreadTarget(subagent, props.environmentId, (id) =>
-  const childThreadId = subagent.childThreadId;
+    presentationById.has(id),
+  );
 
   const row = (
     <View className="border-b border-border py-3.5">
       <SubagentRow
         environmentId={props.environmentId}
         subagent={subagent}
+        canOpenRemote={target !== null}
         elapsed={<AgentElapsed subagent={subagent} tickSeconds={props.tickSeconds} />}
       />
     </View>
   );
 
+  if (target === null) {
-  if (childThreadId === null) {
     return (
       <View
         accessible
+        accessibilityHint={
+          subagent.remoteChild === undefined
+            ? "Provider-managed agent. Its work appears in the transcript."
+            : `Runs on ${subagent.remoteChild.label}. Connect to it to open its thread.`
-        accessibilityHint="Provider-managed agent. Its work appears in the transcript."
+        }
       >
         {row}
       </View>
@@ -165,7 +175,7 @@
     <Pressable
       accessibilityRole="link"
       accessibilityHint="Opens this agent's thread"
+      onPress={() => props.onOpen(target)}
-      onPress={() => props.onOpen(childThreadId)}
       className="active:opacity-70"
     >
       {row}
```

## apps/mobile/src/features/threads/ThreadComposer.tsx

Status: modified; +2; −0

```diff
@@ -62,6 +62,7 @@
 import { appAtomRegistry } from "../../state/atom-registry";
 import type { ComposerDocumentAttachment } from "../../lib/composerContext";
 import { useProject, useThreadShells } from "../../state/entities";
+import { useMentionableEnvironments } from "../../state/environments";
 import { scopeProjectRef } from "@t3tools/client-runtime/environment";
 
 import { AppText as Text } from "../../components/ui/AppText";
@@ -468,6 +469,7 @@
     ownerKey: composerDraftKey,
     environmentId: props.environmentId,
     threadShells: useThreadShells(),
+    environments: useMentionableEnvironments(),
     currentThreadId: props.selectedThread.id,
     projectCwd: props.projectCwd,
     pullRequestProjectId: props.serverConfig?.environment.capabilities.pullRequests
```

## apps/mobile/src/features/threads/ThreadFeed.tsx

Status: modified; +12; −0

```diff
@@ -12,6 +12,7 @@
 } from "../../lib/enrichedLinkPresentation";
 import { ThreadContextDivider } from "./thread-context-divider";
 import { ThreadHandoffDivider, ThreadHandoffRow } from "./thread-handoff-row";
+import { LinkRequestCard } from "./LinkRequestCard";
 import { SecretRequestCard } from "./SecretRequestCard";
 import { AppActivityIndicator } from "../../components/ui/AppActivityIndicator";
 import {
@@ -185,6 +186,7 @@
   threadFeedRunIsUnsettled,
   isContextCompactionActivityGroup,
   isContextHandoffActivityGroup,
+  isLinkRequestActivityGroup,
   isSecretRequestActivityGroup,
   type ThreadFeedEntry,
   type ThreadFeedLatestRun,
@@ -1116,6 +1118,16 @@
     );
   }
 
+  if (entry.type === "activity-group" && isLinkRequestActivityGroup(entry)) {
+    return (
+      <LinkRequestCard
+        environmentId={props.environmentId}
+        projectedItem={entry.activities[0]!.projectedItem}
+        iconColor={iconSubtleColor}
+      />
+    );
+  }
+
   if (entry.type === "activity-group" && isContextCompactionActivityGroup(entry)) {
     const label = entry.activities[0]!.summary;
     const active =
```

## apps/mobile/src/features/threads/ThreadRouteScreen.tsx

Status: modified; +15; −0

```diff
@@ -41,6 +41,8 @@
 import { EmptyState } from "../../components/ui/EmptyState";
 import { LoadingScreen } from "../../components/LoadingScreen";
 import { scopedThreadKey } from "../../lib/scopedEntities";
+import { scopeThreadRef } from "@t3tools/client-runtime/environment";
+import { useThreadShell } from "../../state/entities";
 import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass";
 import { connectionTone } from "../connection/connectionTone";
 import {
@@ -422,9 +424,22 @@
     : null;
 
   const usesNativeHeaderGlass = NATIVE_LIQUID_GLASS_SUPPORTED;
+  const delegatedFrom = selectedThread?.delegatedFrom ?? null;
+  const delegatedFromRef =
+    delegatedFrom === null
+      ? null
+      : scopeThreadRef(delegatedFrom.environmentId, delegatedFrom.threadId);
+  // The title it had at delegation, until this client sees the parent itself.
+  const delegatedFromThread = useThreadShell(delegatedFromRef);
   const headerSubtitle = [
     selectedThreadProject?.title ?? null,
     selectedEnvironmentConnection?.environmentLabel ?? null,
+    // A linked environment's delegated task names its parent; other linked work its origin.
+    delegatedFrom !== null
+      ? `Subagent of ${delegatedFromThread?.title ?? delegatedFrom.title}`
+      : selectedThread?.linkOrigin
+        ? `From ${selectedThread.linkOrigin.label}`
+        : null,
   ]
     .filter(Boolean)
     .join(" · ");
```

## apps/mobile/src/features/threads/thread-subagent-group.tsx

Status: modified; +25; −8

```diff
@@ -19,7 +19,10 @@
 import { serverEnvironment } from "../../state/server";
 import { environmentThreadDetails } from "../../state/threads";
 import { subagentCardElapsed } from "./subagent-card-presentation";
+import { useEnvironments } from "../../state/environments";
+import { useServerConfigs } from "../../state/entities";
 import { SubagentRow } from "./SubagentRow";
+import { subagentThreadTarget } from "./threadAgentsPresentation";
 import { WorkLogBlock } from "./work-log-layout";
 
 type SubagentItem = Extract<OrchestrationV2TurnItem, { type: "subagent" }>;
@@ -78,6 +81,7 @@
     environmentThreadDetails.threadAtom(scopeThreadRef(props.environmentId, members[0]!.threadId)),
     (thread) => thread?.projection.subagents,
   );
+  const { presentationById } = useEnvironments();
   const agents = members.map((item) => {
     const live = liveAgents?.find((agent) => agent.id === item.subagentId);
     return {
@@ -95,8 +99,13 @@
   const label = `${agents.length} subagents`;
   const summary = summarizeSubagentStatuses(agents.map((agent) => agent.status));
   const expanded = props.expandedRows[props.anchorKey] ?? false;
+  const serverConfigs = useServerConfigs();
+  // A task in a linked environment runs a provider instance of that environment.
   const iconUrl = (item: SubagentItem) =>
+    (item.remoteChild === undefined
+      ? config
+      : serverConfigs.get(item.remoteChild.environmentId)
-    config?.providers.find((provider) => provider.instanceId === item.providerInstanceId)?.iconUrl;
+    )?.providers.find((provider) => provider.instanceId === item.providerInstanceId)?.iconUrl;
   return (
     <WorkLogBlock>
       {grouped ? (
@@ -147,24 +156,31 @@
       {!grouped || expanded ? (
         <View className="mb-1 gap-px rounded-xl border border-border bg-card/30 p-1">
           {agents.map((agent) => {
-            const threadId = agent.childThreadId;
+            // A task in a linked environment opens there, if this app is connected to it.
+            const target = subagentThreadTarget(agent, props.environmentId, (id) =>
+              presentationById.has(id),
+            );
             return (
               <Pressable
                 key={agent.item.id}
                 accessible
+                accessibilityRole={target === null ? undefined : "link"}
-                accessibilityRole={threadId === null ? undefined : "link"}
                 accessibilityHint={
-                  threadId === null ? "Provider-managed agent" : "Opens this agent's thread"
+                  target !== null
+                    ? "Opens this agent's thread"
+                    : agent.remoteChild !== undefined
+                      ? `Runs on ${agent.remoteChild.label}`
+                      : "Provider-managed agent"
                 }
-                disabled={threadId === null}
+                disabled={target === null}
                 onPress={() => {
                   // Push, not navigate: navigate reuses this Thread route, so back
                   // would skip the parent thread.
-                  if (threadId !== null)
+                  if (target !== null)
                     navigation.dispatch(
                       StackActions.push("Thread", {
-                        environmentId: String(props.environmentId),
-                        threadId: String(threadId),
+                        environmentId: String(target.environmentId),
+                        threadId: String(target.threadId),
                       }),
                     );
                 }}
@@ -173,6 +189,7 @@
                 <SubagentRow
                   environmentId={props.environmentId}
                   subagent={agent}
+                  canOpenRemote={target !== null}
                   elapsed={<SubagentElapsed agents={[agent]} />}
                 />
               </Pressable>
```

## apps/mobile/src/features/threads/threadAgentsPresentation.test.ts

Status: modified; +26; −1

```diff
@@ -1,6 +1,6 @@
 import { describe, expect, it } from "vite-plus/test";
 
-import { resolveSubagentRowPresentation } from "./threadAgentsPresentation";
+import { resolveSubagentRowPresentation, subagentThreadTarget } from "./threadAgentsPresentation";
 
 const base = {
   title: null,
@@ -59,6 +59,31 @@
     );
   });
 
+  it("opens a task in a linked environment there, only when this app knows it", () => {
+    const remote = {
+      ...base,
+      childThreadId: null,
+      remoteChild: {
+        environmentId: "environment-box" as never,
+        threadId: "thread-on-box" as never,
+        label: "Box",
+      },
+    };
+    expect(resolveSubagentRowPresentation(remote).runsOn).toBe("Box");
+    expect(resolveSubagentRowPresentation(remote, false).canOpenThread).toBe(false);
+    expect(resolveSubagentRowPresentation(remote, true).canOpenThread).toBe(true);
+    const here = "environment-here" as never;
+    expect(subagentThreadTarget(remote, here, () => false)).toBeNull();
+    expect(subagentThreadTarget(remote, here, (id) => id === "environment-box")).toEqual({
+      environmentId: "environment-box",
+      threadId: "thread-on-box",
+    });
+    expect(subagentThreadTarget(base, here, () => false)).toEqual({
+      environmentId: here,
+      threadId: "thread-child",
+    });
+  });
+
   it("uses the status label when there is nothing to report yet", () => {
     const row = resolveSubagentRowPresentation({ ...base, status: "pending" });
 
```

## apps/mobile/src/features/threads/threadAgentsPresentation.ts

Status: modified; +27; −3

```diff
@@ -3,7 +3,7 @@
   subagentDetailPreview,
 } from "@t3tools/client-runtime/state/subagent-display";
 import { isActiveSubagentStatus } from "@t3tools/client-runtime/state/subagentRuntime";
-import type { OrchestrationV2Subagent } from "@t3tools/contracts";
+import type { EnvironmentId, OrchestrationV2Subagent, ThreadId } from "@t3tools/contracts";
 
 const PROMPT_TITLE_LIMIT = 80;
 
@@ -16,8 +16,29 @@
   readonly tone: SubagentRowTone;
   readonly live: boolean;
   readonly canOpenThread: boolean;
+  /** The linked environment the task runs in, when it runs in one. */
+  readonly runsOn: string | null;
 }
 
+/**
+ * Where an agent's thread opens: here, or in the linked environment that runs
+ * it. Null when there is no thread, or this app is not connected to that
+ * environment.
+ */
+export function subagentThreadTarget(
+  subagent: Pick<OrchestrationV2Subagent, "childThreadId" | "remoteChild">,
+  environmentId: EnvironmentId,
+  isKnownEnvironment: (environmentId: EnvironmentId) => boolean,
+): { readonly environmentId: EnvironmentId; readonly threadId: ThreadId } | null {
+  if (subagent.childThreadId !== null) {
+    return { environmentId, threadId: subagent.childThreadId };
+  }
+  const remote = subagent.remoteChild;
+  return remote !== undefined && isKnownEnvironment(remote.environmentId)
+    ? { environmentId: remote.environmentId, threadId: remote.threadId }
+    : null;
+}
+
 function rowTitle(subagent: Pick<OrchestrationV2Subagent, "title" | "prompt">): string {
   const title = subagent.title?.trim();
   if (title) return formatSubagentDisplayTitle(title);
@@ -58,8 +79,9 @@
 export function resolveSubagentRowPresentation(
   subagent: Pick<
     OrchestrationV2Subagent,
-    "title" | "prompt" | "status" | "progress" | "result" | "childThreadId"
+    "title" | "prompt" | "status" | "progress" | "result" | "childThreadId" | "remoteChild"
   >,
+  canOpenRemote = false,
 ): SubagentRowPresentation {
   const live = isActiveSubagentStatus(subagent.status);
   return {
@@ -68,6 +90,8 @@
     statusLabel: rowStatusLabel(subagent.status),
     tone: rowTone(subagent.status),
     live,
+    canOpenThread:
+      subagent.childThreadId !== null || (subagent.remoteChild !== undefined && canOpenRemote),
+    runsOn: subagent.remoteChild?.label ?? null,
-    canOpenThread: subagent.childThreadId !== null,
   };
 }
```

## apps/mobile/src/features/threads/use-composer-command-menu.ts

Status: modified; +43; −2

```diff
@@ -7,16 +7,25 @@
   ThreadId,
   ThreadPullRequestLink,
 } from "@t3tools/contracts";
+import {
+  type ComposerEnvironmentCandidate,
-import { matchComposerThreadItems } from "@t3tools/client-runtime/composerThreadItems";
+  matchComposerEnvironmentItems,
+  matchComposerThreadItems,
+} from "@t3tools/client-runtime/composerThreadItems";
 import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/models";
 
 const EMPTY_THREAD_SHELLS: ReadonlyArray<EnvironmentThreadShell> = [];
+const EMPTY_ENVIRONMENTS: ReadonlyArray<ComposerEnvironmentCandidate> = [];
 import {
   COMPOSER_CONTEXT_MAX_RECORDS,
   PROVIDER_WORKSPACE_SNAPSHOT_TTL_MS,
 } from "@t3tools/contracts";
 import { formatComposerContextReference } from "@t3tools/shared/composerContextReferences";
+import {
+  environmentComposerContext,
+  pullRequestComposerContext,
+  threadComposerContext,
+} from "../../lib/composerContext";
-import { pullRequestComposerContext, threadComposerContext } from "../../lib/composerContext";
 import { uuidv4 } from "../../lib/uuid";
 import {
   getComposerDraftSnapshot,
@@ -172,6 +181,7 @@
   ownerKey,
   environmentId,
   threadShells = EMPTY_THREAD_SHELLS,
+  environments = EMPTY_ENVIRONMENTS,
   currentThreadId = null,
   projectCwd,
   pullRequestProjectId = null,
@@ -190,6 +200,7 @@
   readonly ownerKey: string | null;
   readonly environmentId: EnvironmentId | null;
   readonly threadShells?: ReadonlyArray<EnvironmentThreadShell>;
+  readonly environments?: ReadonlyArray<ComposerEnvironmentCandidate>;
   readonly currentThreadId?: ThreadId | null;
   readonly projectCwd: string | null;
   readonly pullRequestProjectId?: ProjectId | null;
@@ -502,8 +513,12 @@
             query: trigger.query,
           })
         : [];
+      const environmentItems = environmentId
+        ? matchComposerEnvironmentItems({ environments, environmentId, query: trigger.query })
+        : [];
       return [
         ...threadItems,
+        ...environmentItems,
         ...pathSearch.entries.map((entry) => {
           const parts = entry.path.split("/");
           return {
@@ -523,6 +538,32 @@
 
   const onSelect = (item: ComposerCommandItem) => {
     if (!trigger) return;
+    if (item.type === "environment") {
+      if (!ownerKey || trigger.kind !== "path") return;
+      const record = environmentComposerContext(item.environmentId, item.label);
+      const existing = getComposerDraftSnapshot(ownerKey).context?.records ?? [];
+      const alreadyAttached = existing.some((entry) => entry.contextId === record.contextId);
+      if (!alreadyAttached && existing.length >= COMPOSER_CONTEXT_MAX_RECORDS) {
+        Dialog.alert("Too many context items", "Remove some context from the draft and try again.");
+        return;
+      }
+      const result = replaceTextRange(
+        draftMessage,
+        trigger.rangeStart,
+        trigger.rangeEnd,
+        `${formatComposerContextReference(record)} `,
+      );
+      onChangeDraftMessage(result.text);
+      if (!alreadyAttached) {
+        const draft = getComposerDraftSnapshot(ownerKey);
+        setComposerDraftContext(ownerKey, {
+          version: 1,
+          records: [...(draft.context?.records ?? []), record],
+        });
+      }
+      setSelection({ start: result.cursor, end: result.cursor });
+      return;
+    }
     if (item.type === "thread") {
       if (!ownerKey || trigger.kind !== "path") return;
       const shell = threadShells.find(
```

## apps/mobile/src/lib/composerChipPresentation.test.ts

Status: modified; +9; −0

```diff
@@ -17,6 +17,15 @@
 });
 
 describe("contextChipPresentation image detection", () => {
+  it("draws a mentioned machine as a machine, not the generic file chip", async () => {
+    const { contextChipPresentation } = await import("./composerChipPresentation");
+    expect(contextChipPresentation("environment")).toEqual({
+      accent: "#3f9a4f",
+      symbol: "server.rack",
+      icon: null,
+    });
+  });
+
   it("treats a picture attached through the file picker as an image", async () => {
     const { contextChipPresentation } = await import("./composerChipPresentation");
     expect(
```

## apps/mobile/src/lib/composerChipPresentation.ts

Status: modified; +2; −0

```diff
@@ -40,6 +40,8 @@
     symbol: "bubble.left.and.bubble.right",
     icon: "messages-square",
   },
+  // A machine: the record carries only its identity, so every machine wears one glyph.
+  environment: { accent: "#3f9a4f", symbol: "server.rack", icon: null },
 } as const satisfies Record<string, ContextChipPresentation>;
 
 export const UNAVAILABLE_CONTEXT_CHIP_PRESENTATION: ContextChipPresentation = {
```

[Next page](https://git.cafe/pingdotgg/t3code/pull/16731/diffs?after=ImFwcHMvbW9iaWxlL3NyYy9saWIvY29tcG9zZXJDaGlwUHJlc2VudGF0aW9uLnRzIg&expectedVersion=14&format=markdown)
