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

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

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

Visibility: public

[feat\(web,mobile\): @\-mention a machine as a context chip](https://git.cafe/pingdotgg/t3code/pull/17130.md)

Version: 28

Head commit: a0c623abd846b5bfc99a23234dda490b7b949862

Base commit: 71bc8a87e79f3a40b808fe38dfe90a60caf556fb

Comparison base: 71bc8a87e79f3a40b808fe38dfe90a60caf556fb

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

## apps/mobile/assets/context-chip-icons/server.png

Status: added; +0; −0

Binary file; no text patch.

## apps/mobile/scripts/sync-context-chip-icons.mjs

Status: modified; +1; −0

```diff
@@ -43,6 +43,7 @@
   "message-circle",
   "messages-square",
   "mouse-pointer-click",
+  "server",
   "terminal",
 ];
 
```

## 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/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/NewTaskDraftScreen.tsx

Status: modified; +2; −0

```diff
@@ -113,6 +113,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";
@@ -483,6 +484,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/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";
@@ -472,6 +473,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/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: added; +11; −0

```diff
@@ -1,0 +1,11 @@
+import { describe, expect, it } from "vite-plus/test";
+
+describe("contextChipPresentation", () => {
+  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",
+      icon: "server",
+    });
+  });
+});
```

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

Status: modified; +2; −0

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

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

Status: modified; +16; −0

```diff
@@ -13,6 +13,8 @@
   type ScopedThreadRef,
   type ServerProviderSkill,
   type ThreadContextRecord,
+  type EnvironmentContextRecord,
+  type EnvironmentId,
 } from "@t3tools/contracts";
 import * as Schema from "effect/Schema";
 import {
@@ -114,6 +116,20 @@
   };
 }
 
+/** Same identity as web: one record per machine, so mentioning it again reuses the chip. */
+export function environmentComposerContext(
+  environmentId: EnvironmentId,
+  label: string,
+): EnvironmentContextRecord {
+  return {
+    version: 1,
+    kind: "environment",
+    contextId: ComposerContextId.make(`environment_${environmentId}`),
+    label: sanitizeComposerContextLabel(label, "environment"),
+    environmentId,
+  };
+}
+
 export function pullRequestComposerContext(
   pullRequest: PullRequestContextMetadata,
   id: string,
```

## apps/mobile/src/lib/contextChipIcons.generated.ts

Status: modified; +1; −0

```diff
@@ -11,6 +11,7 @@
   "message-circle": require("../../assets/context-chip-icons/message-circle.png"),
   "messages-square": require("../../assets/context-chip-icons/messages-square.png"),
   "mouse-pointer-click": require("../../assets/context-chip-icons/mouse-pointer-click.png"),
+  server: require("../../assets/context-chip-icons/server.png"),
   skill: require("../../assets/context-chip-icons/skill.png"),
   terminal: require("../../assets/context-chip-icons/terminal.png"),
 } as const satisfies Readonly<Record<string, ImageSourcePropType>>;
```

## apps/mobile/src/state/environments.ts

Status: modified; +16; −1

```diff
@@ -4,7 +4,8 @@
   hasRelayRoute,
   type EnvironmentPresentation as BaseEnvironmentPresentation,
 } from "@t3tools/client-runtime/connection";
+import type { ComposerEnvironmentCandidate } from "@t3tools/client-runtime/composerThreadItems";
+import { type EnvironmentId, resolveEnvironmentMachineKind } from "@t3tools/contracts";
-import type { EnvironmentId } from "@t3tools/contracts";
 import { useMemo } from "react";
 
 import { environmentCatalog } from "../connection/catalog";
@@ -55,3 +56,17 @@
 export function useEnvironmentConnectionState(environmentId: EnvironmentId) {
   return useEnvironmentQuery(environmentCatalog.stateAtom(environmentId));
 }
