# Terminal

A shell command an agent runs, such as a lint, a test run or a build, with its output underneath.

Source: https://suiss.app/ui/docs/ai/terminal

**React**

**Example**

```tsx
import { TerminalIcon } from "@phosphor-icons/react"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import { Message, MessageContent } from "@/components/ui/message"
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"

const commands = [
  {
    command: "pnpm lint",
    output: ["> acme-chat@0.1.0 lint", "> eslint ."],
  },
  {
    command: "pnpm test",
    output: [
      "> acme-chat@0.1.0 test",
      "> vitest run",
      "",
      " ✓ lib/ai.test.ts (6 tests) 41ms",
      " ✓ components/chat.test.tsx (9 tests) 212ms",
      "",
      " Test Files  2 passed (2)",
      "      Tests  15 passed (15)",
    ],
  },
  {
    command: "pnpm build",
    output: [
      "> acme-chat@0.1.0 build",
      "> next build",
      "",
      "   ▲ Next.js 16.3.0",
      "   Creating an optimized production build ...",
      " ✓ Compiled successfully in 4.2s",
      " ✓ Generating static pages (12/12)",
      "",
      "Route (app)                Size  First Load JS",
      "┌ ○ /                     1.9 kB       112 kB",
      "└ ƒ /api/chat              141 B       102 kB",
    ],
  },
]

export default function TerminalDemo() {
  return (
    <div className="flex w-full max-w-sm flex-col gap-4 py-6 text-sm">
      <Message align="end">
        <MessageContent>
          <Bubble align="end" variant="muted">
            <BubbleContent>Run the checks, then build the app.</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Message>
        <MessageContent className="gap-4">
          <Trace>
            {commands.map((item, index) => (
              <TraceCollapsible
                key={item.command}
                defaultOpen={index === commands.length - 1}
              >
                <TraceCollapsibleTrigger>
                  <TraceLine>
                    <TraceIcon>
                      <TerminalIcon />
                    </TraceIcon>
                    <TraceTitle>Ran {item.command}</TraceTitle>
                  </TraceLine>
                </TraceCollapsibleTrigger>
                <TraceCollapsibleContent>
                  <CodeBlock size="sm">
                    <CodeBlockContent wrap className="max-h-64">
                      {item.output.join("\n")}
                    </CodeBlockContent>
                  </CodeBlock>
                </TraceCollapsibleContent>
              </TraceCollapsible>
            ))}
          </Trace>
          <p>Lint and tests pass, and the build compiled both routes.</p>
        </MessageContent>
      </Message>
    </div>
  )
}
```

