@jini-ai/vibecoding 0.4.0

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +154 -0
  3. package/dist/core/apply.d.ts +69 -0
  4. package/dist/core/apply.js +69 -0
  5. package/dist/core/history.d.ts +144 -0
  6. package/dist/core/history.js +132 -0
  7. package/dist/core/index.d.ts +19 -0
  8. package/dist/core/index.js +3 -0
  9. package/dist/core/target.d.ts +92 -0
  10. package/dist/core/target.js +2 -0
  11. package/dist/core/types.d.ts +86 -0
  12. package/dist/core/types.js +19 -0
  13. package/dist/html/index.d.ts +12 -0
  14. package/dist/html/index.js +2 -0
  15. package/dist/html/node/index.d.ts +10 -0
  16. package/dist/html/node/index.js +10 -0
  17. package/dist/html/node/parse5-region-parser.d.ts +12 -0
  18. package/dist/html/node/parse5-region-parser.js +213 -0
  19. package/dist/html/regions.d.ts +137 -0
  20. package/dist/html/regions.js +165 -0
  21. package/dist/index.d.ts +10 -0
  22. package/dist/index.js +10 -0
  23. package/dist/react/components/DocumentPreview.d.ts +59 -0
  24. package/dist/react/components/DocumentPreview.js +44 -0
  25. package/dist/react/components/PartsViewer.d.ts +49 -0
  26. package/dist/react/components/PartsViewer.js +61 -0
  27. package/dist/react/components/VibecodingWorkbench.d.ts +38 -0
  28. package/dist/react/components/VibecodingWorkbench.js +96 -0
  29. package/dist/react/index.d.ts +29 -0
  30. package/dist/react/index.js +7 -0
  31. package/dist/react/session.d.ts +108 -0
  32. package/dist/react/session.js +160 -0
  33. package/dist/react/tools.d.ts +91 -0
  34. package/dist/react/tools.js +246 -0
  35. package/dist/react/use-vibecoding-session.d.ts +30 -0
  36. package/dist/react/use-vibecoding-session.js +44 -0
  37. package/package.json +99 -0
