Skip to content
Closed
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
4 changes: 3 additions & 1 deletion .github/workflows/publish-npm-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ on:
workflow_dispatch:
inputs:
package:
description: "Package to publish (react-ui, react-headless, openui-cli or react-lang)"
description: "Package to publish (react-ui, react-headless, svelte-headless, openui-cli, react-lang or svelte-lang)"
required: true
type: choice
options:
- react-ui
- react-headless
- svelte-headless
- react-lang
- svelte-lang
- openui-cli

jobs:
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ Try it yourself in the [Playground](https://www.openui.com/playground) — gener
| Package | Description |
| :--- | :--- |
| [`@openuidev/react-lang`](./packages/react-lang) | Core runtime — component definitions, parser, renderer, prompt generation |
| [`@openuidev/svelte-lang`](./packages/svelte-lang) | Svelte runtime — component definitions, parser, renderer, prompt generation |
| [`@openuidev/react-headless`](./packages/react-headless) | Headless chat state, streaming adapters, message format converters |
| [`@openuidev/svelte-headless`](./packages/svelte-headless) | Headless Svelte chat state, streaming adapters, message format converters |
| [`@openuidev/react-ui`](./packages/react-ui) | Prebuilt chat layouts and two built-in component libraries |
| [`@openuidev/cli`](./packages/openui-cli) | CLI for scaffolding apps and generating system prompts |

Expand Down Expand Up @@ -132,7 +134,9 @@ Detailed documentation is available at [openui.com](https://openui.com).
openui/
├── packages/
│ ├── react-lang/ # Core runtime (parser, renderer, prompt generation)
│ ├── svelte-lang/ # Svelte runtime (parser, renderer, prompt generation)
│ ├── react-headless/ # Headless chat state & streaming adapters
│ ├── svelte-headless/ # Headless chat state & streaming adapters for Svelte
│ ├── react-ui/ # Prebuilt chat layouts & component libraries
│ └── openui-cli/ # CLI for scaffolding & prompt generation
├── examples/
Expand Down
3 changes: 3 additions & 0 deletions packages/svelte-headless/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @openuidev/svelte-headless

Svelte 5 bindings for the OpenUI headless chat runtime.
63 changes: 63 additions & 0 deletions packages/svelte-headless/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"name": "@openuidev/svelte-headless",
"version": "0.1.0",
"description": "Svelte 5 headless chat primitives for OpenUI",
"license": "MIT",
"type": "module",
"svelte": "./dist/index.js",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"README.md"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"svelte": "./dist/index.js"
}
},
"scripts": {
"build": "svelte-package",
"check": "svelte-check --tsconfig ./tsconfig.json",
"test": "vitest run",
"format:check": "prettier --check ./src",
"ci": "pnpm run check && pnpm run test && pnpm run format:check"
},
"keywords": [
"openui",
"svelte",
"headless",
"chat",
"streaming",
"ai",
"zustand"
],
"homepage": "https://openui.com",
"repository": {
"type": "git",
"url": "https://github.com/thesysdev/openui.git",
"directory": "packages/svelte-headless"
},
"bugs": {
"url": "https://github.com/thesysdev/openui/issues"
},
"author": "engineering@thesys.dev",
"dependencies": {
"@ag-ui/core": "^0.0.45",
"zustand": "^4.5.5"
},
"peerDependencies": {
"svelte": ">=5.0.0",
"zustand": "^4.5.5"
},
"devDependencies": {
"@sveltejs/package": "^2.3.7",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"jsdom": "^28.1.0",
"openai": "^6.22.0",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"vitest": "^4.0.18"
}
}
18 changes: 18 additions & 0 deletions packages/svelte-headless/src/lib/ChatProvider.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script lang="ts">
import type { ChatStore } from "./internal";
import type { Snippet } from "svelte";
import type { StoreApi } from "zustand";
import { provideChatStoreContext } from "./context";

let {
store,
children,
}: {
store: StoreApi<ChatStore>;
children?: Snippet;
} = $props();

provideChatStoreContext(() => store);
</script>

{@render children?.()}
28 changes: 28 additions & 0 deletions packages/svelte-headless/src/lib/MessageProvider.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<script lang="ts">
import type { Message } from "./internal";
import type { Snippet } from "svelte";
import { writable } from "svelte/store";
import { provideMessageStoreContext } from "./context";

let {
message,
children,
}: {
message: Message;
children?: Snippet;
} = $props();

const messageStore = writable<Message>({
id: "",
role: "assistant",
content: "",
} as Message);

provideMessageStoreContext(() => messageStore);

$effect(() => {
messageStore.set(message);
});
</script>

{@render children?.()}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script lang="ts">
import type { Message } from "../../index";
import MessageProvider from "../../MessageProvider.svelte";
import MessageSummary from "./MessageSummary.svelte";

let { message }: { message: Message } = $props();
</script>

<MessageProvider {message}>
<MessageSummary />
</MessageProvider>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script lang="ts">
import { getMessageStore } from "../../context";

const message = getMessageStore();
</script>