+
+/** The user's machines as `@` mention candidates, drawn with their machine kind. */
+export function useMentionableEnvironments(): ReadonlyArray<ComposerEnvironmentCandidate> {
+  const { environments } = useEnvironments();
+  return useMemo(
+    () =>
+      environments.map((environment) => ({
+        environmentId: environment.environmentId,
+        label: environment.label,
+        machine: resolveEnvironmentMachineKind(environment.serverConfig),
+      })),
+    [environments],
+  );
+}
```

## apps/web/src/components/ChatView.logic.test.ts

Status: modified; +1; −0

```diff
@@ -1062,6 +1062,7 @@
       previewAnnotations: [],
       reviewComments: [],
       threadContexts: [],
+      environmentContexts: [],
     });
 
     expect(snapshot.terminalContexts).toEqual([live]);
```

## apps/web/src/components/ChatView.logic.ts

Status: modified; +6; −0

```diff
@@ -39,6 +39,7 @@
   type ScopedProjectRef,
   type ScopedThreadRef,
   type ThreadContextRecord,
+  type EnvironmentContextRecord,
   type ThreadId,
   type ThreadLinkedPullRequest,
   type RunId,
@@ -1294,6 +1295,7 @@
   readonly reviewComments: ReadonlyArray<ReviewCommentContext>;
   readonly previewAnnotations: ReadonlyArray<PreviewAnnotationPayload>;
   readonly threadContexts: ReadonlyArray<ThreadContextRecord>;
+  readonly environmentContexts: ReadonlyArray<EnvironmentContextRecord>;
 }
 
 /**
@@ -1308,6 +1310,7 @@
   readonly writeReviewComments: (comments: ReadonlyArray<ReviewCommentContext>) => void;
   readonly writePreviewAnnotations: (annotations: ReadonlyArray<PreviewAnnotationPayload>) => void;
   readonly writeThreadContexts: (records: ReadonlyArray<ThreadContextRecord>) => void;
+  readonly writeEnvironmentContexts: (records: ReadonlyArray<EnvironmentContextRecord>) => void;
   readonly resetCursor: (options: {
     cursor: number;
     prompt: string;
@@ -1319,6 +1322,7 @@
   input.writeReviewComments(input.snapshot.reviewComments);
   input.writePreviewAnnotations(input.snapshot.previewAnnotations);
   input.writeThreadContexts(input.snapshot.threadContexts);
+  input.writeEnvironmentContexts(input.snapshot.environmentContexts);
   input.resetCursor({
     cursor: collapseExpandedComposerCursor(input.snapshot.prompt, input.snapshot.prompt.length),
     prompt: input.snapshot.prompt,
@@ -1484,6 +1488,7 @@
   readonly previewAnnotations: ReadonlyArray<PreviewAnnotationPayload>;
   readonly reviewComments: ReadonlyArray<ReviewCommentContext>;
   readonly threadContexts: ReadonlyArray<ThreadContextRecord>;
+  readonly environmentContexts: ReadonlyArray<EnvironmentContextRecord>;
 }
 
 /**
@@ -1503,6 +1508,7 @@
     previewAnnotations: [...contents.previewAnnotations],
     reviewComments: [...contents.reviewComments],
     threadContexts: [...contents.threadContexts],
+    environmentContexts: [...contents.environmentContexts],
     messageText: contents.terminalContexts
       .filter((context) => !terminalContexts.includes(context))
       .reduce(
```

## apps/web/src/components/ContextChip.tsx

Status: modified; +2; −0

```diff
@@ -27,6 +27,7 @@
         "pr-closed": "[--context-chip-accent:oklch(0.62_0.16_16)]",
         skill: "[--context-chip-accent:oklch(0.62_0.16_322)]",
         thread: "[--context-chip-accent:oklch(0.62_0.12_190)]",
+        environment: "[--context-chip-accent:oklch(0.62_0.11_145)]",
         citation: "[--context-chip-accent:oklch(0.62_0.16_259)]",
       },
       state: {
@@ -52,6 +53,7 @@
           "pr-closed",
           "skill",
           "thread",
+          "environment",
           "citation",
         ],
         className:
```

## apps/web/src/components/EnvironmentContextChip.tsx

Status: added; +40; −0

```diff
@@ -1,0 +1,40 @@
+import { type EnvironmentContextRecord, resolveEnvironmentMachineKind } from "@t3tools/contracts";
+
+import { useEnvironment } from "~/state/environments";
+import { ContextChip, ContextChipLabel } from "./ContextChip";
+import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";
+import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
+
+/**
+ * Inline chip for a mentioned machine, in the composer and in sent messages. Prefers the
+ * name this client knows the machine by, and draws it with its machine icon.
+ */
+export function EnvironmentContextChip(props: {
+  record: Pick<EnvironmentContextRecord, "environmentId" | "label">;
+  copyMarkdown?: string;
+}) {
+  const environment = useEnvironment(props.record.environmentId);
+  const label = environment?.label.trim() || props.record.label;
+  return (
+    <Tooltip>
+      <TooltipTrigger
+        render={
+          <ContextChip
+            kind="environment"
+            tabIndex={0}
+            aria-label={`Machine, ${label}`}
+            data-markdown-copy={props.copyMarkdown}
+          >
+            <EnvironmentMachineIcon
+              kind={resolveEnvironmentMachineKind(environment?.serverConfig ?? null)}
+            />
+            <ContextChipLabel>{label}</ContextChipLabel>
+          </ContextChip>
+        }
+      />
+      <TooltipPopup side="top">
+        {environment ? "T3 Code environment" : "Machine not in this app"}
+      </TooltipPopup>
+    </Tooltip>
+  );
+}
```

## apps/web/src/components/chat/ChatComposer.tsx

Status: modified; +59; −0

```diff
@@ -22,6 +22,7 @@
   PreviewAnnotationPayload,
   ProviderApprovalDecision,
   ThreadContextRecord,