@@ -0,0 +1,165 @@
1
+ /** The attribute that publishes a region's handle. Mirrors `@jini-ai/agentic`'s convention. */
2
+ export const AGENT_ELEMENT_ATTRIBUTE = "data-agent-element";
3
+ /**
4
+ * Lowercase words joined by single hyphens — the same grammar `@jini-ai/agentic` enforces, restated
5
+ * here rather than imported so `/html` does not acquire a dependency on the agentic package for one
6
+ * regular expression. If that package's grammar ever widens, this must be revisited deliberately.
7
+ */
8
+ const HANDLE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
9
+ const MAX_HANDLE_LENGTH = 128;
10
+ /** Whether `handle` is a syntactically valid region handle. */
11
+ export function isValidRegionHandle({ handle }) {
12
+ return handle.length > 0 && handle.length <= MAX_HANDLE_LENGTH && HANDLE_PATTERN.test(handle);
13
+ }
14
+ /** Handles in document order, including duplicates — order and multiplicity both matter. */
15
+ function handlesOf(regions) {
16
+ return regions.map((region) => region.handle);
17
+ }
18
+ /** Counts the whole document first so even the first occurrence of an ambiguous handle is omitted. */
19
+ function handleCounts(regions) {
20
+ const counts = new Map();
21
+ for (const { handle } of regions)
22
+ counts.set(handle, (counts.get(handle) ?? 0) + 1);
23
+ return counts;
24
+ }
25
+ /** A stable, order-insensitive description of a handle multiset, for comparison in a message. */
26
+ function describeHandles(handles) {
27
+ return [...handles].sort().join(", ") || "(none)";
28
+ }
29
+ /** Locates one region by handle, refusing ambiguity rather than silently picking the first. */
30
+ function findOne(regions, id) {
31
+ const matches = regions.filter((region) => region.handle === id);
32
+ if (matches.length === 0)
33
+ return { ok: false, reason: `no region is tagged ${AGENT_ELEMENT_ATTRIBUTE}="${id}"` };
34
+ if (matches.length > 1) {
35
+ return {
36
+ ok: false,
37
+ reason: `${matches.length} regions are tagged ${AGENT_ELEMENT_ATTRIBUTE}="${id}" — a handle must identify exactly one region`,
38
+ };
39
+ }
40
+ // `matches[0]` is present: length is exactly 1 on this branch.
41
+ return { ok: true, region: matches[0] };
42
+ }
43
+ /**
44
+ * Build an `EditTarget` over the tagged regions of one HTML document.
45
+ *
46
+ * @param deps - document storage and an HTML parser, both host-supplied.
47
+ * @param options - optional region size limit, separate from the required ports.
48
+ * @returns a target ready to hand to `applyEdit`/`applyEdits` or to `createEditHistory`.
49
+ * @complexity 4
50
+ */
51
+ export function createHtmlRegionTarget(deps, options = {}) {
52
+ const { store, parser } = deps;
53
+ const maxPartLength = options.maxPartLength ?? 256 * 1024;
54
+ /** Splices `content` into `region`'s inner range, preserving every byte outside it. */
55
+ function spliced(html, region, content) {
56
+ return html.slice(0, region.innerStart) + content + html.slice(region.innerEnd);
57
+ }
58
+ return {
59
+ async listParts() {
60
+ const regions = parser.findRegions({ html: await store.read() });
61
+ const counts = handleCounts(regions);
62
+ const refs = [];
63
+ for (const region of regions) {
64
+ // A malformed or duplicated handle is not publishable: publishing it would advertise an id
65
+ // that `readPart`/`replacePart` must then refuse, or worse, resolve ambiguously.
66
+ if (!isValidRegionHandle({ handle: region.handle }) || counts.get(region.handle) !== 1)
67
+ continue;
68
+ refs.push({
69
+ id: region.handle,
70
+ ...(region.role === undefined ? {} : { kind: region.role }),
71
+ ...(region.label === undefined ? {} : { label: region.label }),
72
+ });
73
+ }
74
+ return refs;
75
+ },
76
+ async readPart({ id }) {
77
+ const html = await store.read();
78
+ const found = findOne(parser.findRegions({ html }), id);
79
+ if (!found.ok)
80
+ throw new Error(found.reason);
81
+ return html.slice(found.region.innerStart, found.region.innerEnd);
82
+ },
83
+ /**
84
+ * Note that this is NOT an upsert here, unlike the port's general contract: a region that does
85
+ * not exist cannot be created by writing to it, because creating one would mean inventing a
86
+ * position in someone else's document and a tag to wrap it in. A host that wants a new region
87
+ * adds it to the document itself, and it becomes addressable at the next `listParts`.
88
+ */
89
+ async replacePart({ id, content }) {
90
+ const html = await store.read();
91
+ const found = findOne(parser.findRegions({ html }), id);
92
+ if (!found.ok)
93
+ throw new Error(found.reason);
94
+ await store.write({ html: spliced(html, found.region, content) });
95
+ },
96
+ async snapshot() {
97
+ const html = await store.read();
98
+ const regions = parser.findRegions({ html });
99
+ const parts = {};
100
+ const counts = handleCounts(regions);
101
+ for (const region of regions) {
102
+ if (!isValidRegionHandle({ handle: region.handle }) || counts.get(region.handle) !== 1)
103
+ continue;
104
+ parts[region.handle] = html.slice(region.innerStart, region.innerEnd);
105
+ }
106
+ return { id: `html-${regions.length}-${html.length}`, parts };
107
+ },
108
+ /**
109
+ * Restores each region's content in one pass.
110
+ *
111
+ * Offsets are re-resolved between writes rather than computed once, because every splice shifts
112
+ * the offsets of every region after it. Computing them all up front is the obvious
113
+ * implementation and it silently corrupts the document whenever two regions change length.
114
+ */
115
+ async restore({ snapshot }) {
116
+ for (const [handle, content] of Object.entries(snapshot.parts)) {
117
+ const html = await store.read();
118
+ const found = findOne(parser.findRegions({ html }), handle);
119
+ if (!found.ok)
120
+ continue; // The region is gone; there is nowhere to put its content back.
121
+ await store.write({ html: spliced(html, found.region, content) });
122
+ }
123
+ },
124
+ async validate(candidate) {
125
+ if (candidate.content.length > maxPartLength) {
126
+ return {
127
+ ok: false,
128
+ reason: `that content is ${candidate.content.length} characters, over the ${maxPartLength}-character limit for one region — send a shorter version`,
129
+ };
130
+ }
131
+ const html = await store.read();
132
+ const before = parser.findRegions({ html });
133
+ const found = findOne(before, candidate.id);
134
+ if (!found.ok)
135
+ return { ok: false, reason: found.reason };
136
+ // Judge the WHOLE prospective document, never the fragment alone: a fragment that is
137
+ // well-formed by itself can still break the document it lands in.
138
+ const prospective = spliced(html, found.region, candidate.content);
139
+ const structural = parser.checkWellFormed?.({ html: prospective });
140
+ if (structural && !structural.ok)
141
+ return { ok: false, reason: structural.reason };
142
+ const afterHandles = handlesOf(parser.findRegions({ html: prospective }));
143
+ const beforeHandles = handlesOf(before);
144
+ // The allowlist check. Compared as multisets so an invented, deleted OR duplicated handle is
145
+ // all caught by one comparison — see this module's doc for why this is the security property.
146
+ const beforeKey = describeHandles(beforeHandles);
147
+ const afterKey = describeHandles(afterHandles);
148
+ if (beforeKey !== afterKey) {
149
+ const invented = afterHandles.filter((handle) => !beforeHandles.includes(handle));
150
+ const removed = beforeHandles.filter((handle) => !afterHandles.includes(handle));
151
+ const detail = invented.length > 0
152
+ ? `it adds ${AGENT_ELEMENT_ATTRIBUTE} handles that did not exist (${describeHandles(invented)})`
153
+ : removed.length > 0
154
+ ? `it removes existing ${AGENT_ELEMENT_ATTRIBUTE} handles (${describeHandles(removed)})`
155
+ : `it changes how many times a ${AGENT_ELEMENT_ATTRIBUTE} handle appears`;
156
+ return {
157
+ ok: false,
158
+ reason: `${detail}. Edit only the content inside the region you were asked to change, and do not add, remove or duplicate ${AGENT_ELEMENT_ATTRIBUTE} attributes.`,
159
+ };
160
+ }
161
+ return { ok: true };
162
+ },
163
+ };
164
+ }
165
+ //# sourceMappingURL=regions.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @module @jini-ai/vibecoding
3
+ *
4
+ * Conversational artifact authoring, target-agnostic.
5
+ *
6
+ * The root entry re-exports `./core` for convenience. Importing `@jini-ai/vibecoding/core`
7
+ * directly is equivalent and states the universal-runtime intent more plainly.
8
+ */
9
+ export * from "./core/index.js";
10
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @module @jini-ai/vibecoding
3
+ *
4
+ * Conversational artifact authoring, target-agnostic.
5
+ *
6
+ * The root entry re-exports `./core` for convenience. Importing `@jini-ai/vibecoding/core`
7
+ * directly is equivalent and states the universal-runtime intent more plainly.
8
+ */
9
+ export * from "./core/index.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,59 @@
1
+ /**
2
+ * @module react/components/DocumentPreview
3
+ *
4
+ * The "renderer" half of the user's vision: a live preview of whatever HTML the host currently
5
+ * wants shown. Deliberately the simplest thing that renders live HTML, not an editing surface —
6
+ * see this file's "Scope: preview, not sandbox, not an editor" section below for why.
7
+ *
8
+ * `html` is host-supplied rather than derived from a `VibecodingSession` here, because a generic
9
+ * `EditTarget` (`../../core/target.ts`) has no "whole document" verb at all — only per-part
10
+ * `listParts`/`readPart`. A single-document host (`../../html/regions.ts`'s tagged-region target)
11
+ * genuinely owns the composed document as its own storage detail; a filesystem-backed host would
12
+ * have no single document to compose in the first place. Keeping this component's contract to
13
+ * "render whatever HTML string you hand me" is what keeps it usable by every future `EditTarget`
14
+ * host alike, the same way `../../core` itself stays target-agnostic.
15
+ */
16
+ import type { CSSProperties } from 'react';
17
+ export interface DocumentPreviewProps {
18
+ /** The document to render. Treated as untrusted, model-authored markup — see this file's
19
+ * "Sandboxing" note below. */
20
+ readonly html: string;
21
+ readonly title?: string;
22
+ readonly className?: string;
23
+ readonly style?: CSSProperties;
24
+ }
25
+ /**
26
+ * Renders `html` live inside a sandboxed `<iframe>`.
27
+ *
28
+ * ## Scope: preview, not sandbox, not an editor
29
+ *
30
+ * `@jini-ai/sandbox`'s own README draws the line this component is built to respect: vibecoding
31
+ * owns the conversational edit loop over an addressable artifact, sandbox owns making that
32
+ * artifact actually run and produce a preview. This component renders a static HTML string the
33
+ * host already has in hand — it boots no process, runs no build, and has no notion of a dev
34
+ * server. A host whose artifact needs a real build/run step (anything beyond one static HTML
35
+ * document) gets its preview from `@jini-ai/sandbox`'s `SandboxSession.getPreview()` instead and
36
+ * would point an ordinary `<iframe src={previewUrl}>` at that — this component is not the right
37
+ * tool there, and does not try to be. It is also deliberately not `@jini-ai/ui`'s
38
+ * `InteractiveHtmlEditor`: that component is a GrapesJS-backed click-to-edit canvas, a second,
39
+ * heavier concern (and a large third-party dependency) this "see it live" renderer does not need
40
+ * to pull in just to display the current document. A host that wants inline manual editing on top
41
+ * of the same document can layer `InteractiveHtmlEditor` in separately; nothing here precludes it.
42
+ *
43
+ * ## Sandboxing
44
+ *
45
+ * `html` is model-authored per `../../core/target.ts`'s own contract ("Implementations should
46
+ * assume content is model-authored and therefore arbitrary"), so this iframe is sandboxed with
47
+ * `allow-scripts` ONLY — no `allow-same-origin`. Combining the two on a `srcDoc` frame is a known
48
+ * sandbox-escape pattern (the frame could otherwise reach back into a same-origin-equivalent
49
+ * context and strip its own restrictions); omitting `allow-same-origin` keeps the frame's content
50
+ * in a permanently unique, opaque origin regardless of what markup or script it contains, while
51
+ * `allow-scripts` still lets an interactive preview (a toggle, an animation) actually run.
52
+ * `React`'s `srcDoc` prop re-navigates the iframe on every change, so no manual reload/key-remount
53
+ * logic is needed for this to update live as the host's `html` changes.
54
+ *
55
+ * @complexity O(1) — a single element; rendering cost is the browser's, not this component's.
56
+ * @overallScore 100/100
57
+ */
58
+ export declare function DocumentPreview({ html, title, className, style }: DocumentPreviewProps): import("react").JSX.Element;
59
+ //# sourceMappingURL=DocumentPreview.d.ts.map
@@ -0,0 +1,44 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ const FRAME_STYLE = {
3
+ width: '100%',
4
+ height: '100%',
5
+ border: 'none',
6
+ background: '#fff',
7
+ };
8
+ /**
9
+ * Renders `html` live inside a sandboxed `<iframe>`.
10
+ *
11
+ * ## Scope: preview, not sandbox, not an editor
12
+ *
13
+ * `@jini-ai/sandbox`'s own README draws the line this component is built to respect: vibecoding
14
+ * owns the conversational edit loop over an addressable artifact, sandbox owns making that
15
+ * artifact actually run and produce a preview. This component renders a static HTML string the
16
+ * host already has in hand — it boots no process, runs no build, and has no notion of a dev
17
+ * server. A host whose artifact needs a real build/run step (anything beyond one static HTML
18
+ * document) gets its preview from `@jini-ai/sandbox`'s `SandboxSession.getPreview()` instead and
19
+ * would point an ordinary `<iframe src={previewUrl}>` at that — this component is not the right
20
+ * tool there, and does not try to be. It is also deliberately not `@jini-ai/ui`'s
21
+ * `InteractiveHtmlEditor`: that component is a GrapesJS-backed click-to-edit canvas, a second,
22
+ * heavier concern (and a large third-party dependency) this "see it live" renderer does not need
23
+ * to pull in just to display the current document. A host that wants inline manual editing on top
24
+ * of the same document can layer `InteractiveHtmlEditor` in separately; nothing here precludes it.
25
+ *
26
+ * ## Sandboxing
27
+ *
28
+ * `html` is model-authored per `../../core/target.ts`'s own contract ("Implementations should
29
+ * assume content is model-authored and therefore arbitrary"), so this iframe is sandboxed with
30
+ * `allow-scripts` ONLY — no `allow-same-origin`. Combining the two on a `srcDoc` frame is a known
31
+ * sandbox-escape pattern (the frame could otherwise reach back into a same-origin-equivalent
32
+ * context and strip its own restrictions); omitting `allow-same-origin` keeps the frame's content
33
+ * in a permanently unique, opaque origin regardless of what markup or script it contains, while
34
+ * `allow-scripts` still lets an interactive preview (a toggle, an animation) actually run.
35
+ * `React`'s `srcDoc` prop re-navigates the iframe on every change, so no manual reload/key-remount
36
+ * logic is needed for this to update live as the host's `html` changes.
37
+ *
38
+ * @complexity O(1) — a single element; rendering cost is the browser's, not this component's.
39
+ * @overallScore 100/100
40
+ */
41
+ export function DocumentPreview({ html, title, className, style }) {
42
+ return (_jsx("iframe", { title: title ?? 'Live preview', srcDoc: html, sandbox: "allow-scripts", className: className, style: style ? { ...FRAME_STYLE, ...style } : FRAME_STYLE }));
43
+ }
44
+ //# sourceMappingURL=DocumentPreview.js.map
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @module react/components/PartsViewer
3
+ *
4
+ * The "file viewer" half of the user's vision: browse the parts an `EditTarget` currently
5
+ * advertises (`../core/target.ts`'s `listParts()` allowlist) and inspect one part's raw content.
6
+ *
7
+ * ## Why this is not `@jini-ai/ui`'s `AssetTreeBrowser`
8
+ *
9
+ * `AssetTreeBrowser` (`@jini-ai/ui`'s `features/asset-tree-browser`) was checked before writing
10
+ * this component, per this task's own instruction to reuse existing primitives. It was not reused,
11
+ * for a reason worth recording rather than silently deviating from: its `AssetTreeSelectors`
12
+ * REQUIRES `getSize`/`getModifiedAt` on every item, and its data model is a folder-structured
13
+ * filesystem (breadcrumbs, directories, upload, rename, drag/drop). `PartRef` (`../core/types.ts`)
14
+ * has none of that — a part is a flat, host-published id with only an optional `kind`/`label`, and
15
+ * the one `EditTarget` implementation that exists today (`../html/regions.ts`'s tagged-region
16
+ * target) has no filesystem at all. Fabricating a size or a modified time for a part would be
17
+ * inventing data the host never provided, which is worse than a simpler, honestly-scoped list.
18
+ * Once a filesystem-backed `EditTarget` lands (this package's planned `./node`), a file-tree host
19
+ * has the real fields `AssetTreeBrowser` wants, and reusing it there is the right call.
20
+ *
21
+ * ## Deliberately presentational
22
+ *
23
+ * `PartsViewer` takes already-resolved data and callbacks only, no `VibecodingSession` reference —
24
+ * the same split `@jini-ai/ui`'s own feature folders use between headless hooks/state and dumb
25
+ * components. `./VibecodingWorkbench.js` is the composed piece that owns a session and feeds this
26
+ * component; a host that wants a different layout or a different session-driving hook can still
27
+ * reuse this component directly by supplying the same props.
28
+ */
29
+ import type { CSSProperties } from 'react';
30
+ import type { PartId, PartRef } from '../../core/types.js';
31
+ export interface PartsViewerProps {
32
+ readonly parts: readonly PartRef[];
33
+ readonly selectedId?: PartId | null;
34
+ readonly onSelect?: (id: PartId) => void;
35
+ /** The selected part's content, or `undefined` while unread/unselected — see
36
+ * `./VibecodingWorkbench.js` for how this is sourced from `session.readPart`. */
37
+ readonly content?: string;
38
+ readonly contentLoading?: boolean;
39
+ readonly className?: string;
40
+ readonly style?: CSSProperties;
41
+ }
42
+ /**
43
+ * A flat, selectable list of `parts` on the left and the selected one's raw content on the right.
44
+ *
45
+ * @complexity O(parts.length) to render the list; content rendering is O(content.length).
46
+ * @overallScore 100/100
47
+ */
48
+ export declare function PartsViewer({ parts, selectedId, onSelect, content, contentLoading, className, style }: PartsViewerProps): import("react").JSX.Element;
49
+ //# sourceMappingURL=PartsViewer.d.ts.map
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ const ROOT_STYLE = {
3
+ display: 'flex',
4
+ minHeight: 0,
5
+ height: '100%',
6
+ fontSize: 13,
7
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
8
+ color: '#1f2430',
9
+ };
10
+ const LIST_STYLE = {
11
+ width: 220,
12
+ flexShrink: 0,
13
+ overflowY: 'auto',
14
+ borderRight: '1px solid #e2e5ea',
15
+ padding: 4,
16
+ margin: 0,
17
+ listStyle: 'none',
18
+ };
19
+ const ROW_BASE_STYLE = {
20
+ display: 'flex',
21
+ flexDirection: 'column',
22
+ gap: 2,
23
+ width: '100%',
24
+ textAlign: 'left',
25
+ border: 'none',
26
+ background: 'transparent',
27
+ borderRadius: 6,
28
+ padding: '6px 8px',
29
+ cursor: 'pointer',
30
+ color: 'inherit',
31
+ font: 'inherit',
32
+ };
33
+ const ROW_SELECTED_STYLE = {
34
+ ...ROW_BASE_STYLE,
35
+ background: '#e8edfb',
36
+ };
37
+ const ROW_LABEL_STYLE = { fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' };
38
+ const ROW_META_STYLE = { color: '#6b7280', fontSize: 11 };
39
+ const CONTENT_STYLE = {
40
+ flex: 1,
41
+ minWidth: 0,
42
+ overflow: 'auto',
43
+ margin: 0,
44
+ padding: 12,
45
+ whiteSpace: 'pre-wrap',
46
+ wordBreak: 'break-word',
47
+ };
48
+ const EMPTY_STYLE = { padding: 12, color: '#6b7280' };
49
+ /**
50
+ * A flat, selectable list of `parts` on the left and the selected one's raw content on the right.
51
+ *
52
+ * @complexity O(parts.length) to render the list; content rendering is O(content.length).
53
+ * @overallScore 100/100
54
+ */
55
+ export function PartsViewer({ parts, selectedId, onSelect, content, contentLoading, className, style }) {
56
+ return (_jsxs("div", { className: className, style: style ? { ...ROOT_STYLE, ...style } : ROOT_STYLE, children: [_jsx("ul", { style: LIST_STYLE, "aria-label": "Artifact parts", children: parts.length === 0 ? (_jsx("li", { style: EMPTY_STYLE, children: "No parts yet." })) : (parts.map((part) => {
57
+ const isSelected = part.id === selectedId;
58
+ return (_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onSelect?.(part.id), style: isSelected ? ROW_SELECTED_STYLE : ROW_BASE_STYLE, "aria-current": isSelected, children: [_jsx("span", { style: ROW_LABEL_STYLE, children: part.label ?? part.id }), part.kind ? _jsx("span", { style: ROW_META_STYLE, children: part.kind }) : null] }) }, part.id));
59
+ })) }), selectedId === undefined || selectedId === null ? (_jsx("p", { style: { ...CONTENT_STYLE, ...EMPTY_STYLE }, children: "Select a part to inspect its content." })) : contentLoading ? (_jsx("p", { style: { ...CONTENT_STYLE, ...EMPTY_STYLE }, children: "Loading\u2026" })) : (_jsx("pre", { style: CONTENT_STYLE, children: content ?? '' }))] }));
60
+ }
61
+ //# sourceMappingURL=PartsViewer.js.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @module react/components/VibecodingWorkbench
3
+ *
4
+ * The composed convenience component: owns a `VibecodingSession` (via `useVibecodingSession`) and
5
+ * wires it into `PartsViewer` (the file viewer) and `DocumentPreview` (the renderer) plus an
6
+ * undo/redo toolbar, the same "headless hook + presentational pieces + one drop-in pane" shape
7
+ * `@jini-ai/chat/react`'s `<ChatPane>` uses over its own headless primitives. A host that wants a
8
+ * different layout composes `PartsViewer`/`DocumentPreview`/`useVibecodingSession` directly instead
9
+ * — this component does not own anything those three don't already expose.
10
+ */
11
+ import { type CSSProperties } from 'react';
12
+ import type { VibecodingSession } from '../session.js';
13
+ export interface VibecodingWorkbenchProps {
14
+ readonly session: VibecodingSession;
15
+ /**
16
+ * Host-supplied whole-document HTML for the preview pane. Omitted, the preview instead shows the
17
+ * SELECTED part's own content — see `./DocumentPreview.js`'s doc for why a generic `EditTarget`
18
+ * has no "whole document" verb this component could read on its own. A host built on the
19
+ * `html` region target (`../../html/regions.ts`) already holds the composed document as its own
20
+ * `HtmlDocumentStore` state and is the natural place to supply this.
21
+ */
22
+ readonly documentHtml?: string;
23
+ readonly className?: string;
24
+ readonly style?: CSSProperties;
25
+ }
26
+ /**
27
+ * A ready-to-drop-in pane combining the file viewer, the live renderer, and undo/redo controls
28
+ * over one `session`.
29
+ *
30
+ * Selection is local UI state, not part of `VibecodingSession` — a session describes the artifact,
31
+ * not which part a particular viewer happens to have open, and a host rendering two workbenches
32
+ * over the same session (unlikely today, but not precluded) should not have them fight over one
33
+ * shared selection.
34
+ *
35
+ * @complexity O(parts.length) to render the viewer; content fetches are cached — see `../session.js`.
36
+ */
37
+ export declare function VibecodingWorkbench({ session, documentHtml, className, style }: VibecodingWorkbenchProps): import("react").JSX.Element;
38
+ //# sourceMappingURL=VibecodingWorkbench.d.ts.map
@@ -0,0 +1,96 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * @module react/components/VibecodingWorkbench
4
+ *
5
+ * The composed convenience component: owns a `VibecodingSession` (via `useVibecodingSession`) and
6
+ * wires it into `PartsViewer` (the file viewer) and `DocumentPreview` (the renderer) plus an
7
+ * undo/redo toolbar, the same "headless hook + presentational pieces + one drop-in pane" shape
8
+ * `@jini-ai/chat/react`'s `<ChatPane>` uses over its own headless primitives. A host that wants a
9
+ * different layout composes `PartsViewer`/`DocumentPreview`/`useVibecodingSession` directly instead
10
+ * — this component does not own anything those three don't already expose.
11
+ */
12
+ import { useEffect, useState } from 'react';
13
+ import { useVibecodingSession } from '../use-vibecoding-session.js';
14
+ import { PartsViewer } from './PartsViewer.js';
15
+ import { DocumentPreview } from './DocumentPreview.js';
16
+ const ROOT_STYLE = {
17
+ display: 'flex',
18
+ flexDirection: 'column',
19
+ height: '100%',
20
+ minHeight: 0,
21
+ border: '1px solid #e2e5ea',
22
+ borderRadius: 8,
23
+ overflow: 'hidden',
24
+ background: '#fff',
25
+ };
26
+ const TOOLBAR_STYLE = {
27
+ display: 'flex',
28
+ alignItems: 'center',
29
+ gap: 8,
30
+ padding: '6px 10px',
31
+ borderBottom: '1px solid #e2e5ea',
32
+ fontSize: 12,
33
+ fontFamily: 'ui-sans-serif, system-ui, sans-serif',
34
+ color: '#374151',
35
+ };
36
+ const BUTTON_STYLE = {
37
+ font: 'inherit',
38
+ border: '1px solid #d0d5dd',
39
+ background: '#fff',
40
+ borderRadius: 6,
41
+ padding: '3px 10px',
42
+ cursor: 'pointer',
43
+ };
44
+ const BUTTON_DISABLED_STYLE = { ...BUTTON_STYLE, opacity: 0.5, cursor: 'default' };
45
+ const ERROR_STYLE = { color: '#b42318', marginLeft: 'auto' };
46
+ const BODY_STYLE = { display: 'flex', flex: 1, minHeight: 0 };
47
+ const PANE_STYLE = { flex: 1, minWidth: 0, minHeight: 0, borderRight: '1px solid #e2e5ea' };
48
+ const PREVIEW_PANE_STYLE = { flex: 1, minWidth: 0, minHeight: 0 };
49
+ /**
50
+ * A ready-to-drop-in pane combining the file viewer, the live renderer, and undo/redo controls
51
+ * over one `session`.
52
+ *
53
+ * Selection is local UI state, not part of `VibecodingSession` — a session describes the artifact,
54
+ * not which part a particular viewer happens to have open, and a host rendering two workbenches
55
+ * over the same session (unlikely today, but not precluded) should not have them fight over one
56
+ * shared selection.
57
+ *
58
+ * @complexity O(parts.length) to render the viewer; content fetches are cached — see `../session.js`.
59
+ */
60
+ export function VibecodingWorkbench({ session, documentHtml, className, style }) {
61
+ const { parts, partContent, canUndo, canRedo, lastError, readPart, undo, redo } = useVibecodingSession({ session });
62
+ const [selectedId, setSelectedId] = useState(null);
63
+ const [selectedContent, setSelectedContent] = useState(undefined);
64
+ const [contentLoading, setContentLoading] = useState(false);
65
+ // Re-fetches on part-list or cache changes so a chat-driven edit or history replay to
66
+ // the currently open part shows up immediately: `readPart` resolves from `session`'s cache
67
+ // (already updated by `applyEdits`/`undo`/`redo` themselves — see `../session.js`), so this is a
68
+ // cache hit, not a host round trip, whenever the edit is what triggered the re-render.
69
+ useEffect(() => {
70
+ if (selectedId === null) {
71
+ setSelectedContent(undefined);
72
+ return;
73
+ }
74
+ let cancelled = false;
75
+ setContentLoading(true);
76
+ readPart({ id: selectedId })
77
+ .then((content) => {
78
+ if (!cancelled)
79
+ setSelectedContent(content);
80
+ })
81
+ .catch(() => {
82
+ // `lastError` on the session snapshot already surfaces this — see `useVibecodingSession`'s
83
+ // own doc for why a rejection here is not rethrown into an unhandled effect error.
84
+ })
85
+ .finally(() => {
86
+ if (!cancelled)
87
+ setContentLoading(false);
88
+ });
89
+ return () => {
90
+ cancelled = true;
91
+ };
92
+ }, [selectedId, readPart, parts, partContent]);
93
+ const previewHtml = documentHtml ?? selectedContent ?? '';
94
+ return (_jsxs("div", { className: className, style: style ? { ...ROOT_STYLE, ...style } : ROOT_STYLE, children: [_jsxs("div", { style: TOOLBAR_STYLE, children: [_jsx("button", { type: "button", style: canUndo ? BUTTON_STYLE : BUTTON_DISABLED_STYLE, disabled: !canUndo, onClick: () => void undo(), children: "Undo" }), _jsx("button", { type: "button", style: canRedo ? BUTTON_STYLE : BUTTON_DISABLED_STYLE, disabled: !canRedo, onClick: () => void redo(), children: "Redo" }), lastError ? _jsx("span", { style: ERROR_STYLE, children: lastError }) : null] }), _jsxs("div", { style: BODY_STYLE, children: [_jsx("div", { style: PANE_STYLE, children: _jsx(PartsViewer, { parts: parts, selectedId: selectedId, onSelect: setSelectedId, ...(selectedContent !== undefined ? { content: selectedContent } : {}), contentLoading: contentLoading, style: { height: '100%' } }) }), _jsx("div", { style: PREVIEW_PANE_STYLE, children: _jsx(DocumentPreview, { html: previewHtml }) })] })] }));
95
+ }
96
+ //# sourceMappingURL=VibecodingWorkbench.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * @module react
3
+ *
4
+ * The chat + preview surface: a `VibecodingSession` (a subscribable wrapper over `../core`'s
5
+ * `EditTarget`/`EditHistory`), the `@jini-ai/core`-registrable tool set that drives it from chat,
6
+ * and the presentational React pieces — `PartsViewer` (file viewer), `DocumentPreview` (renderer),
7
+ * and `VibecodingWorkbench` (both, composed). See this package's README for the three-piece shape
8
+ * this entry point exists to fill in, and each exported module's own doc for its design rationale.
9
+ *
10
+ * React (and `react-dom`, transitively via these components) is an **optional peer dependency** —
11
+ * see this package's `package.json` — so importing `../core` or `../html` alone never pulls React
12
+ * in. Only a consumer of this `./react` subpath needs it installed.
13
+ */
14
+ export type { VibecodingSession, VibecodingSessionOptions, VibecodingSessionSnapshot, ApplyEditsResult } from './session.js';
15
+ export { createVibecodingSession } from './session.js';
16
+ export type { VibecodingSessionArgs } from './session.js';
17
+ export type { UseVibecodingSessionResult } from './use-vibecoding-session.js';
18
+ export { useVibecodingSession } from './use-vibecoding-session.js';
19
+ export type { UseVibecodingSessionArgs } from './use-vibecoding-session.js';
20
+ export type { CreateVibecodingToolRegistrationsOptions, VibecodingToolRunner, } from './tools.js';
21
+ export type { VibecodingToolArgs } from './tools.js';
22
+ export { VIBECODING_TOOL_IDS, createVibecodingToolRegistrations, createVibecodingToolRunner, } from './tools.js';
23
+ export type { PartsViewerProps } from './components/PartsViewer.js';
24
+ export { PartsViewer } from './components/PartsViewer.js';
25
+ export type { DocumentPreviewProps } from './components/DocumentPreview.js';
26
+ export { DocumentPreview } from './components/DocumentPreview.js';
27
+ export type { VibecodingWorkbenchProps } from './components/VibecodingWorkbench.js';
28
+ export { VibecodingWorkbench } from './components/VibecodingWorkbench.js';
29
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,7 @@
1
+ export { createVibecodingSession } from './session.js';
2
+ export { useVibecodingSession } from './use-vibecoding-session.js';
3
+ export { VIBECODING_TOOL_IDS, createVibecodingToolRegistrations, createVibecodingToolRunner, } from './tools.js';
4
+ export { PartsViewer } from './components/PartsViewer.js';
5
+ export { DocumentPreview } from './components/DocumentPreview.js';
6
+ export { VibecodingWorkbench } from './components/VibecodingWorkbench.js';
7
+ //# sourceMappingURL=index.js.map