# TuilStorySession

Source: /tuil/docs/reference/packages/story/api/tuil-story-session
Locale: en

class exported by @mwillbanks/tuil-story.



{/* Generated by tooling/docs/generate-reference.ts. */}

## class [#class]

Public class exported by `@mwillbanks/tuil-story`.

```ts
export class TuilStorySession {
  readonly #catalog: TuilStoryCatalog;
  readonly #storyId: string;
  readonly #variant: string;
  readonly #themeRegistry?: ThemeRegistry;
  readonly #signal?: AbortSignal;
  readonly #releaseRenderLock: () => void;
  readonly #previousChalkLevel: 0 | 1 | 2 | 3;
  readonly #events: ObservedEvent[] = [];
  readonly #actions: StoryAction[] = [];
  #args: Readonly<Record<string, unknown>>;
  #controls: TerminalStoryControls;
  #instance?: TuilTestInstance;
  #stopObserving?: () => void;
  #closed = false;

  private constructor(
    catalog: TuilStoryCatalog,
    options: OpenStoryOptions,
    releaseRenderLock: () => void,
    previousChalkLevel: 0 | 1 | 2 | 3,
  ) {
    this.#catalog = catalog;
    this.#storyId = options.storyId;
    this.#variant = options.variant;
    this.#themeRegistry = options.themeRegistry;
    this.#signal = options.signal;
    this.#releaseRenderLock = releaseRenderLock;
    this.#previousChalkLevel = previousChalkLevel;
    const story = this.#story();
    this.#args = Object.freeze({ ...story.args, ...options.args });
    this.#controls = Object.freeze({
      ...defaultTerminalStoryControls,
      ...story.terminal,
      ...options.controls,
    });
  }

  static async open(
    catalog: TuilStoryCatalog,
    options: OpenStoryOptions,
  ): Promise<TuilStorySession> {
    const releaseRenderLock = await storyRenderLock.acquire(options.signal);
    const previousChalkLevel = chalk.level;
    let session: TuilStorySession;
    try {
      session = new TuilStorySession(
        catalog,
        options,
        releaseRenderLock,
        previousChalkLevel,
      );
    } catch (error) {
      chalk.level = previousChalkLevel;
      releaseRenderLock();
      throw error;
    }
    try {
      await session.#render(options.signal);
      return session;
    } catch (error) {
      try {
        await session.#disposeInstance();
      } catch (cleanupError) {
        session.#releaseRenderer();
        throw new AggregateError(
          [error, cleanupError],
          "Story rendering and cleanup failed",
        );
      }
      session.#releaseRenderer();
      throw error;
    }
  }

  get args(): Readonly<Record<string, unknown>> {
    return this.#args;
  }

  get controls(): TerminalStoryControls {
    return this.#controls;
  }

  async setArgs(
    args: Readonly<Record<string, unknown>>,
    signal = this.#signal,
  ): Promise<void> {
    this.#assertOpen();
    signal?.throwIfAborted();
    chalk.level = chalkLevel(this.#controls.colorDepth);
    this.#args = Object.freeze({ ...this.#args, ...args });
    this.#recordAction({ type: "args", timestamp: Date.now(), detail: args });
    this.#instance?.rerender(this.#element());
    await waitWithSignal(Bun.sleep(10), signal);
  }

  async setControls(
    controls: Partial<TerminalStoryControls>,
    signal = this.#signal,
  ): Promise<void> {
    this.#assertOpen();
    signal?.throwIfAborted();
    const previous = this.#controls;
    this.#controls = Object.freeze({ ...previous, ...controls });
    this.#recordAction({
      type: "controls",
      timestamp: Date.now(),
      detail: controls,
    });
    if (controls.width !== undefined || controls.height !== undefined) {
      this.#recordAction({
        type: "resize",
        timestamp: Date.now(),
        detail: {
          width: this.#controls.width,
          height: this.#controls.height,
        },
      });
    }
    // Capabilities and normalized themes are immutable runtime state. Recreate
    // the renderer so every control, including dimensions, reaches components.
    await this.#disposeInstance();
    await this.#render(signal);
  }

  async press(input: string, signal = this.#signal): Promise<void> {
    this.#assertOpen();
    signal?.throwIfAborted();
    chalk.level = chalkLevel(this.#controls.colorDepth);
    this.#recordAction({
      type: "input",
      timestamp: Date.now(),
      detail: { input },
    });
    await waitWithSignal(this.#requireInstance().user.press(input), signal);
  }

  snapshot(): StoryFrame {
    this.#assertOpen();
    const instance = this.#requireInstance();
    const snapshot = instance.screen.snapshot();
    return Object.freeze({
      storyId: this.#storyId,
      variant: this.#variant,
      frame: snapshot.frame,
      ansiFrame: instance.frames.at(-1) ?? snapshot.frame,
      semantics: Object.freeze([...snapshot.nodes]),
      focus: Object.freeze({
        focusedId: instance.app.focus.focusedId,
        nodes: Object.freeze(
          instance.app.focus.nodes().map(({ id, label, role }) => ({
            id,
            label,
            role,
          })),
        ),
      }),
      events: Object.freeze([...this.#events]),
      actions: Object.freeze([...this.#actions]),
      controls: this.#controls,
    });
  }

  async close(signal?: AbortSignal): Promise<void> {
    if (this.#closed) {
      signal?.throwIfAborted();
      return;
    }
    this.#closed = true;
    try {
      await this.#disposeInstance();
    } finally {
      this.#releaseRenderer();
    }
    signal?.throwIfAborted();
  }

  #story() {
    const set = this.#catalog.get(this.#storyId);
    if (!set) throw new Error(`Unknown story set "${this.#storyId}"`);
    const story = set.definition.stories[this.#variant];
    if (!story) {
      throw new Error(
        `Unknown variant "${this.#variant}" in story set "${this.#storyId}"`,
      );
    }
    return story;
  }

  #element(): ReactElement {
    const set = this.#catalog.get(this.#storyId);
    if (!set) throw new Error(`Unknown story set "${this.#storyId}"`);
    return createElement(set.definition.component, this.#args);
  }

  #requireInstance(): TuilTestInstance {
    this.#assertOpen();
    if (!this.#instance) throw new Error("Story session is closed");
    return this.#instance;
  }

  async #render(signal = this.#signal): Promise<void> {
    signal?.throwIfAborted();
    const controls = this.#controls;
    chalk.level = chalkLevel(controls.colorDepth);
    const instance = renderTuil(this.#element() as ReactElement, {
      theme: resolveTheme(controls, this.#themeRegistry),
      terminal: {
        mode: controls.interactive ? "interactive" : "static",
        capabilities: {
          width: controls.width,
          height: controls.height,
          colorDepth: controls.colorDepth,
          unicode: controls.unicode,
          hyperlinks: controls.hyperlinks,
          interactive: controls.interactive,
          tty: controls.interactive,
          alternateScreen: controls.interactive,
          mouse: controls.mouse,
          images: false,
          reducedMotion: controls.reducedMotion,
          platform: controls.platform,
        },
      },
    });
    this.#instance = instance;
    instance.resize(controls.width, controls.height);
    this.#stopObserving = instance.app.events.observe((event) => {
      this.#events.push(event);
      if (this.#events.length > 200) this.#events.shift();
    });
    try {
      await waitWithSignal(instance.ready, signal);
    } catch (error) {
      await this.#disposeInstance().catch(() => undefined);
      throw error;
    }
    this.#recordAction({ type: "render", timestamp: Date.now() });
  }

  async #disposeInstance(): Promise<void> {
    this.#stopObserving?.();
    this.#stopObserving = undefined;
    const instance = this.#instance;
    this.#instance = undefined;
    if (instance) await instance.cleanup();
  }

  #recordAction(action: StoryAction): void {
    this.#actions.push(action);
    if (this.#actions.length > 200) this.#actions.shift();
  }

  #assertOpen(): void {
    if (this.#closed) throw new Error("Story session is closed");
  }

  #releaseRenderer(): void {
    if (this.#released) return;
    this.#released = true;
    chalk.level = this.#previousChalkLevel;
    this.#releaseRenderLock();
  }

  #released = false;
}
```

