@jini-ai/vibecoding 0.6.15-rc.1 → 0.6.15

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.
@@ -49,17 +49,6 @@ export declare function applyEdits({ target, edits }: {
49
49
  readonly target: EditTarget;
50
50
  readonly edits: readonly ProposedEdit[];
51
51
  }): Promise<readonly ApplyOutcome[]>;
52
- /**
53
- * The subset of outcomes a host should feed back to the model as its next turn.
54
- *
55
- * Rejections carry a reason written for the model; write failures carry an error message. Both are
56
- * things the model can act on, and neither reaches it unless the host asks for them here — which
57
- * is what closes the loop between validation and correction.
58
- *
59
- * @param requiredArgs - results from `applyEdits`.
60
- * @returns one `{ id, reason }` per non-applied outcome, in input order.
61
- * @complexity 3
62
- */
63
52
  export declare function correctionsFor({ outcomes }: {
64
53
  readonly outcomes: readonly ApplyOutcome[];
65
54
  }): readonly {
@@ -54,15 +54,18 @@ export async function applyEdits({ target, edits }) {
54
54
  * @returns one `{ id, reason }` per non-applied outcome, in input order.
55
55
  * @complexity 3
56
56
  */
57
+ /** Why an outcome needs correcting; applied outcomes need none. */
58
+ function correctionReason(outcome) {
59
+ if (outcome.status === "rejected")
60
+ return outcome.reason;
61
+ return outcome.status === "failed" ? outcome.error.message : undefined;
62
+ }
57
63
  export function correctionsFor({ outcomes }) {
58
64
  const corrections = [];
59
65
  for (const outcome of outcomes) {
60
- if (outcome.status === "rejected") {
61
- corrections.push({ id: outcome.id, reason: outcome.reason });
62
- }
63
- else if (outcome.status === "failed") {
64
- corrections.push({ id: outcome.id, reason: outcome.error.message });
65
- }
66
+ const reason = correctionReason(outcome);
67
+ if (reason !== undefined)
68
+ corrections.push({ id: outcome.id, reason });
66
69
  }
67
70
  return corrections;
68
71
  }
package/dist/html/edit.js CHANGED
@@ -1,3 +1,19 @@
1
+ import { trimCharsEnd } from "@jini-ai/core/primitives";
2
+ /** Replaces each `<...>` tag with a space, exactly as `text.replace(/<[^>]*>/g, " ")` does but in
3
+ * linear time: once a `<` has no closing `>` after it, no later `<` has one either. */
4
+ function replaceTags(text) {
5
+ let out = "";
6
+ let from = 0;
7
+ let open = text.indexOf("<");
8
+ let close = open === -1 ? -1 : text.indexOf(">", open + 1);
9
+ while (close !== -1) {
10
+ out += text.slice(from, open) + " ";
11
+ from = close + 1;
12
+ open = text.indexOf("<", from);
13
+ close = open === -1 ? -1 : text.indexOf(">", open + 1);
14
+ }
15
+ return out + text.slice(from);
16
+ }
1
17
  /** Resolve one address, refusing duplicates and unsafe boundaries without throwing on reads. */
2
18
  export function locateRegion({ scan, handle }, _options = {}) {
3
19
  const matches = scan.regions.filter((region) => region.handle === handle);
@@ -56,7 +72,7 @@ export function suggestRegionHandle({ inner, taken }, { slugify }) {
56
72
  const heading = HEADING_PATTERN.exec(inner);
57
73
  if (!heading)
58
74
  return undefined;
59
- const slug = slugify(heading[1].replace(/<[^>]*>/g, " ")).slice(0, 48).replace(/-+$/g, "");
75
+ const slug = trimCharsEnd({ text: slugify(replaceTags(heading[1])).slice(0, 48), chars: "-" });
60
76
  if (slug === "" || taken.has(slug))
61
77
  return undefined;
62
78
  return slug;
@@ -10,7 +10,9 @@ import { isValidRegionHandle, type HtmlRegionParser } from "../regions.js";
10
10
  */
11
11
  export declare function scanHtmlRegions({ html: html_ }: {
12
12
  readonly html: string;
13
- }, _options?: Record<string, never>): HtmlRegionScan;
13
+ }, _options?: Record<string, never>): HtmlRegionScan & {
14
+ readonly parseErrorReasons: readonly string[];
15
+ };
14
16
  /**
15
17
  * Build the parse5-backed `HtmlRegionParser`. Stateless — a single instance is safe to share as a
16
18
  * module-level singleton across every call, per `./regions.js`'s injected-port contract.
@@ -104,9 +104,7 @@ function* walkNodes(root) {
104
104
  while (pending.length > 0) {
105
105
  const node = pending.pop(); // Nonempty stack on this branch.
106
106
  yield node;
107
- const children = "content" in node ? node.content.childNodes : "childNodes" in node ? node.childNodes : undefined;
108
- if (children === undefined)
109
- continue;
107
+ const children = childrenOf(node);
110
108
  for (let index = children.length - 1; index >= 0; index -= 1)
111
109
  pending.push(children[index]);
112
110
  }
@@ -174,15 +172,19 @@ function analyzeRegion(element, handle) {
174
172
  ...(problem === undefined ? {} : { problem }),
175
173
  };
176
174
  }
175
+ /** A template's content children, an ordinary node's children, or none for a leaf. */
176
+ function childrenOf(node) {
177
+ if ("content" in node)
178
+ return node.content.childNodes;
179
+ return "childNodes" in node ? node.childNodes : [];
180
+ }
177
181
  /** Direct fragment children in source order; implied closes suffice for tagging advice, not edits. */
178
182
  function topLevelElements(fragment) {
179
183
  const elements = [];
180
184
  for (const child of fragment.childNodes) {
181
- if (!("tagName" in child))
185
+ // Implicit wrappers have no authored open tag to annotate.
186
+ if (!("tagName" in child) || !child.sourceCodeLocation?.startTag)
182
187
  continue;
183
- const location = child.sourceCodeLocation;
184
- if (!location?.startTag)
185
- continue; // Implicit wrappers have no authored open tag to annotate.
186
188
  const handle = attributeValue(child, AGENT_ELEMENT_ATTRIBUTE);
187
189
  elements.push({
188
190
  tag: child.tagName,
@@ -292,7 +294,8 @@ export function createParse5RegionParser() {
292
294
  const problem = locationProblem(scan);
293
295
  if (problem !== undefined)
294
296
  return { ok: false, reason: problem };
295
- const parseErrorReasons = scan.parseErrorReasons ?? [];
297
+ // This concrete scanner always initializes the list, unlike the optional generic port field.
298
+ const parseErrorReasons = scan.parseErrorReasons;
296
299
  if (parseErrorReasons.length > 0) {
297
300
  return { ok: false, reason: `the document does not parse cleanly: ${parseErrorReasons.join("; ")}` };
298
301
  }
@@ -77,6 +77,14 @@ function handleCounts(regions) {
77
77
  return counts;
78
78
  }
79
79
  /** A stable, order-insensitive description of a handle multiset, for comparison in a message. */
80
+ /** Which way an edit disturbed the region's agent handles. */
81
+ function handleChangeDetail({ invented, removed }) {
82
+ if (invented.length > 0)
83
+ return `it adds ${AGENT_ELEMENT_ATTRIBUTE} handles that did not exist (${describeHandles(invented)})`;
84
+ if (removed.length > 0)
85
+ return `it removes existing ${AGENT_ELEMENT_ATTRIBUTE} handles (${describeHandles(removed)})`;
86
+ return `it changes how many times a ${AGENT_ELEMENT_ATTRIBUTE} handle appears`;
87
+ }
80
88
  function describeHandles(handles) {
81
89
  return [...handles].sort().join(", ") || "(none)";
82
90
  }
@@ -202,11 +210,7 @@ export function createHtmlRegionTarget(deps, options = {}) {
202
210
  if (beforeKey !== afterKey) {
203
211
  const invented = afterHandles.filter((handle) => !beforeHandles.includes(handle));
204
212
  const removed = beforeHandles.filter((handle) => !afterHandles.includes(handle));
205
- const detail = invented.length > 0
206
- ? `it adds ${AGENT_ELEMENT_ATTRIBUTE} handles that did not exist (${describeHandles(invented)})`
207
- : removed.length > 0
208
- ? `it removes existing ${AGENT_ELEMENT_ATTRIBUTE} handles (${describeHandles(removed)})`
209
- : `it changes how many times a ${AGENT_ELEMENT_ATTRIBUTE} handle appears`;
213
+ const detail = handleChangeDetail({ invented, removed });
210
214
  return {
211
215
  ok: false,
212
216
  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.`,
@@ -41,7 +41,8 @@ export function canonicalPath({ relativePath, maxDepth }, _options = {}) {
41
41
  }
42
42
  /** Each segment must survive Windows parsing without aliases, streams or device semantics. */
43
43
  function assertSegment({ segment }) {
44
- if (!segment || segment === '.' || segment === '..' || /[<>:"|?*\x00-\x1f\x7f]/.test(segment) || /[. ]$/.test(segment) || DEVICE.test(segment)) {
44
+ const reservedName = !segment || segment === '.' || segment === '..';
45
+ if (reservedName || /[<>:"|?*\x00-\x1f\x7f]/.test(segment) || /[. ]$/.test(segment) || DEVICE.test(segment)) {
45
46
  throw new ProjectError({ code: 'INVALID_PATH', message: `nonportable path segment: ${segment}` });
46
47
  }
47
48
  }
@@ -29,11 +29,9 @@ function changeFiles({ files: base, changes, paths, limits }) {
29
29
  return files;
30
30
  }
31
31
  function changeFile({ files, change, path, limits }) {
32
- if (change.op === 'create') {
33
- if (files.has(path))
34
- fail('INVALID_CHANGE', `file already exists: ${path}`);
35
- }
36
- else if (!files.has(path))
32
+ if (change.op === 'create' && files.has(path))
33
+ fail('INVALID_CHANGE', `file already exists: ${path}`);
34
+ if (change.op !== 'create' && !files.has(path))
37
35
  fail('INVALID_CHANGE', `file not listed: ${path}`);
38
36
  if (change.op === 'delete') {
39
37
  files.delete(path);
@@ -300,15 +298,17 @@ function assembleProject({ store, paths, limits, historyLimit, validator }) {
300
298
  undo: () => replay('undo'), redo: () => replay('redo'), restore: restoreText,
301
299
  entries: rawHistory.entries, retainedEntries: rawHistory.retainedEntries, clear: rawHistory.clear,
302
300
  };
301
+ const listPartsLocked = async () => [...(await captureLocked()).keys()].sort().map(id => ({ id }));
302
+ const readPartLocked = async (id) => {
303
+ paths.check({ relativePath: id });
304
+ const manifest = await store.manifest({ revisionId: selected().revision });
305
+ if (!manifest.files.some(file => file.path === id))
306
+ fail('INVALID_CHANGE', `file not listed: ${id}`);
307
+ return decodeText({ bytes: await store.readWorkingFile({ relativePath: id }) });
308
+ };
303
309
  const target = {
304
- listParts: () => serial(() => locked(async () => [...(await captureLocked()).keys()].sort().map(id => ({ id })))),
305
- readPart: ({ id }) => serial(() => locked(async () => {
306
- paths.check({ relativePath: id });
307
- const manifest = await store.manifest({ revisionId: selected().revision });
308
- if (!manifest.files.some(file => file.path === id))
309
- fail('INVALID_CHANGE', `file not listed: ${id}`);
310
- return decodeText({ bytes: await store.readWorkingFile({ relativePath: id }) });
311
- })),
310
+ listParts: () => serial(() => locked(listPartsLocked)),
311
+ readPart: ({ id }) => serial(() => locked(() => readPartLocked(id))),
312
312
  replacePart: async (edit) => {
313
313
  const outcomes = await applyEdits({ edits: [edit] });
314
314
  const outcome = outcomes[0];
@@ -35,12 +35,18 @@ function assertHash(hash) { if (!HASH.test(hash))
35
35
  function missing(error) { return error?.code === 'ENOENT'; }
36
36
  /** Absent destinations (or a file where a parent folder was expected) are not errors here. */
37
37
  async function lstatIfPresent(path) {
38
- return fs.lstat(path).catch(error => { if (missing(error) || error.code === 'ENOTDIR')
39
- return undefined; throw error; });
38
+ return fs.lstat(path).catch(error => {
39
+ if (missing(error) || error.code === 'ENOTDIR')
40
+ return undefined;
41
+ throw error;
42
+ });
40
43
  }
41
44
  async function statIfPresent(path) {
42
- return fs.stat(path).catch(error => { if (missing(error))
43
- return undefined; throw error; });
45
+ return fs.stat(path).catch(error => {
46
+ if (missing(error))
47
+ return undefined;
48
+ throw error;
49
+ });
44
50
  }
45
51
  /** Reject links and special files even when platform containment permits an in-root alias.
46
52
  * Host owns the directory; this recheck is not a sandbox against hostile local processes.
@@ -111,12 +117,22 @@ function checkManifest({ value, paths, limits }) {
111
117
  assertCandidatePaths({ paths: [...seen] });
112
118
  return manifest;
113
119
  }
120
+ /** A safe, non-negative integer no larger than `max` (a stored size is untrusted data). */
121
+ function isByteCount(value, max = Number.POSITIVE_INFINITY) {
122
+ return Number.isSafeInteger(value) && value >= 0 && value <= max;
123
+ }
124
+ /** Code-unit order, independent of locale. */
125
+ function compareCodeUnits(a, b) {
126
+ if (a === b)
127
+ return 0;
128
+ return a < b ? -1 : 1;
129
+ }
114
130
  function checkManifestFile({ file, paths, limits }) {
115
131
  if (!file || typeof file.path !== 'string' || typeof file.hash !== 'string')
116
132
  corrupt('invalid manifest file');
117
133
  assertHash(file.hash);
118
134
  paths.check({ relativePath: file.path });
119
- if (!Number.isSafeInteger(file.size) || file.size < 0 || file.size > limits.maxFileBytes)
135
+ if (!isByteCount(file.size, limits.maxFileBytes))
120
136
  corrupt('invalid manifest size');
121
137
  }
122
138
  function checkCurrent({ value, root }) {
@@ -139,7 +155,7 @@ function checkObserved(observed) {
139
155
  function checkFingerprint(value) {
140
156
  if (!value || typeof value !== 'object')
141
157
  corrupt('invalid file fingerprint');
142
- if (!Number.isSafeInteger(value.size) || value.size < 0)
158
+ if (!isByteCount(value.size))
143
159
  corrupt('invalid fingerprint size');
144
160
  if (!Number.isFinite(value.mtimeMs) || !Number.isFinite(value.ctimeMs))
145
161
  corrupt('invalid fingerprint timestamps');
@@ -209,7 +225,7 @@ export function createProjectStore({ root, stateDir, paths, limits, filesystem }
209
225
  async function prepare({ files }) {
210
226
  assertCandidatePaths({ paths: [...files.keys()] });
211
227
  const entries = [];
212
- for (const [path, bytes] of [...files].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
228
+ for (const [path, bytes] of [...files].sort(([a], [b]) => compareCodeUnits(a, b))) {
213
229
  const file = { path, hash: digest({ bytes }), size: bytes.byteLength };
214
230
  entries.push(file);
215
231
  }
@@ -464,8 +480,11 @@ export function createProjectStore({ root, stateDir, paths, limits, filesystem }
464
480
  }
465
481
  /** Generated orphan objects/manifests are safe to discard only while holding the project lock. */
466
482
  async function removeUnreferenced({ directory, keep }) {
467
- const names = await fs.readdir(directory).catch(error => { if (missing(error))
468
- return []; throw error; });
483
+ const names = await fs.readdir(directory).catch(error => {
484
+ if (missing(error))
485
+ return [];
486
+ throw error;
487
+ });
469
488
  for (const name of names) {
470
489
  if (!keep.has(name))
471
490
  await fs.rm(resolve(directory, name), { force: true });
@@ -39,11 +39,5 @@ export interface PartsViewerProps {
39
39
  readonly className?: string;
40
40
  readonly style?: CSSProperties;
41
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
42
  export declare function PartsViewer({ parts, selectedId, onSelect, content, contentLoading, className, style }: PartsViewerProps): import("react").JSX.Element;
49
43
  //# sourceMappingURL=PartsViewer.d.ts.map
@@ -52,10 +52,19 @@ const EMPTY_STYLE = { padding: 12, color: '#6b7280' };
52
52
  * @complexity O(parts.length) to render the list; content rendering is O(content.length).
53
53
  * @overallScore 100/100
54
54
  */
55
+ /** The content pane: a prompt with no selection, a loading note, or the selected part's text. */
56
+ function partContent({ selectedId, contentLoading, content }) {
57
+ if (selectedId === undefined || selectedId === null) {
58
+ return _jsx("p", { style: { ...CONTENT_STYLE, ...EMPTY_STYLE }, children: "Select a part to inspect its content." });
59
+ }
60
+ if (contentLoading)
61
+ return _jsx("p", { style: { ...CONTENT_STYLE, ...EMPTY_STYLE }, children: "Loading\u2026" });
62
+ return _jsx("pre", { style: CONTENT_STYLE, children: content ?? '' });
63
+ }
55
64
  export function PartsViewer({ parts, selectedId, onSelect, content, contentLoading, className, style }) {
56
65
  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
66
  const isSelected = part.id === selectedId;
58
67
  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 ?? '' }))] }));
68
+ })) }), partContent({ selectedId, contentLoading, content })] }));
60
69
  }
61
70
  //# sourceMappingURL=PartsViewer.js.map
@@ -69,7 +69,7 @@ export function VibecodingWorkbench({ session, documentHtml, className, style })
69
69
  useEffect(() => {
70
70
  if (selectedId === null) {
71
71
  setSelectedContent(undefined);
72
- return;
72
+ return undefined;
73
73
  }
74
74
  let cancelled = false;
75
75
  setContentLoading(true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/vibecoding",
3
- "version": "0.6.15-rc.1",
3
+ "version": "0.6.15",
4
4
  "description": "Conversational artifact authoring: a target-agnostic edit loop — addressable parts, validated replace, operation-level undo/redo, and per-turn snapshot/rewind. Framework-free in `/core`; tagged-region HTML documents behind ./html; filesystem targets behind ./node and the chat/preview surface behind ./react.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -87,11 +87,11 @@
87
87
  "typescript": "^5.9.2",
88
88
  "vitest": "^3.2.4",
89
89
  "parse5": "^8.0.1",
90
- "@jini-ai/core": "0.6.15-rc.1",
91
- "@jini-ai/platform": "^0.6.15-rc.1"
90
+ "@jini-ai/core": "0.6.15",
91
+ "@jini-ai/platform": "^0.6.15"
92
92
  },
93
93
  "dependencies": {
94
- "@jini-ai/core": "^0.6.15-rc.1"
94
+ "@jini-ai/core": "^0.6.15"
95
95
  },
96
96
  "peerDependencies": {
97
97
  "react": "^18.3.0 || ^19.0.0",