+  EnvironmentContextRecord,
   ProviderInteractionMode,
   ResolvedKeybindingsConfig,
   RuntimeMode,
@@ -182,6 +183,8 @@
   terminalContextRecord,
   threadContextRecord,
   threadContextReference,
+  environmentContextRecord,
+  environmentContextReference,
 } from "~/lib/composerContextRecords";
 import { rememberCopiedComposerAttachments } from "~/lib/copiedComposerAttachments";
 import { useThreadContextDrop, threadContextDropTargetProps } from "./threadContextDrag";
@@ -347,6 +350,7 @@
     previewAnnotations: PreviewAnnotationPayload[];
     reviewComments: ReviewCommentContext[];
     threadContexts: ThreadContextRecord[];
+    environmentContexts: EnvironmentContextRecord[];
     selectedPromptEffort: string | null;
     selectedModelOptionsForDispatch: unknown;
     selectedModelSelection: ModelSelection;
@@ -739,6 +743,7 @@
   const composerPreviewAnnotations = composerDraft.previewAnnotations;
   const composerReviewComments = composerDraft.reviewComments;
   const composerThreadContexts = attachmentDraft.threadContexts;
+  const composerEnvironmentContexts = composerDraft.environmentContexts;
   const {
     pendingSnapShotAnimations,
     pendingSnapShotIds,
@@ -789,6 +794,7 @@
     reviewComments: composerReviewComments,
     previewAnnotations: composerPreviewAnnotations,
     threadContexts: composerThreadContexts,
+    environmentContexts: composerEnvironmentContexts,
     images: composerImages,
     files: composerFiles,
     uploadsByImageId,
@@ -1209,6 +1215,7 @@
     selectedProviderSkills,
     selectedProviderSlashCommands,
     showSkillsInSlashMenu: settings.showSkillsInSlashMenu,
+    questionAttachmentTarget,
   });
 
   const composerMenuOpen = Boolean(composerTrigger);
