@jigs.design/react 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tom Bates
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,53 @@
1
- # Temporary Holding Version
1
+ # @jigs.design/react
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The web renderer. `PickleView` runs a compiled program in a sandboxed frame and draws it with your registry. `usePickleSession` holds a conversation with the server and streams its latest program.
4
+
5
+ ```tsx
6
+ import { components } from "@jigs.design/catalog/react";
7
+ import { PickleView, usePickleSession } from "@jigs.design/react";
8
+ import { client } from "./pickle-client";
9
+
10
+ export function App() {
11
+ const session = usePickleSession(client);
12
+ return (
13
+ <>
14
+ <button onClick={() => session.send("My open orders")}>Ask</button>
15
+ {session.program && (
16
+ <PickleView
17
+ client={client}
18
+ components={components}
19
+ documentId={session.documentId}
20
+ program={session.program}
21
+ />
22
+ )}
23
+ </>
24
+ );
25
+ }
26
+ ```
27
+
28
+ The program runs in a worker inside a frame served by your Pickle route. The frame has no network: queries and mutations go through the client, and every prop is checked against the program's schema before it reaches your component. A new `program` replaces the running one and keeps the state it still declares. `onAction` handles the program's actions after the server checked their input. The view drops an `autoFocus` prop unless you pass `allowAutoFocus`, so a pickle streaming into a chat cannot take the focus. It checks what the worker asks for against the program, since the worker is not trusted.
29
+
30
+ ## Exports
31
+
32
+ | Export | What it is |
33
+ | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
34
+ | `PickleView` | Runs one program and draws it. Takes `client`, `components`, `program`, `onAction`, `showIssues`, `allowAutoFocus` |
35
+ | `usePickleSession` | The conversation and its latest program. `initialMessages` resumes one |
36
+ | `useHandlerSettled` | For a registry control: changes once the program has answered its events, so it can drop an edit the program refused |
37
+ | `useLocalValue`, `controlled` | For a controlled control: a local copy of the program's value that runs ahead of the round trip and drops a refused edit |
38
+ | `Registry`, `RegistryFor` | The type of a map from catalog names to React components |
39
+ | `SessionState`, `PickleSession` | The session's state and the hook's return value |
40
+ | `ViewIssue` | What `onIssue` receives |
41
+
42
+ It re-exports the shared types from `@jigs.design/runtime`. The frame, its messages and the calls through the client are `createViewController` in `@jigs.design/client`, which `PickleView` is built on; a renderer for another framework starts there.
43
+
44
+ ## Source
45
+
46
+ - `src/view/PickleView.tsx`: the view component, on the client's view controller.
47
+ - `src/view/registry.ts`, `src/view/settled.ts`: the registry types and `useHandlerSettled`.
48
+ - `src/view/local-value.ts`: `useLocalValue` and `controlled`, for controls.
49
+ - `src/view/session.ts`, `src/view/usePickleSession.ts`: the session store and its hook.
50
+
51
+ ## Docs
52
+
53
+ [React](../../apps/docs/docs/react/index.mdx), [Registry](../../apps/docs/docs/react/registry.mdx), [Sandbox](../../apps/docs/docs/react/sandbox.mdx), [Session](../../apps/docs/docs/running/session.mdx), [Renderers](../../apps/docs/docs/renderers/index.mdx).
@@ -0,0 +1,9 @@
1
+ import { controlled, useLocalValue } from "./view/local-value.js";
2
+ import { Registry, RegistryFor } from "./view/registry.js";
3
+ import { PickleView } from "./view/PickleView.js";
4
+ import { useHandlerSettled } from "./view/settled.js";
5
+ import { SessionState } from "./view/session.js";
6
+ import { PickleSession, usePickleSession } from "./view/usePickleSession.js";
7
+ import { ConfirmFunction, ViewIssue } from "@jigs.design/client";
8
+ import { Diagnostic, Message, PickleErrorCode, PickleErrorData, Program, SessionEvent, SessionOptions, Span } from "@jigs.design/runtime";
9
+ export { type ConfirmFunction, type Diagnostic, type Message, type PickleErrorCode, type PickleErrorData, type PickleSession, PickleView, type Program, type Registry, type RegistryFor, type SessionEvent, type SessionOptions, type SessionState, type Span, type ViewIssue, controlled, useHandlerSettled, useLocalValue, usePickleSession };
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ import { useHandlerSettled } from "./view/settled.js";
2
+ import { controlled, useLocalValue } from "./view/local-value.js";
3
+ import { PickleView } from "./view/PickleView.js";
4
+ import { usePickleSession } from "./view/usePickleSession.js";
5
+ export { PickleView, controlled, useHandlerSettled, useLocalValue, usePickleSession };
@@ -0,0 +1,63 @@
1
+ import { Registry } from "./registry.js";
2
+ import { ConfirmFunction, PickleClient, ViewIssue } from "@jigs.design/client";
3
+ import { Program } from "@jigs.design/runtime";
4
+ //#region src/view/PickleView.d.ts
5
+ /** An action as `onAction` receives it; type it with `ActionEvent<typeof host>`. */
6
+ interface ActionLike {
7
+ input: unknown;
8
+ name: string;
9
+ }
10
+ /**
11
+ * Runs one program in the sandbox and draws it with `components`. A new
12
+ * `program` replaces the running one; state the new program still declares
13
+ * keeps its value, unless `documentId` changed with it. A new `client`
14
+ * starts the program over on that client.
15
+ */
16
+ export declare function PickleView<TAction extends ActionLike = ActionLike>(props: {
17
+ /**
18
+ * Let the program focus a control when it appears, through the
19
+ * `autoFocus` prop. Default false: in a chat, a UI that streams in would
20
+ * otherwise take the focus from the composer while the user types.
21
+ */
22
+ allowAutoFocus?: boolean;
23
+ client: PickleClient;
24
+ components: Registry;
25
+ /**
26
+ * Which UI `program` is a version of. When it changes, the program starts
27
+ * from fresh state instead of keeping the state of the one before. Pass
28
+ * `session.documentId`, which changes when the model writes a new UI
29
+ * rather than editing the last one. Leave it out to keep state across
30
+ * every program, as for edits.
31
+ */
32
+ documentId?: number | string;
33
+ /**
34
+ * Handles the program's actions, after the server checked their input.
35
+ * What it returns, awaited and copied with `structuredClone`, is the
36
+ * handle's `data`; what it throws is its `error`. Without it, every
37
+ * action fails. It is not called once the view has unmounted.
38
+ */
39
+ onAction?: (action: TAction) => unknown;
40
+ /**
41
+ * Asks the user to approve a mutation whose policy is "confirm", with the
42
+ * host's `message`, in the page's own UI: the sandboxed program never
43
+ * draws or answers it. Resolve true to run the mutation; false, or a
44
+ * throw, fails it with `DECLINED`. Without it, the client's `confirm` asks.
45
+ */
46
+ onConfirm?: ConfirmFunction;
47
+ /**
48
+ * Called once for each new issue: a runtime error, something the view
49
+ * dropped, or a sandbox notice. An issue that persists is reported once;
50
+ * one that clears and comes back, again. It runs whether or not
51
+ * `showIssues` is on, so an app can count issues where users don't see
52
+ * them.
53
+ */
54
+ onIssue?: (issue: ViewIssue) => void;
55
+ program: Program;
56
+ /**
57
+ * List runtime errors, dropped props and sandbox notices under the tree.
58
+ * Default true; turn it off where end users should not see them.
59
+ */
60
+ showIssues?: boolean;
61
+ }): import("react").JSX.Element;
62
+ //#endregion
63
+ export {};
@@ -0,0 +1,106 @@
1
+ import { SettledContext } from "./settled.js";
2
+ import { useEffect, useState, useSyncExternalStore } from "react";
3
+ import { createViewController, missingComponents } from "@jigs.design/client";
4
+ import { ROOT_ID } from "@jigs.design/runtime";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ //#region src/view/PickleView.tsx
7
+ function TreeNodeView(props) {
8
+ const { components, controller } = props;
9
+ const getNode = () => controller.store.get(props.id);
10
+ const node = useSyncExternalStore(controller.subscribe, getNode, getNode);
11
+ const getSettled = () => controller.getSettled(props.id);
12
+ const settled = useSyncExternalStore(controller.subscribe, getSettled, getSettled);
13
+ if (node === void 0) return null;
14
+ if (node.type === "#text") return node.text;
15
+ const children = node.children.map((child) => /* @__PURE__ */ jsx(TreeNodeView, {
16
+ allowAutoFocus: props.allowAutoFocus,
17
+ components,
18
+ controller,
19
+ id: child
20
+ }, child));
21
+ if (node.type === "#root") return children;
22
+ const registered = Object.hasOwn(components, node.type) ? components[node.type] : void 0;
23
+ if (registered === void 0) return null;
24
+ const drawn = controller.drawnProps(node, { allowAutoFocus: props.allowAutoFocus });
25
+ return /* @__PURE__ */ jsx(SettledContext, {
26
+ value: settled,
27
+ children: /* @__PURE__ */ jsx(registered, {
28
+ ...drawn,
29
+ children: controller.childrenAllowed(node.type) && children.length > 0 ? children : void 0
30
+ })
31
+ });
32
+ }
33
+ const missingFromRegistry = (program, components) => missingComponents(program, Object.keys(components));
34
+ /**
35
+ * Runs one program in the sandbox and draws it with `components`. A new
36
+ * `program` replaces the running one; state the new program still declares
37
+ * keeps its value, unless `documentId` changed with it. A new `client`
38
+ * starts the program over on that client.
39
+ */
40
+ function PickleView(props) {
41
+ const { program } = props;
42
+ const [view, setView] = useState(() => ({
43
+ client: props.client,
44
+ controller: createViewController(props.client)
45
+ }));
46
+ let { controller } = view;
47
+ if (view.client !== props.client) {
48
+ controller = createViewController(props.client);
49
+ setView({
50
+ client: props.client,
51
+ controller
52
+ });
53
+ }
54
+ const status = useSyncExternalStore(controller.subscribe, controller.getStatus, controller.getStatus);
55
+ useEffect(() => {
56
+ controller.setIssueHandler(props.onIssue);
57
+ }, [controller, props.onIssue]);
58
+ useEffect(() => {
59
+ controller.start();
60
+ return () => controller.stop();
61
+ }, [controller]);
62
+ useEffect(() => {
63
+ controller.setUnregistered(missingFromRegistry(program, props.components));
64
+ }, [
65
+ controller,
66
+ program,
67
+ props.components
68
+ ]);
69
+ useEffect(() => {
70
+ const handler = props.onAction;
71
+ controller.setActionHandler(handler);
72
+ }, [controller, props.onAction]);
73
+ useEffect(() => {
74
+ controller.setConfirmHandler(props.onConfirm);
75
+ }, [controller, props.onConfirm]);
76
+ const documentId = props.documentId;
77
+ useEffect(() => {
78
+ controller.load(program, documentId);
79
+ }, [
80
+ controller,
81
+ program,
82
+ documentId
83
+ ]);
84
+ const showIssues = props.showIssues ?? true;
85
+ const issues = showIssues ? [...missingFromRegistry(program, props.components), ...status.issues] : [];
86
+ const errors = showIssues ? status.errors : [];
87
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
88
+ /* @__PURE__ */ jsx(TreeNodeView, {
89
+ allowAutoFocus: props.allowAutoFocus === true,
90
+ components: props.components,
91
+ controller,
92
+ id: ROOT_ID
93
+ }),
94
+ showIssues && status.notice !== null && /* @__PURE__ */ jsx("p", {
95
+ className: "pickle-view-notice",
96
+ role: "status",
97
+ children: status.notice
98
+ }),
99
+ errors.length + issues.length > 0 && /* @__PURE__ */ jsxs("ul", {
100
+ className: "pickle-view-errors",
101
+ children: [errors.map((error, index) => /* @__PURE__ */ jsxs("li", { children: [error.line === null ? "" : `Line ${error.line}: `, error.message] }, `error:${index}:${error.message}`)), issues.map((issue) => /* @__PURE__ */ jsx("li", { children: issue }, `issue:${issue}`))]
102
+ })
103
+ ] });
104
+ }
105
+ //#endregion
106
+ export { PickleView };
@@ -0,0 +1,19 @@
1
+ //#region src/view/local-value.d.ts
2
+ /**
3
+ * Whether the program controls a control's value: it sets the value prop
4
+ * (`value`, or `checked`) and handles `onChange`. A control with only
5
+ * `onChange` is uncontrolled, like a React input without `value`: the
6
+ * program hears each edit but the control keeps what the user did.
7
+ */
8
+ export declare const controlled: <T>(value: T | undefined, onChange: ((next: T) => void) | undefined) => boolean;
9
+ /**
10
+ * A local copy of a value the program owns, so a control can run ahead of the
11
+ * round trip to the sandbox. Pass `echoes` when the program controls the
12
+ * value (`controlled`): echoes of the edits sent to it then don't reset the
13
+ * control, but any other new value does, and so does an edit the program did
14
+ * not take: once it has answered every call (`useHandlerSettled`) and `value`
15
+ * is still not the edit, the control goes back to `value`. Without `echoes`,
16
+ * the control keeps its own state until `value` changes.
17
+ */
18
+ export declare function useLocalValue<T>(value: T, echoes: boolean): [T, (next: T) => void];
19
+ //#endregion
@@ -0,0 +1,47 @@
1
+ import { useHandlerSettled } from "./settled.js";
2
+ import { useState } from "react";
3
+ //#region src/view/local-value.ts
4
+ /**
5
+ * Whether the program controls a control's value: it sets the value prop
6
+ * (`value`, or `checked`) and handles `onChange`. A control with only
7
+ * `onChange` is uncontrolled, like a React input without `value`: the
8
+ * program hears each edit but the control keeps what the user did.
9
+ */
10
+ const controlled = (value, onChange) => value !== void 0 && onChange !== void 0;
11
+ /**
12
+ * A local copy of a value the program owns, so a control can run ahead of the
13
+ * round trip to the sandbox. Pass `echoes` when the program controls the
14
+ * value (`controlled`): echoes of the edits sent to it then don't reset the
15
+ * control, but any other new value does, and so does an edit the program did
16
+ * not take: once it has answered every call (`useHandlerSettled`) and `value`
17
+ * is still not the edit, the control goes back to `value`. Without `echoes`,
18
+ * the control keeps its own state until `value` changes.
19
+ */
20
+ function useLocalValue(value, echoes) {
21
+ const settled = useHandlerSettled();
22
+ const [local, setLocal] = useState(value);
23
+ const [seen, setSeen] = useState(value);
24
+ const [seenSettled, setSeenSettled] = useState(settled);
25
+ const [pending, setPending] = useState([]);
26
+ if (!Object.is(value, seen)) {
27
+ setSeen(value);
28
+ const echo = pending.findIndex((sent) => Object.is(sent, value));
29
+ if (echo === -1) {
30
+ setLocal(value);
31
+ setPending([]);
32
+ } else setPending(pending.slice(echo + 1));
33
+ } else if (settled !== seenSettled) {
34
+ setSeenSettled(settled);
35
+ if (pending.length > 0) {
36
+ setLocal(value);
37
+ setPending([]);
38
+ }
39
+ }
40
+ const edit = (next) => {
41
+ setLocal(next);
42
+ if (echoes) setPending([...pending.slice(-20), next]);
43
+ };
44
+ return [local, edit];
45
+ }
46
+ //#endregion
47
+ export { controlled, useLocalValue };
@@ -0,0 +1,21 @@
1
+ import { ComponentType, ReactNode } from "react";
2
+ //#region src/view/registry.d.ts
3
+ /**
4
+ * One React component per catalog name. Props reach a component only after
5
+ * they passed the schema the program carries, so each component declares the
6
+ * props it reads; `never` lets any such component into the map.
7
+ */
8
+ export type Registry = Readonly<Record<string, ComponentType<never>>>;
9
+ /**
10
+ * A registry checked against a catalog's props: every name present, each
11
+ * component accepting what the catalog may pass. Use it with
12
+ * `CatalogProps` from `@jigs.design/host`:
13
+ *
14
+ * ```ts
15
+ * export const components = { … } satisfies RegistryFor<CatalogProps<typeof catalog>>;
16
+ * ```
17
+ */
18
+ export type RegistryFor<TProps extends Readonly<Record<string, object>>> = { readonly [K in keyof TProps]: (props: TProps[K] & {
19
+ children?: ReactNode;
20
+ }) => ReactNode; };
21
+ //#endregion
@@ -0,0 +1,21 @@
1
+ import { PickleClient } from "@jigs.design/client";
2
+ import { Diagnostic, Message, PickleErrorData, Program, SessionOptions } from "@jigs.design/runtime";
3
+ //#region src/view/session.d.ts
4
+ export interface SessionState {
5
+ diagnostics: ReadonlyArray<Diagnostic>;
6
+ /**
7
+ * Which UI `program` is a version of. It goes up when the model writes a
8
+ * new UI rather than editing the last one, or a stored one is restored,
9
+ * and goes back with the program when a turn is stopped. Pass it to
10
+ * `PickleView` as `documentId`, so a new UI starts from fresh state.
11
+ */
12
+ documentId: number;
13
+ /** `{ code, message }` when `status` is "error". */
14
+ error: PickleErrorData | null;
15
+ /** The conversation, to store or render. Assistant entries hold checked source. */
16
+ messages: ReadonlyArray<Message>;
17
+ /** The latest program, or null before the first one arrives. */
18
+ program: Program | null;
19
+ status: "error" | "idle" | "streaming";
20
+ }
21
+ //#endregion
@@ -0,0 +1,141 @@
1
+ //#region src/view/session.ts
2
+ const endedEarly = {
3
+ code: "NETWORK",
4
+ message: "The turn ended before it finished."
5
+ };
6
+ /**
7
+ * The conversation and the latest program for one chat. A turn that ends
8
+ * without `done` leaves no trace: the messages and the program go back to
9
+ * where they were before it started.
10
+ */
11
+ function createSessionStore(initialClient, initialMessages) {
12
+ let client = initialClient;
13
+ const listeners = /* @__PURE__ */ new Set();
14
+ let committed = {
15
+ diagnostics: [],
16
+ documentId: 0,
17
+ messages: initialMessages,
18
+ program: null
19
+ };
20
+ let state = {
21
+ ...committed,
22
+ error: null,
23
+ status: "idle"
24
+ };
25
+ let active = null;
26
+ const set = (next) => {
27
+ state = next;
28
+ for (const listener of listeners) listener();
29
+ };
30
+ const rollback = (error) => {
31
+ set({
32
+ ...committed,
33
+ error,
34
+ status: error === null ? "idle" : "error"
35
+ });
36
+ };
37
+ const run = async (messages, options, turn) => {
38
+ const body = options === void 0 ? { messages } : {
39
+ messages,
40
+ options
41
+ };
42
+ try {
43
+ for await (const event of client.session(body, { signal: turn.signal })) {
44
+ if (turn !== active) return;
45
+ if (event.type === "program") {
46
+ set({
47
+ ...state,
48
+ diagnostics: event.diagnostics,
49
+ documentId: event.fresh ? state.documentId + 1 : state.documentId,
50
+ program: event.program
51
+ });
52
+ continue;
53
+ }
54
+ if (event.type === "error") {
55
+ active = null;
56
+ rollback(event.error);
57
+ return;
58
+ }
59
+ const last = messages.at(-1);
60
+ committed = {
61
+ diagnostics: event.diagnostics,
62
+ documentId: state.documentId,
63
+ messages: last?.role === "assistant" ? messages : [...messages, {
64
+ content: event.source,
65
+ role: "assistant"
66
+ }],
67
+ program: state.program
68
+ };
69
+ active = null;
70
+ set({
71
+ ...committed,
72
+ error: null,
73
+ status: "idle"
74
+ });
75
+ return;
76
+ }
77
+ } catch {}
78
+ if (turn === active) {
79
+ active = null;
80
+ rollback(endedEarly);
81
+ }
82
+ };
83
+ const begin = (messages, options) => {
84
+ const turn = new AbortController();
85
+ active = turn;
86
+ set({
87
+ ...state,
88
+ error: null,
89
+ messages,
90
+ status: "streaming"
91
+ });
92
+ run(messages, options, turn);
93
+ };
94
+ const stop = () => {
95
+ if (active === null) return;
96
+ active.abort();
97
+ active = null;
98
+ rollback(null);
99
+ };
100
+ const resume = () => {
101
+ if (active === null && committed.messages.at(-1)?.role === "assistant") begin(committed.messages, void 0);
102
+ };
103
+ return {
104
+ getState: () => state,
105
+ reset: (messages = []) => {
106
+ stop();
107
+ committed = {
108
+ diagnostics: [],
109
+ documentId: committed.documentId,
110
+ messages,
111
+ program: null
112
+ };
113
+ set({
114
+ ...committed,
115
+ error: null,
116
+ status: "idle"
117
+ });
118
+ resume();
119
+ },
120
+ resume,
121
+ send: (prompt, options) => {
122
+ stop();
123
+ begin([...committed.messages, {
124
+ content: prompt,
125
+ role: "user"
126
+ }], options);
127
+ },
128
+ setClient: (next) => {
129
+ client = next;
130
+ },
131
+ stop,
132
+ subscribe: (listener) => {
133
+ listeners.add(listener);
134
+ return () => {
135
+ listeners.delete(listener);
136
+ };
137
+ }
138
+ };
139
+ }
140
+ //#endregion
141
+ export { createSessionStore };
@@ -0,0 +1,11 @@
1
+ //#region src/view/settled.d.ts
2
+ /**
3
+ * Counts the times the program finished answering every handler call of the
4
+ * component being drawn: it changes once the program has seen the
5
+ * component's latest events and its answer is on screen. A control that
6
+ * runs ahead of the program, such as a text field showing what was typed,
7
+ * can then go back to its value prop if the program kept another value.
8
+ * Always 0 outside `PickleView`.
9
+ */
10
+ export declare function useHandlerSettled(): number;
11
+ //#endregion
@@ -0,0 +1,17 @@
1
+ import { createContext, useContext } from "react";
2
+ //#region src/view/settled.ts
3
+ /** How many times the node being drawn had all its handler calls answered. */
4
+ const SettledContext = createContext(0);
5
+ /**
6
+ * Counts the times the program finished answering every handler call of the
7
+ * component being drawn: it changes once the program has seen the
8
+ * component's latest events and its answer is on screen. A control that
9
+ * runs ahead of the program, such as a text field showing what was typed,
10
+ * can then go back to its value prop if the program kept another value.
11
+ * Always 0 outside `PickleView`.
12
+ */
13
+ function useHandlerSettled() {
14
+ return useContext(SettledContext);
15
+ }
16
+ //#endregion
17
+ export { SettledContext, useHandlerSettled };
@@ -0,0 +1,31 @@
1
+ import { SessionState } from "./session.js";
2
+ import { PickleClient } from "@jigs.design/client";
3
+ import { Message, SessionOptions } from "@jigs.design/runtime";
4
+ //#region src/view/usePickleSession.d.ts
5
+ export interface PickleSession<TOptions extends SessionOptions = SessionOptions> extends SessionState {
6
+ /**
7
+ * Stops any turn and starts over with `messages` (default none). Like
8
+ * `initialMessages`, messages that end with an assistant message are
9
+ * compiled again without calling the model.
10
+ */
11
+ reset: (messages?: ReadonlyArray<Message>) => void;
12
+ /**
13
+ * Starts a turn. A turn already streaming stops first. `options` go to the
14
+ * server's `options` schema; type them with the schema's input type.
15
+ */
16
+ send: (prompt: string, options?: TOptions) => void;
17
+ /** Aborts the turn in flight; the conversation goes back to before it. */
18
+ stop: () => void;
19
+ }
20
+ /**
21
+ * Holds a conversation with the server and its latest program. Pass
22
+ * `initialMessages` to resume a stored conversation: when they end with an
23
+ * assistant message, the server compiles it again without calling the model.
24
+ * `initialMessages` is read once, when the hook mounts; to load another
25
+ * conversation later, call `reset(messages)`. A new `client` stops the turn
26
+ * in flight and compiles the conversation's last answer again through it.
27
+ */
28
+ export declare function usePickleSession<TOptions extends SessionOptions = SessionOptions>(client: PickleClient, options?: {
29
+ initialMessages?: ReadonlyArray<Message>;
30
+ }): PickleSession<TOptions>;
31
+ //#endregion
@@ -0,0 +1,29 @@
1
+ import { createSessionStore } from "./session.js";
2
+ import { useEffect, useState, useSyncExternalStore } from "react";
3
+ //#region src/view/usePickleSession.ts
4
+ const NO_MESSAGES = [];
5
+ /**
6
+ * Holds a conversation with the server and its latest program. Pass
7
+ * `initialMessages` to resume a stored conversation: when they end with an
8
+ * assistant message, the server compiles it again without calling the model.
9
+ * `initialMessages` is read once, when the hook mounts; to load another
10
+ * conversation later, call `reset(messages)`. A new `client` stops the turn
11
+ * in flight and compiles the conversation's last answer again through it.
12
+ */
13
+ function usePickleSession(client, options) {
14
+ const [store] = useState(() => createSessionStore(client, options?.initialMessages ?? NO_MESSAGES));
15
+ const state = useSyncExternalStore(store.subscribe, store.getState, store.getState);
16
+ useEffect(() => {
17
+ store.setClient(client);
18
+ store.resume();
19
+ return () => store.stop();
20
+ }, [store, client]);
21
+ return {
22
+ ...state,
23
+ reset: store.reset,
24
+ send: store.send,
25
+ stop: store.stop
26
+ };
27
+ }
28
+ //#endregion
29
+ export { usePickleSession };
package/package.json CHANGED
@@ -1,6 +1,34 @@
1
1
  {
2
2
  "name": "@jigs.design/react",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "The web renderer: PickleView runs a compiled program in a sandbox and draws it with your registry; usePickleSession streams programs from the server.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/yoamomonstruos/pickle.git",
9
+ "directory": "packages/react"
10
+ },
11
+ "type": "module",
12
+ "sideEffects": false,
13
+ "files": [
14
+ "dist",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "exports": {
19
+ ".": {
20
+ "types": "./dist/index.d.ts",
21
+ "import": "./dist/index.js"
22
+ }
23
+ },
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "dependencies": {
28
+ "@jigs.design/runtime": "0.1.1",
29
+ "@jigs.design/client": "0.1.1"
30
+ },
31
+ "peerDependencies": {
32
+ "react": "^18.0.0 || ^19.0.0"
33
+ }
6
34
  }