> ## Documentation Index
> Fetch the complete documentation index at: https://docs.photon.codes/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Spaces and Users

> Send messages, manage typing indicators, and resolve participants

export const TypeTooltip = ({name, type, children}) => {
  const [visible, setVisible] = React.useState(false);
  const [pos, setPos] = React.useState({
    top: 0,
    left: 0
  });
  const triggerRef = React.useRef(null);
  const show = () => {
    if (triggerRef.current) {
      const rect = triggerRef.current.getBoundingClientRect();
      setPos({
        top: rect.bottom + 6,
        left: rect.left
      });
    }
    setVisible(true);
  };
  const hide = () => setVisible(false);
  return <>
      <span ref={triggerRef} onMouseEnter={show} onMouseLeave={hide} style={{
    cursor: "pointer",
    position: "relative",
    display: "inline"
  }}>
        {children || <code>{name}</code>}
      </span>
      {visible && <span style={{
    position: "fixed",
    top: pos.top,
    left: pos.left,
    zIndex: 9999,
    padding: "8px 12px",
    borderRadius: "8px",
    fontSize: "13px",
    lineHeight: "1.5",
    fontFamily: "'Azeret Mono', monospace",
    whiteSpace: "pre",
    backgroundColor: "var(--tw-prose-pre-bg, #1e1e1e)",
    color: "var(--tw-prose-pre-code, #e5e5e5)",
    border: "1px solid var(--border, rgba(128,128,128,0.2))",
    boxShadow: "0 4px 16px rgba(0,0,0,0.3)",
    pointerEvents: "none"
  }}>
          {type}
        </span>}
    </>;
};

A **space** is a conversation — a DM, a group chat, a terminal session. A **user** is a participant identified by a platform-specific ID. Both carry a `__platform` tag so the narrowing functions from [platform narrowing](/spectrum-ts/platform-narrowing) can recover their platform-specific shapes.

## Space

Every <TypeTooltip name="Space" type={`interface Space<_Def = unknown> {
readonly __platform: string;
avatar(input: string | URL, options?: {
    mimeType?: string;
}): Promise<void>;
avatar(input: Buffer, options: {
    mimeType: string;
}): Promise<void>;
edit(message: Message | undefined, newContent: ContentInput): Promise<void>;
getMessage(id: string): Promise<Message | undefined>;
readonly id: string;
read(message: Message): Promise<void>;
rename(displayName: string): Promise<void>;
responding<T>(fn: () => T | Promise<T>): Promise<T>;
send(content: ReactionBuilder): Promise<(Message<string, AgentSender> & {
    content: Reaction;
}) | undefined>;
send(content: ContentInput): Promise<Message<string, AgentSender> | undefined>;
send(...content: [
    ContentInput,
    ContentInput,
    ...ContentInput[]
]): Promise<Message<string, AgentSender>[]>;
startTyping(): Promise<void>;
stopTyping(): Promise<void>;
unsend(message: Message | undefined): Promise<void>;
}`} /> exposes the same interface regardless of platform:

| Method                      | Description                                                                                                |
| --------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `send(...content)`          | Send one or more content items into the conversation.                                                      |
| `edit(message, newContent)` | Rewrite a previously-sent message. Sugar for `send(edit(newContent, message))`.                            |
| `unsend(message)`           | Retract a previously-sent message. Sugar for `send(unsend(message))`.                                      |
| `startTyping()`             | Show a typing indicator. No-op if the platform doesn't support it.                                         |
| `stopTyping()`              | Hide the typing indicator.                                                                                 |
| `responding(fn)`            | Start a typing indicator, run `fn`, and stop the indicator when it completes — even if `fn` throws.        |
| `getMessage(id)`            | Fetch a message by ID from the conversation. Throws `UnsupportedError` on platforms that don't support it. |
| `rename(displayName)`       | Rename the chat. Sugar for `send(rename(displayName))`.                                                    |
| `avatar(input, options?)`   | Set or clear the chat avatar. Sugar for `send(avatar(input, options?))`.                                   |

```ts theme={null}
for await (const [space, message] of app.messages) {
  await space.send("Got it.");
}
```

## User

Users are minimal: an ID and a platform tag.

```ts theme={null}
interface User {
  readonly id: string;
  readonly __platform: string;
}
```

Resolve a user from a platform-specific identifier through a narrowed platform instance:

```ts theme={null}
import { imessage } from "spectrum-ts/providers/imessage";

const im = imessage(app);
const alice = await im.user("+15551234567");
```

The returned user may include additional platform-specific fields when the provider defines a `user.schema`.

## Typing indicators

### Manual

```ts theme={null}
await space.startTyping();
// ... do work ...
await space.stopTyping();
```

These are sugar for `space.send(typing("start"))` and `space.send(typing("stop"))` — see [Typing indicators](/spectrum-ts/content/typing-indicators) for the canonical form.

### Automatic with `responding`

`responding` is the recommended pattern. It guarantees the typing indicator is cleared even if the inner function throws:

```ts theme={null}
await space.responding(async () => {
  const result = await generateResponse(message);
  await space.send(result);
});
```

The helper is also available on the app itself:

```ts theme={null}
await app.responding(space, async () => {
  await space.send("Thinking...");
});
```

## Creating a space

To start a new conversation, use [platform narrowing](/spectrum-ts/platform-narrowing) to get a platform instance, then call `space.create(...)` with the users:

```ts theme={null}
const im = imessage(app);
const alice = await im.user("+15551111111");
const bob = await im.user("+15552222222");

// DM
const dm = await im.space.create(alice);

// Group
const group = await im.space.create([alice, bob]);

await group.send("Welcome to the group.");
```

To look up an existing conversation by its platform ID, use `space.get(id)`:

```ts theme={null}
const existing = await im.space.get("any;-;+15551111111");
await existing.send("Hello again.");
```

The returned space is the platform-specific type — so you can read extra fields like `type: "dm" | "group"` on iMessage — but it also satisfies the generic `Space` interface, so `send()`, `startTyping()`, and friends are always available.