@@ -1361,6 +1368,12 @@
   );
   const addComposerDraftThreadContexts = useComposerDraftStore((store) => store.addThreadContexts);
   const setComposerDraftThreadContexts = useComposerDraftStore((store) => store.setThreadContexts);
+  const addComposerDraftEnvironmentContexts = useComposerDraftStore(
+    (store) => store.addEnvironmentContexts,
+  );
+  const setComposerDraftEnvironmentContexts = useComposerDraftStore(
+    (store) => store.setEnvironmentContexts,
+  );
   const buildContextClipboardFragment = (contextIds: ReadonlyArray<string>): string | null => {
     const wanted = new Set(contextIds);
     const copiedFiles = new Map<string, File>();
@@ -1377,6 +1390,7 @@
         .filter((c) => wanted.has(terminalContextReference(c).contextId))
         .map(terminalContextRecord),
       ...composerThreadContexts.filter((record) => wanted.has(record.contextId)),
+      ...composerEnvironmentContexts.filter((record) => wanted.has(record.contextId)),
       ...composerReviewComments
         .filter((c) => wanted.has(reviewCommentContextId(c.id)))
         .map(reviewCommentContextRecord),
@@ -1756,6 +1770,28 @@
       setComposerDraftThreadContexts(composerDraftTarget, nextThreads);
     }
 
+    const liveEnvironmentIds = new Set<string>(
+      composerEnvironmentContexts.map((record) => record.contextId),
+    );
+    const restoredEnvironments = [...referenced].flatMap((contextId) => {
+      if (liveEnvironmentIds.has(contextId)) return [];
+      const record = retained.environments.get(contextId);
+      return record ? [record] : [];
+    });
+    const nextEnvironments = [
+      ...composerEnvironmentContexts.filter((record) => referenced.has(record.contextId)),
+      ...restoredEnvironments,
+    ];
+    for (const record of composerEnvironmentContexts) {
+      if (!referenced.has(record.contextId)) retained.environments.set(record.contextId, record);
+    }
+    if (
+      nextEnvironments.length !== composerEnvironmentContexts.length ||
+      restoredEnvironments.length > 0
+    ) {
+      setComposerDraftEnvironmentContexts(composerDraftTarget, nextEnvironments);
+    }
+
     for (const comment of composerReviewComments) {
       const contextId = reviewCommentContextId(comment.id);
       if (!referenced.has(contextId)) {
@@ -2054,6 +2090,26 @@
       }
       return;
     }
+    if (item.type === "environment") {
+      if (trigger.kind !== "path") return;
+      const record = environmentContextRecord(item.environmentId, item.label);
+      const replacement = `${formatInlineContextReference(environmentContextReference(record))} `;
+      const replacementRangeEnd = extendReplacementRangeForTrailingSpace(
+        snapshot.value,
+        trigger.rangeEnd,
+        replacement,
+      );
+      const applied = applyPromptReplacement(trigger.rangeStart, replacementRangeEnd, replacement, {
+        expectedText: snapshot.value.slice(trigger.rangeStart, replacementRangeEnd),
+      });
+      if (applied) {
+        addComposerDraftEnvironmentContexts(composerDraftTarget, [record], {
+          appendReference: false,
+        });
+        setComposerHighlightedItemId(null);
+      }
+      return;
+    }
   };
   const onSelectComposerItemStable = useStableCallback(onSelectComposerItem);
 
@@ -3269,6 +3325,7 @@
         previewAnnotations: composerPreviewAnnotations,
         reviewComments: composerReviewComments,
         threadContexts: composerThreadContexts,
+        environmentContexts: composerEnvironmentContexts,
         selectedPromptEffort,
         selectedModelOptionsForDispatch,
         selectedModelSelection,
@@ -3324,6 +3381,8 @@
       composerTerminalContextsRef,
       composerPreviewAnnotations,
       composerReviewComments,