## Members [#members]

| Member                | Type                                                                                             | Required | Description                                                                                                                                  | Related types                                                                         |
| --------------------- | ------------------------------------------------------------------------------------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `#catalog`            | `TuilStoryCatalog`                                                                               | Yes      | The `#catalog` member uses the `TuilStoryCatalog` contract.                                                                                  | [`TuilStoryCatalog`](/tuil/docs/reference/packages/story/api/tuil-story-catalog)           |
| `#storyId`            | `string`                                                                                         | Yes      | The `#storyId` member uses the `string` contract.                                                                                            | —                                                                                     |
| `#variant`            | `string`                                                                                         | Yes      | The `#variant` member uses the `string` contract.                                                                                            | —                                                                                     |
| `#themeRegistry`      | `ThemeRegistry \| undefined`                                                                     | No       | The `#themeRegistry` member uses the `ThemeRegistry \| undefined` contract.                                                                  | [`ThemeRegistry`](/tuil/docs/reference/packages/theme/api/theme-registry)                  |
| `#signal`             | `AbortSignal \| undefined`                                                                       | No       | The `#signal` member uses the `AbortSignal \| undefined` contract.                                                                           | —                                                                                     |
| `#releaseRenderLock`  | `() => void`                                                                                     | Yes      | The `#releaseRenderLock` member uses the `() => void` contract.                                                                              | —                                                                                     |
| `#previousChalkLevel` | `0 \| 1 \| 2 \| 3`                                                                               | Yes      | The `#previousChalkLevel` member uses the `0 \| 1 \| 2 \| 3` contract.                                                                       | —                                                                                     |
| `#events`             | `ObservedEvent[]`                                                                                | Yes      | The `#events` member uses the `ObservedEvent[]` contract.                                                                                    | [`ObservedEvent`](/tuil/docs/reference/packages/events/api/observed-event)                 |
| `#actions`            | `StoryAction[]`                                                                                  | Yes      | The `#actions` member uses the `StoryAction[]` contract.                                                                                     | [`StoryAction`](/tuil/docs/reference/packages/story/api/story-action)                      |
| `#args`               | `Readonly<Record<string, unknown>>`                                                              | Yes      | The `#args` member uses the `Readonly<Record<string, unknown>>` contract.                                                                    | —                                                                                     |
| `#controls`           | `TerminalStoryControls`                                                                          | Yes      | The `#controls` member uses the `TerminalStoryControls` contract.                                                                            | [`TerminalStoryControls`](/tuil/docs/reference/packages/story/api/terminal-story-controls) |
| `#instance`           | `TuilTestInstance \| undefined`                                                                  | No       | The `#instance` member uses the `TuilTestInstance \| undefined` contract.                                                                    | [`TuilTestInstance`](/tuil/docs/reference/packages/testing-ink/api/tuil-test-instance)     |
| `#stopObserving`      | `(() => void) \| undefined`                                                                      | No       | The `#stopObserving` member uses the `(() => void) \| undefined` contract.                                                                   | —                                                                                     |
| `#closed`             | `boolean`                                                                                        | Yes      | The `#closed` member uses the `boolean` contract.                                                                                            | —                                                                                     |
| `open`                | `typeof TuilStorySession.open`                                                                   | Yes      | The `open` member uses the `typeof TuilStorySession.open` contract.                                                                          | [`TuilStorySession`](/tuil/docs/reference/packages/story/api/tuil-story-session)           |
| `args`                | `Readonly<Record<string, unknown>>`                                                              | Yes      | The `args` member uses the `Readonly<Record<string, unknown>>` contract.                                                                     | —                                                                                     |
| `controls`            | `TerminalStoryControls`                                                                          | Yes      | The `controls` member uses the `TerminalStoryControls` contract.                                                                             | [`TerminalStoryControls`](/tuil/docs/reference/packages/story/api/terminal-story-controls) |
| `setArgs`             | `(args: Readonly<Record<string, unknown>>, signal?: AbortSignal \| undefined) => Promise<void>`  | Yes      | The `setArgs` member uses the `(args: Readonly<Record<string, unknown>>, signal?: AbortSignal \| undefined) => Promise<void>` contract.      | —                                                                                     |
| `setControls`         | `(controls: Partial<TerminalStoryControls>, signal?: AbortSignal \| undefined) => Promise<void>` | Yes      | The `setControls` member uses the `(controls: Partial<TerminalStoryControls>, signal?: AbortSignal \| undefined) => Promise<void>` contract. | [`TerminalStoryControls`](/tuil/docs/reference/packages/story/api/terminal-story-controls) |
| `press`               | `(input: string, signal?: AbortSignal \| undefined) => Promise<void>`                            | Yes      | The `press` member uses the `(input: string, signal?: AbortSignal \| undefined) => Promise<void>` contract.                                  | —                                                                                     |
| `snapshot`            | `() => StoryFrame`                                                                               | Yes      | The `snapshot` member uses the `() => StoryFrame` contract.                                                                                  | [`StoryFrame`](/tuil/docs/reference/packages/story/api/story-frame)                        |
| `close`               | `(signal?: AbortSignal) => Promise<void>`                                                        | Yes      | The `close` member uses the `(signal?: AbortSignal) => Promise<void>` contract.                                                              | —                                                                                     |
| `#story`              | `() => TuilStory<Record<string, unknown>>`                                                       | Yes      | The `#story` member uses the `() => TuilStory<Record<string, unknown>>` contract.                                                            | [`TuilStory`](/tuil/docs/reference/packages/story/api/tuil-story)                          |
| `#element`            | `() => ReactElement`                                                                             | Yes      | The `#element` member uses the `() => ReactElement` contract.                                                                                | —                                                                                     |
| `#requireInstance`    | `() => TuilTestInstance`                                                                         | Yes      | The `#requireInstance` member uses the `() => TuilTestInstance` contract.                                                                    | [`TuilTestInstance`](/tuil/docs/reference/packages/testing-ink/api/tuil-test-instance)     |
| `#render`             | `(signal?: AbortSignal \| undefined) => Promise<void>`                                           | Yes      | The `#render` member uses the `(signal?: AbortSignal \| undefined) => Promise<void>` contract.                                               | —                                                                                     |
| `#disposeInstance`    | `() => Promise<void>`                                                                            | Yes      | The `#disposeInstance` member uses the `() => Promise<void>` contract.                                                                       | —                                                                                     |
| `#recordAction`       | `(action: StoryAction) => void`                                                                  | Yes      | The `#recordAction` member uses the `(action: StoryAction) => void` contract.                                                                | [`StoryAction`](/tuil/docs/reference/packages/story/api/story-action)                      |
| `#assertOpen`         | `() => void`                                                                                     | Yes      | The `#assertOpen` member uses the `() => void` contract.                                                                                     | —                                                                                     |
| `#releaseRenderer`    | `() => void`                                                                                     | Yes      | The `#releaseRenderer` member uses the `() => void` contract.                                                                                | —                                                                                     |
| `#released`           | `boolean`                                                                                        | Yes      | The `#released` member uses the `boolean` contract.                                                                                          | —                                                                                     |

## Parameters [#parameters]

This declaration has no public members.

## Returns [#returns]

This declaration does not return a value.

## Throws [#throws]

No thrown errors are documented for this declaration.

## Related types [#related-types]

* [`OpenStoryOptions`](/tuil/docs/reference/packages/story/api/open-story-options)
* [`StoryAction`](/tuil/docs/reference/packages/story/api/story-action)
* [`StoryFrame`](/tuil/docs/reference/packages/story/api/story-frame)
* [`TerminalStoryControls`](/tuil/docs/reference/packages/story/api/terminal-story-controls)
* [`TuilStoryCatalog`](/tuil/docs/reference/packages/story/api/tuil-story-catalog)

## Source [#source]

[View the secondary source reference](https://github.com/mwillbanks/tuil/blob/main/packages/story/src/index.tsx)

## Package [#package]

[@mwillbanks/tuil-story](/tuil/docs/reference/packages/story)