Terminal is a shell command the agent runs, such as a lint, a test run or a build. The row shows the command, and its output sits underneath in a [Code Block](https://suiss.app/ui/docs/components/code-block).

Terminal is built with [Trace](https://suiss.app/ui/docs/components/trace), with a `TraceCollapsible` for each command.

## Installation

Terminal is built with the [Trace](https://suiss.app/ui/docs/components/trace) and [Code Block](https://suiss.app/ui/docs/components/code-block) components.

```bash title="Terminal"
npx shadcn@latest add @esuiss/trace @esuiss/code-block
```

Or use the [suiss/ui CLI](https://suiss.app/ui/docs/cli):

```bash title="Terminal"
npx @esuiss/ui@latest add trace code-block
```

## Usage

```tsx
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"
```

```tsx
<Trace>
  <TraceCollapsible>
    <TraceCollapsibleTrigger>
      <TraceLine>
        <TraceIcon>
          <TerminalIcon />
        </TraceIcon>
        <TraceTitle>Ran pnpm build</TraceTitle>
      </TraceLine>
    </TraceCollapsibleTrigger>
    <TraceCollapsibleContent>
      <CodeBlock size="sm">
        <CodeBlockContent wrap className="max-h-64">
          {output}
        </CodeBlockContent>
      </CodeBlock>
    </TraceCollapsibleContent>
  </TraceCollapsible>
</Trace>
```

## Examples

### Exit code

A command that exits with any code other than `0` failed. Set `status="error"` and open the row so the output is visible.

**Example**

```tsx
import { TerminalIcon } from "@phosphor-icons/react"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import {
  Trace,
  TraceCollapsible,
  TraceCollapsibleContent,
  TraceCollapsibleTrigger,
  TraceIcon,
  TraceLine,
  TraceTitle,
} from "@/components/ui/trace"

const LINT_OUTPUT = `> acme-chat@0.1.0 lint
> eslint .

app/api/chat/route.ts
  14:7  error  'model' is assigned a value but never used  no-unused-vars

components/chat.tsx
  42:5  error  React Hook useEffect has a missing dependency: 'messages'  react-hooks/exhaustive-deps

✖ 2 problems (2 errors, 0 warnings)

 ELIFECYCLE  Command failed with exit code 1.`

export default function TerminalError() {
  return (
    <Trace className="max-w-sm">
      <TraceLine>
        <TraceIcon>
          <TerminalIcon />
        </TraceIcon>
        <TraceTitle>Ran pnpm install</TraceTitle>
      </TraceLine>
      <TraceCollapsible defaultOpen>
        <TraceCollapsibleTrigger>
          <TraceLine status="error">
            <TraceIcon>
              <TerminalIcon />
            </TraceIcon>
            <TraceTitle>pnpm lint exited with code 1</TraceTitle>
          </TraceLine>
        </TraceCollapsibleTrigger>
        <TraceCollapsibleContent>
          <CodeBlock size="sm">
            <CodeBlockContent wrap className="max-h-64">
              {LINT_OUTPUT}
            </CodeBlockContent>
          </CodeBlock>
        </TraceCollapsibleContent>
      </TraceCollapsible>
    </Trace>
  )
}
```

| Command | `status` | Title |
| --- | --- | --- |
| Running | `"running"` | Running pnpm build |
| Exited with `0` | — | Ran pnpm build |
| Exited with another code | `"error"` | pnpm lint exited with code 1 |

### Open while running

Open the row once the first output arrives, so the output is visible while the command runs, and let the reader's choice win after that. See [Trace](https://suiss.app/ui/docs/components/trace#controlled).

## AI SDK

A tool result arrives only once the command exits. To stream the output while it runs, write it from the tool as a data part with the tool call's id. The AI SDK replaces a data part that has the same id, so the output grows in place.

```ts title="app/api/chat/route.ts"
writer.write({
  type: "data-terminal",
  id: toolCallId,
  data: { output },
})
```

On the client, find the data part that matches each tool part.

```tsx
function getOutput(message: ChatMessage, toolCallId: string) {
  for (const part of message.parts) {
    if (part.type === "data-terminal" && part.id === toolCallId) {
      return part.data.output
    }
  }
  return ""
}
```

## API Reference

See [Trace](https://suiss.app/ui/docs/components/trace#api-reference) and [Code Block](https://suiss.app/ui/docs/components/code-block#api-reference).

## Accessibility

- Say the result in the title, not only with color: "Ran pnpm build" or "pnpm lint exited with code 1".
- Each row is a button with `aria-expanded`, and the output is reachable by keyboard so long lines can be scrolled.

**React Native, SwiftUI, Jetpack Compose**

```tsx
import { TerminalIcon } from "phosphor-react-native"
import { View } from "react-native"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import { Icon } from "@/components/ui/icon"
import { Message, MessageContent } from "@/components/ui/message"
import { Text } from "@/components/ui/text"
import { Trace, TraceCollapsible, TraceCollapsibleContent, TraceCollapsibleTrigger, TraceDescription, TraceIcon, TraceLine, TraceTitle } from "@/components/ui/trace"

export function AiTerminalExample() {
  return (
    <View className="w-full gap-4">
      <Message align="end">
        <MessageContent>
          <Bubble variant="muted">
            <BubbleContent>Run the checks, then build the app.</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Trace>
        <TraceLine>
          <TraceIcon>
            <Icon as={TerminalIcon} />
          </TraceIcon>
          <TraceTitle>Ran pnpm lint</TraceTitle>
          <TraceDescription>2s</TraceDescription>
        </TraceLine>
        <TraceLine>
          <TraceIcon>
            <Icon as={TerminalIcon} />
          </TraceIcon>
          <TraceTitle>Ran pnpm test</TraceTitle>
          <TraceDescription>14s</TraceDescription>
        </TraceLine>
        <TraceCollapsible defaultOpen>
          <TraceCollapsibleTrigger>
            <TraceLine>
              <TraceIcon>
                <Icon as={TerminalIcon} />
              </TraceIcon>
              <TraceTitle>Ran pnpm build</TraceTitle>
              <TraceDescription>38s</TraceDescription>
            </TraceLine>
          </TraceCollapsibleTrigger>
          <TraceCollapsibleContent>
            <CodeBlock size="sm">
              <CodeBlockContent language="bash">
                {
                  "$ pnpm build\nCompiled successfully in 31s\nRoute (app)  Size\n/            4.2 kB\n/checkout    8.9 kB"
                }
              </CodeBlockContent>
            </CodeBlock>
          </TraceCollapsibleContent>
        </TraceCollapsible>
      </Trace>
      <Text>Lint and tests pass, and the build compiled both routes.</Text>
    </View>
  )
}
```

```swift
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TerminalExample: View {
  var body: some View {
    VStack(alignment: .leading, spacing: 16) {
      Message(align: .end) {
        MessageContent {
          Bubble(variant: .muted, align: .end) { BubbleContent("Run the checks, then build the app.") }
        }
      }
      Trace {
        TraceLine {
          TraceIcon { Ph.terminal.regular }
          TraceTitle(verbatim: "Ran pnpm lint")
          TraceDescription(verbatim: "2s")
        }
        TraceLine {
          TraceIcon { Ph.terminal.regular }
          TraceTitle(verbatim: "Ran pnpm test")
          TraceDescription(verbatim: "14s")
        }
        TraceCollapsible(initiallyExpanded: true) {
          TraceCollapsibleTrigger {
            TraceLine {
              TraceIcon { Ph.terminal.regular }
              TraceTitle(verbatim: "Ran pnpm build")
              TraceDescription(verbatim: "38s")
            }
          }
          TraceCollapsibleContent {
            CodeBlock(size: .sm) { CodeBlockContent("$ pnpm build\\nCompiled successfully in 31s\\nRoute (app)  Size\\n/            4.2 kB\\n/checkout    8.9 kB", language: "bash") }
          }
        }
      }
      Text("Lint and tests pass, and the build compiled both routes.").textStyle(.base).foregroundStyle(EsuissColor.foreground)
    }
  }
}
```

```kotlin
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Terminal
import com.esuiss.ui.components.Bubble
import com.esuiss.ui.components.BubbleContent
import com.esuiss.ui.components.BubbleVariant
import com.esuiss.ui.components.CodeBlock
import com.esuiss.ui.components.CodeBlockContent
import com.esuiss.ui.components.CodeBlockSize
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Message
import com.esuiss.ui.components.MessageAlign
import com.esuiss.ui.components.MessageContent
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.Trace
import com.esuiss.ui.components.TraceCollapsible
import com.esuiss.ui.components.TraceCollapsibleContent
import com.esuiss.ui.components.TraceCollapsibleTrigger
import com.esuiss.ui.components.TraceDescription
import com.esuiss.ui.components.TraceIcon
import com.esuiss.ui.components.TraceLine
import com.esuiss.ui.components.TraceTitle

@Composable
fun TerminalAIDefault() {
    Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(16.dp)) {
        Message(Modifier.fillMaxWidth(), align = MessageAlign.End) {
            MessageContent {
                Bubble(variant = BubbleVariant.Muted) { BubbleContent("Run the checks, then build the app.") }
            }
        }
        Trace {
            TraceLine {
                TraceIcon { Icon(PhosphorIcons.Regular.Terminal, contentDescription = null) }
                TraceTitle("Ran pnpm lint")
                TraceDescription("2s")
            }
            TraceLine {
                TraceIcon { Icon(PhosphorIcons.Regular.Terminal, contentDescription = null) }
                TraceTitle("Ran pnpm test")
                TraceDescription("14s")
            }
            TraceCollapsible(initiallyExpanded = true) {
                TraceCollapsibleTrigger {
                    TraceLine {
                        TraceIcon { Icon(PhosphorIcons.Regular.Terminal, contentDescription = null) }
                        TraceTitle("Ran pnpm build")
                        TraceDescription("38s")
                    }
                }
                TraceCollapsibleContent {
                    CodeBlock(size = CodeBlockSize.Sm) { CodeBlockContent("${'$'} pnpm build\nCompiled successfully in 31s\nRoute (app)  Size\n/            4.2 kB\n/checkout    8.9 kB", language = "bash") }
                }
            }
        }
        Text("Lint and tests pass, and the build compiled both routes.")
    }
}
```

Terminal on mobile is built from the same parts as on the web: [Trace](https://suiss.app/ui/docs/components/trace), [Code Block](https://suiss.app/ui/docs/components/code-block), [Message](https://suiss.app/ui/docs/components/message), [Bubble](https://suiss.app/ui/docs/components/bubble). The example above is a static demo; connect it to your agent by rendering the same parts from your messages.

## Installation

**React Native**

```bash title="Terminal"
npx @esuiss/ui@latest add trace code-block message bubble
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add trace code-block message bubble --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add trace code-block message bubble --platform kotlin
```

## API Reference

See [Trace](https://suiss.app/ui/docs/components/trace), [Code Block](https://suiss.app/ui/docs/components/code-block), [Message](https://suiss.app/ui/docs/components/message), [Bubble](https://suiss.app/ui/docs/components/bubble).