+      composerThreadContexts,
+      composerEnvironmentContexts,
       focusComposer,
       environmentId,
       primaryEnvironmentId,
```

## apps/web/src/components/chat/ComposerCommandMenu.tsx

Status: modified; +18; −0

```diff
@@ -5,6 +5,8 @@
   type ProviderSkillSourceKind,
 } from "@t3tools/client-runtime/providerSkills";
 import {
+  type EnvironmentId,
+  type EnvironmentMachineKind,
   type ProjectEntry,
   type ProviderDriverKind,
   type PullRequestContextMetadata,
@@ -27,6 +29,7 @@
 import { cn } from "~/lib/utils";
 import { Badge } from "../ui/badge";
 import { Command, CommandGroup, CommandItem, CommandList } from "../ui/command";
+import { EnvironmentMachineIcon } from "../EnvironmentMachineIcon";
 import { PierreEntryIcon } from "./PierreEntryIcon";
 import { ComposerBanner } from "./ComposerBanner";
 import { resolvePullRequestState } from "../pullRequest/pullRequestPresentation";
@@ -77,6 +80,14 @@
       thread: ScopedThreadRef;
       label: string;
       description: string;
+    }
+  | {
+      id: string;
+      type: "environment";
+      environmentId: EnvironmentId;
+      machine: EnvironmentMachineKind;
+      label: string;
+      description: string;
     };
 
 export const ComposerCommandMenu = memo(function ComposerCommandMenu(props: {
@@ -202,6 +213,13 @@
       {props.item.type === "thread" ? (
         <MessagesSquareIcon aria-hidden="true" className="size-4 shrink-0 text-secondary-label" />
       ) : null}
+      {props.item.type === "environment" ? (
+        <EnvironmentMachineIcon
+          kind={props.item.machine}
+          aria-hidden="true"
+          className="size-4 shrink-0 text-secondary-label"
+        />
+      ) : null}
       {pullRequestPresentation ? (
         <pullRequestPresentation.Icon
           role="img"
```

## apps/web/src/components/chat/UserMessageContextChips.tsx

Status: modified; +11; −0

```diff
@@ -20,6 +20,7 @@
 } from "../../lib/diffRendering";
 import { PREFERRED_HIGHLIGHTER } from "../../lib/syntaxHighlighting";
 import ChatMarkdown from "../ChatMarkdown";
+import { EnvironmentContextChip } from "../EnvironmentContextChip";
 import { ThreadContextChip } from "../ThreadContextChip";
 import { MessageCircleIcon, MousePointerClickIcon, PaintbrushIcon } from "lucide-react";
 import type { ComposerContextId, KnownComposerContextRecord } from "@t3tools/contracts";
@@ -333,6 +334,16 @@
         ),
     },
     {
+      kind: "environment",
+      canRender: (record) => record.kind === "environment",
+      render: (record, context) =>
+        record.kind === "environment" ? (
+          <EnvironmentContextChip record={record} copyMarkdown={context.copyMarkdown} />
+        ) : (
+          <UnavailableUserMessageContextChip {...context} />
+        ),
+    },
+    {
       kind: "image",
       canRender: (record, context) =>
         record.kind === "image" &&
```

## apps/web/src/components/chat/chatViewSend.ts

Status: modified; +1; −0

```diff
@@ -269,6 +269,7 @@
   store.setPreviewAnnotations(draftTarget, snapshot.previewAnnotations);
   store.setReviewComments(draftTarget, snapshot.reviewComments);
   store.setThreadContexts(draftTarget, snapshot.threadContexts);
+  store.setEnvironmentContexts(draftTarget, snapshot.environmentContexts);
   return images;
 }
 
```

[Next page](https://git.cafe/pingdotgg/t3code/pull/17130/diffs?after=ImFwcHMvd2ViL3NyYy9jb21wb25lbnRzL2NoYXQvY2hhdFZpZXdTZW5kLnRzIg&expectedVersion=28&format=markdown)
