Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* Global oai object injected by the web sandbox for communicating with chatgpt host page.
*/
import type { API, OpenAiGlobals, type SET_GLOBALS_EVENT_TYPE, type SetGlobalsEvent } from "./types"
declare global {
interface DefaultConfig {
LinkComponent: "a"
Expand All @@ -16,6 +20,13 @@ declare global {
export type LinkComponent = Config["LinkComponent"]
export type Breakpoint = Config["Breakpoint"]
}
interface Window {
openai: API & OpenAiGlobals
}

interface WindowEventMap {
[SET_GLOBALS_EVENT_TYPE]: SetGlobalsEvent
}
}

export {}
96 changes: 96 additions & 0 deletions src/hooks/OpenAI globals.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Meta, Subtitle, Title } from "@storybook/blocks"

<Meta title="Hooks/OpenAI globals" />

<Title>OpenAI globals hooks</Title>
<Subtitle>Subscribe to ChatGPT host values like layout and widget state</Subtitle>

## Overview
ChatGPT injects a `window.openai` object that includes layout details, locale, safe area, tool IO, and widget state. The hooks below subscribe to `openai:set_globals` events so your UI stays in sync with host updates. Until the host provides data they return `null`, so add sensible fallbacks for local development. See the [ChatGPT UI guide](https://developers.openai.com/apps-sdk/build/chatgpt-ui#managing-state) for the full host API.

## State placement
- **Business data (authoritative)** – lives on your MCP server or backend; return fresh snapshots via tools.
- **Widget UI state (ephemeral)** – lives in `window.openai.widgetState` and should be updated through `useWidgetState` after every meaningful UI change.
- **Cross-session state (durable)** – store in your backend if it must survive across conversations or devices.

## `useOpenAiGlobal`
Base hook for reading any key from `window.openai` with live updates. Pass the key you want and receive a typed value.

```tsx
import { useOpenAiGlobal } from "@openai/apps-sdk-ui/hooks/useOpenaiGlobal"

function SafeAreaAwarePanel() {
const safeArea = useOpenAiGlobal("safeArea")
const locale = useOpenAiGlobal("locale")

return (
<section style={{ paddingBottom: safeArea?.insets.bottom ?? 16 }}>
<p>Current locale: {locale ?? "loading..."}</p>
</section>
)
}
```

## Layout hooks
### `useDisplayMode`
Returns the current display mode (`pip`, `inline`, or `fullscreen`) granted by the host. Useful for adjusting density or controls when PiP or fullscreen is active.

### `useMaxHeight`
Returns the available vertical space in pixels for the widget, or `null` while loading. Combine with overflow styles to keep content scrollable.

```tsx
import { useDisplayMode } from "@openai/apps-sdk-ui/hooks/useDisplayMode"
import { useMaxHeight } from "@openai/apps-sdk-ui/hooks/useMaxHeight"

function WidgetFrame({ children }: { children: React.ReactNode }) {
const displayMode = useDisplayMode()
const maxHeight = useMaxHeight()

return (
<div
className={displayMode === "pip" ? "rounded-xl shadow-lg" : undefined}
style={{ maxHeight: maxHeight ?? undefined, overflow: "auto" }}
>
{children}
</div>
)
}
```

## Widget data
### `useWidgetProps`
Reads the latest `toolOutput` provided by the host and falls back to an optional default (value or lazy initializer) when developing outside ChatGPT.

```tsx
import { useWidgetProps } from "@openai/apps-sdk-ui/hooks/useWidgetProps"

type ReservationProps = { venue: string; time: string }

const props = useWidgetProps<ReservationProps>(() => ({ venue: "Loading", time: "" }))
```

### `useWidgetState`
Synchronizes widget UI state with ChatGPT. The hook hydrates from `window.openai.widgetState` when the widget mounts (or falls back to your initializer), subscribes to host changes, and mirrors writes through `window.openai.setWidgetState` so the host persists the snapshot for this widget instance. State is message-scoped and survives re-opens of the same widget but not new runs.

```tsx
import { useWidgetState } from "@openai/apps-sdk-ui/hooks/useWidgetState"

type Filters = { cuisine: string | null; guests: number }

function FiltersPanel() {
const [filters, setFilters] = useWidgetState<Filters>(() => ({ cuisine: null, guests: 2 }))

return (
<div className="space-y-3">
<button onClick={() => setFilters((prev) => ({ ...prev, cuisine: "italian" }))}>
Set to Italian
</button>
<button onClick={() => setFilters((prev) => ({ ...prev, guests: prev.guests + 1 }))}>
Add guest
</button>
</div>
)
}
```

All hooks return `null` until `window.openai` is available. Provide defaults where appropriate so your components render gracefully in non-ChatGPT environments.
6 changes: 6 additions & 0 deletions src/hooks/useDisplayMode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { type DisplayMode } from "../types"
import { useOpenAiGlobal } from "./useOpenaiGlobal"

export const useDisplayMode = (): DisplayMode | null => {
return useOpenAiGlobal("displayMode")
}
5 changes: 5 additions & 0 deletions src/hooks/useMaxHeight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { useOpenAiGlobal } from "./useOpenaiGlobal"

export const useMaxHeight = (): number | null => {
return useOpenAiGlobal("maxHeight")
}
32 changes: 32 additions & 0 deletions src/hooks/useOpenaiGlobal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useSyncExternalStore } from "react"
import { SET_GLOBALS_EVENT_TYPE, SetGlobalsEvent, type OpenAiGlobals } from "../types"

export function useOpenAiGlobal<K extends keyof OpenAiGlobals>(key: K): OpenAiGlobals[K] | null {
return useSyncExternalStore(
(onChange) => {
if (typeof window === "undefined") {
return () => {}
}

const handleSetGlobal: EventListener = (event) => {
const { globals } = (event as SetGlobalsEvent).detail
const value = globals[key]
if (value === undefined) {
return
}

onChange()
}

window.addEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal, {
passive: true,
})

return () => {
window.removeEventListener(SET_GLOBALS_EVENT_TYPE, handleSetGlobal)
}
},
() => window.openai?.[key] ?? null,
() => window.openai?.[key] ?? null,
)
}
10 changes: 10 additions & 0 deletions src/hooks/useWidgetProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useOpenAiGlobal } from "./useOpenaiGlobal"

export function useWidgetProps<T extends Record<string, unknown>>(defaultState?: T | (() => T)): T {
const props = useOpenAiGlobal("toolOutput") as T

const fallback =
typeof defaultState === "function" ? (defaultState as () => T | null)() : defaultState ?? null

return props ?? fallback
}
Loading