<p>{$message.content}</p>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script lang="ts">
import type { ChatStore } from "../../index";
import type { StoreApi } from "zustand";
import ChatProvider from "../../ChatProvider.svelte";
import ThreadSummary from "./ThreadSummary.svelte";

let { store }: { store: StoreApi<ChatStore> } = $props();
</script>

<ChatProvider {store}>
<ThreadSummary />
</ChatProvider>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script lang="ts">
import { getThreadListStore } from "../../stores";

const threadList = getThreadListStore();
</script>

<p>{$threadList.threads.length}</p>
45 changes: 45 additions & 0 deletions packages/svelte-headless/src/lib/__tests__/providers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { flushSync, mount, unmount } from "svelte";
import { describe, expect, it, vi } from "vitest";
import { createChatStore } from "../index";
import type { Message } from "../index";
import MessageProviderHarness from "./fixtures/MessageProviderHarness.svelte";
import ThreadProviderHarness from "./fixtures/ThreadProviderHarness.svelte";

describe("@openuidev/svelte-headless", () => {
it("exposes reactive thread state through ChatProvider context", () => {
const store = createChatStore({ processMessage: vi.fn() });
store.setState({
threads: [{ id: "t1", title: "First", createdAt: new Date().toISOString() }],
});

const component = mount(ThreadProviderHarness, {
target: document.body,
props: { store },
});

flushSync();

expect(document.body.textContent).toContain("1");

unmount(component);
});

it("exposes message context through MessageProvider", () => {
const message = {
id: "m1",
role: "assistant",
content: "hello from context",
} as Message;

const component = mount(MessageProviderHarness, {
target: document.body,
props: { message },
});

flushSync();

expect(document.body.textContent).toContain("hello from context");

unmount(component);
});
});
23 changes: 23 additions & 0 deletions packages/svelte-headless/src/lib/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import type { ChatStore, Message } from "./internal";
import { createContext } from "svelte";
import { get, type Readable } from "svelte/store";
import type { StoreApi } from "zustand";

const [consumeChatStoreContext, provideChatStoreContext] = createContext<() => StoreApi<ChatStore>>();
const [consumeMessageStoreContext, provideMessageStoreContext] = createContext<
() => Readable<Message>
>();

export { provideChatStoreContext, provideMessageStoreContext };

export function getChatStore(): StoreApi<ChatStore> {
return consumeChatStoreContext()();
}

export function getMessageStore(): Readable<Message> {
return consumeMessageStoreContext()();
}

export function getMessage(): Message {
return get(getMessageStore());
}
52 changes: 52 additions & 0 deletions packages/svelte-headless/src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export { default as ChatProvider } from "./ChatProvider.svelte";
export { default as MessageProvider } from "./MessageProvider.svelte";

export { createChatStore } from "./internal";
export {
agUIAdapter,
openAIAdapter,
openAIReadableStreamAdapter,
openAIResponsesAdapter,
openAIConversationMessageFormat,
openAIMessageFormat,
processStreamedMessage,
identityMessageFormat,
EventType,
} from "./internal";
export type {
AGUIEvent,
ActivityMessage,
AssistantMessage,
BinaryInputContent,
ChatStore,
ChatStoreConfig,
CreateChatStoreOptions,
CreateMessage,
DeveloperMessage,
FunctionCall,
InputContent,
Message,
MessageFormat,
ReasoningMessage,
StreamProtocolAdapter,
SystemMessage,
TextInputContent,
Thread,
ThreadActions,
ThreadListActions,
ThreadListState,
ThreadState,
ToolCall,
ToolMessage,
UserMessage,
} from "./internal";

export { getChatStore, getMessage, getMessageStore } from "./context";
export {
createThreadListStore,
createThreadStore,
getMessageSnapshot,
getThreadListStore,
getThreadStore,
selectChatStore,
} from "./stores";
42 changes: 42 additions & 0 deletions packages/svelte-headless/src/lib/internal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
export {
agUIAdapter,
openAIAdapter,
openAIReadableStreamAdapter,
openAIResponsesAdapter,
} from "./stream/adapters";
export { openAIConversationMessageFormat, openAIMessageFormat } from "./stream/formats";
export { processStreamedMessage } from "./stream/processStreamedMessage";

export { createChatStore } from "./store/createChatStore";
export type {
ChatStore,
ChatStoreConfig,
CreateChatStoreOptions,
CreateMessage,
Thread,
ThreadActions,
ThreadListActions,
ThreadListState,
ThreadState,
} from "./store/types";

export type {
ActivityMessage,
AssistantMessage,
BinaryInputContent,
DeveloperMessage,
FunctionCall,
InputContent,
Message,
ReasoningMessage,
SystemMessage,
TextInputContent,
ToolCall,
ToolMessage,
UserMessage,
} from "./types/message";

export { identityMessageFormat } from "./types/messageFormat";
export type { MessageFormat } from "./types/messageFormat";
export { EventType } from "./types/stream";
export type { AGUIEvent, StreamProtocolAdapter } from "./types/stream";
Loading