@vibes.diy/call-ai-v2 2.2.1 → 2.2.2

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/README.md CHANGED
@@ -179,6 +179,23 @@ Parses markdown structure from accumulated content, detecting code fences and im
179
179
  **Input:** `DeltaStreamMsg`
180
180
  **Messages:** `block.begin`, `block.toplevel.begin/line/end`, `block.code.begin/line/end`, `block.image`, `block.end`, `block.stats`
181
181
 
182
+ `block.code.*` carries an optional `path` field, derived aider-style from the most-recent non-blank toplevel line preceding the fence (if it looks like a relative path with a recognized extension). Falls back to `App.jsx` otherwise.
183
+
184
+ ### fence-body-parser.ts
185
+
186
+ Pure function that turns the lines inside a code fence into `Edit[]`. A body with no markers is a single `create`. A body with `<<<<<<< SEARCH` / `=======` / `>>>>>>> REPLACE` markers becomes one or more `replace` edits (multiple sections allowed in one fence).
187
+
188
+ ### apply-edits.ts
189
+
190
+ Pure helpers `applyReplace` and `applyEdits`. `applyReplace` first tries an exact match; on failure it falls back to a trailing-whitespace-tolerant match. Result reports `matchKind` (`exact` | `trailing-ws`).
191
+
192
+ ### filesystem-stream.ts
193
+
194
+ Aider-style virtual filesystem stage. Sits after `sections-stream`. Owns a `VirtualFS = Map<path, string>` for the life of one streamed turn, seeded from the caller-supplied `seed` (typically the saved `App.jsx`). Each `block.code.end` is parsed via `parseFenceBody`; the resulting edits are applied with `applyEdits`. On success, emits `fs.file.snapshot`. Failed sections (parse errors, missing or ambiguous SEARCH) emit `fs.apply.error` and leave the VFS unchanged. On `block.end`, emits `fs.turn.end` with the final files map.
195
+
196
+ **Input:** `BlockStreamMsg`
197
+ **Messages added:** `fs.file.snapshot`, `fs.apply.error`, `fs.turn.end`
198
+
182
199
  ## The Passthrough Pattern
183
200
 
184
201
  Streams use `passthrough()` to automatically forward all upstream messages while adding their own:
@@ -246,21 +263,41 @@ if (isDeltaStats(msg)) console.log("Delta stats:", msg.stats);
246
263
  if (isBlockStats(msg)) console.log("Block stats:", msg.stats);
247
264
  ```
248
265
 
249
- ## Current Status
266
+ ## Filesystem stage usage
267
+
268
+ ```typescript
269
+ import { createSectionsStream, createFileSystemStream, isFsFileSnapshot, isFsTurnEnd } from "call-ai/v2";
250
270
 
251
- Currently used by:
271
+ const seed = new Map([["App.jsx", priorAppJsx]]);
272
+
273
+ const pipeline = response.body
274
+ // …line/data/sse/delta stages…
275
+ .pipeThrough(createSectionsStream(streamId, createId))
276
+ .pipeThrough(createFileSystemStream({ streamId, createId, seed }));
277
+
278
+ for await (const msg of pipeline) {
279
+ if (isFsFileSnapshot(msg)) {
280
+ // Update live preview with msg.content for msg.path
281
+ }
282
+ if (isFsTurnEnd(msg)) {
283
+ // Persist msg.files to the session doc
284
+ }
285
+ }
286
+ ```
287
+
288
+ ## Current Status
252
289
 
253
- - CLI tool (`cli.ts`) for testing and debugging
254
- - Unit tests
290
+ Production: this pipeline is the live streaming path for vibes.diy chat
291
+ ([`prompt-chat-section.ts`](../../vibes.diy/api/svc/public/prompt-chat-section.ts)
292
+ pipes the LLM response body through line → data → sse → delta → sections, and
293
+ the client reducer consumes the typed block messages directly).
255
294
 
256
- Integration plans (not part of this package):
295
+ Also used by:
257
296
 
258
- - vibes.diy chat using advanced API
259
- - Auth for vibes iframe runtime
260
- - Generated vibes legacy callAI wrapper
297
+ - CLI tool (`cli.ts`) for replay/debugging captured SSE files
298
+ - Unit tests across the v2 modules
261
299
 
262
300
  ## TODO
263
301
 
264
302
  - [ ] **Chunked image decoding**: Add `createImageDecodeStream` that fetches image URLs, decodes to bytes, and emits `image.begin`/`image.fragment`/`image.end` with shared `imageId` for streaming large images in fixed-size chunks
265
303
  - [ ] **Production worker**: Deploy pipeline to Cloudflare Worker with events as network transport, client consumes typed events directly instead of raw SSE
266
- - [ ] **Migrate vibes.diy chat**: Replace `call-ai` v1 + `segment-parser.ts` with v2 pipeline, consuming typed `isCodeLine`/`isToplevelLine` events instead of regex parsing
@@ -0,0 +1,39 @@
1
+ export interface ApplyReplaceInput {
2
+ readonly source: string;
3
+ readonly search: string;
4
+ readonly replace: string;
5
+ }
6
+ export interface ApplyEditOk {
7
+ readonly ok: true;
8
+ readonly content: string;
9
+ readonly matchKind: "exact" | "trailing-ws";
10
+ }
11
+ export type ApplyEditErrReason = "no-match" | "multiple-match";
12
+ export interface ApplyEditErr {
13
+ readonly ok: false;
14
+ readonly reason: ApplyEditErrReason;
15
+ readonly matchCount: number;
16
+ }
17
+ export type ApplyEditResult = ApplyEditOk | ApplyEditErr;
18
+ export declare function applyReplace(input: ApplyReplaceInput): ApplyEditResult;
19
+ export interface ReplaceEdit {
20
+ readonly op: "replace";
21
+ readonly search: string;
22
+ readonly replace: string;
23
+ }
24
+ export interface CreateEdit {
25
+ readonly op: "create";
26
+ readonly content: string;
27
+ }
28
+ export type Edit = ReplaceEdit | CreateEdit;
29
+ export interface ApplyEditsError {
30
+ readonly index: number;
31
+ readonly reason: ApplyEditErrReason;
32
+ readonly matchCount: number;
33
+ readonly search: string;
34
+ }
35
+ export interface ApplyEditsResult {
36
+ readonly content: string;
37
+ readonly errors: readonly ApplyEditsError[];
38
+ }
39
+ export declare function applyEdits(seed: string, edits: readonly Edit[]): ApplyEditsResult;
package/apply-edits.js ADDED
@@ -0,0 +1,76 @@
1
+ function rstripLines(s) {
2
+ return s
3
+ .split("\n")
4
+ .map((l) => l.replace(/[ \t]+$/, ""))
5
+ .join("\n");
6
+ }
7
+ function findAllOccurrences(haystack, needle) {
8
+ const hits = [];
9
+ if (needle.length === 0)
10
+ return hits;
11
+ let from = 0;
12
+ while (true) {
13
+ const idx = haystack.indexOf(needle, from);
14
+ if (idx === -1)
15
+ break;
16
+ hits.push(idx);
17
+ from = idx + needle.length;
18
+ }
19
+ return hits;
20
+ }
21
+ export function applyReplace(input) {
22
+ const { source, search, replace } = input;
23
+ if (search.length === 0) {
24
+ return { ok: false, reason: "no-match", matchCount: 0 };
25
+ }
26
+ const exact = findAllOccurrences(source, search);
27
+ if (exact.length === 1) {
28
+ const idx = exact[0];
29
+ return {
30
+ ok: true,
31
+ matchKind: "exact",
32
+ content: source.slice(0, idx) + replace + source.slice(idx + search.length),
33
+ };
34
+ }
35
+ if (exact.length > 1) {
36
+ return { ok: false, reason: "multiple-match", matchCount: exact.length };
37
+ }
38
+ const sourceTrimmed = rstripLines(source);
39
+ const searchTrimmed = rstripLines(search);
40
+ const tolerant = findAllOccurrences(sourceTrimmed, searchTrimmed);
41
+ if (tolerant.length === 1) {
42
+ const idx = tolerant[0];
43
+ return {
44
+ ok: true,
45
+ matchKind: "trailing-ws",
46
+ content: sourceTrimmed.slice(0, idx) + replace + sourceTrimmed.slice(idx + searchTrimmed.length),
47
+ };
48
+ }
49
+ if (tolerant.length > 1) {
50
+ return { ok: false, reason: "multiple-match", matchCount: tolerant.length };
51
+ }
52
+ return { ok: false, reason: "no-match", matchCount: 0 };
53
+ }
54
+ export function applyEdits(seed, edits) {
55
+ let content = seed;
56
+ const errors = [];
57
+ edits.forEach((edit, index) => {
58
+ if (edit.op === "create") {
59
+ content = edit.content;
60
+ return;
61
+ }
62
+ const r = applyReplace({ source: content, search: edit.search, replace: edit.replace });
63
+ if (r.ok) {
64
+ content = r.content;
65
+ return;
66
+ }
67
+ errors.push({
68
+ index,
69
+ reason: r.reason,
70
+ matchCount: r.matchCount,
71
+ search: edit.search,
72
+ });
73
+ });
74
+ return { content, errors };
75
+ }
76
+ //# sourceMappingURL=apply-edits.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-edits.js","sourceRoot":"","sources":["../jsr/apply-edits.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,85 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { applyEdits, applyReplace } from "./apply-edits.js";
3
+ describe("applyReplace", () => {
4
+ it("replaces a unique exact match", () => {
5
+ const r = applyReplace({ source: "hello world", search: "world", replace: "there" });
6
+ expect(r).toEqual({ ok: true, matchKind: "exact", content: "hello there" });
7
+ });
8
+ it("fails with no-match when search is absent", () => {
9
+ const r = applyReplace({ source: "hello world", search: "xyz", replace: "abc" });
10
+ expect(r).toEqual({ ok: false, reason: "no-match", matchCount: 0 });
11
+ });
12
+ it("fails with multiple-match when search appears twice", () => {
13
+ const r = applyReplace({ source: "ab ab", search: "ab", replace: "cd" });
14
+ expect(r.ok).toBe(false);
15
+ if (r.ok === false) {
16
+ expect(r.reason).toBe("multiple-match");
17
+ expect(r.matchCount).toBe(2);
18
+ }
19
+ });
20
+ it("treats empty search as no-match", () => {
21
+ const r = applyReplace({ source: "hello", search: "", replace: "x" });
22
+ expect(r).toEqual({ ok: false, reason: "no-match", matchCount: 0 });
23
+ });
24
+ it("preserves whitespace and indentation on exact match", () => {
25
+ const r = applyReplace({
26
+ source: "line1\n line2\nline3",
27
+ search: " line2",
28
+ replace: " LINE2",
29
+ });
30
+ expect(r).toEqual({ ok: true, matchKind: "exact", content: "line1\n LINE2\nline3" });
31
+ });
32
+ it("falls back to trailing-whitespace-tolerant match", () => {
33
+ const r = applyReplace({ source: "foo \nbar\nbaz", search: "foo\nbar", replace: "FOO\nBAR" });
34
+ expect(r.ok).toBe(true);
35
+ if (r.ok === true) {
36
+ expect(r.matchKind).toBe("trailing-ws");
37
+ expect(r.content).toBe("FOO\nBAR\nbaz");
38
+ }
39
+ });
40
+ it("tolerant fallback still reports multiple-match", () => {
41
+ const r = applyReplace({ source: "foo \nfoo\t\nend", search: "foo", replace: "X" });
42
+ expect(r.ok).toBe(false);
43
+ if (r.ok === false)
44
+ expect(r.reason).toBe("multiple-match");
45
+ });
46
+ });
47
+ describe("applyEdits", () => {
48
+ it("applies a create then a sequence of replaces", () => {
49
+ const edits = [
50
+ { op: "create", content: "const a = 1;\nconst b = 2;\n" },
51
+ { op: "replace", search: "const a = 1;", replace: "const a = 10;" },
52
+ { op: "replace", search: "const b = 2;", replace: "const b = 20;" },
53
+ ];
54
+ const r = applyEdits("", edits);
55
+ expect(r.content).toBe("const a = 10;\nconst b = 20;\n");
56
+ expect(r.errors).toEqual([]);
57
+ });
58
+ it("uses seed when first edit is a replace", () => {
59
+ const seed = "hello world";
60
+ const r = applyEdits(seed, [{ op: "replace", search: "world", replace: "there" }]);
61
+ expect(r.content).toBe("hello there");
62
+ expect(r.errors).toEqual([]);
63
+ });
64
+ it("collects failures and continues with unchanged source", () => {
65
+ const seed = "one two three";
66
+ const r = applyEdits(seed, [
67
+ { op: "replace", search: "missing", replace: "x" },
68
+ { op: "replace", search: "two", replace: "TWO" },
69
+ { op: "replace", search: "e", replace: "E" },
70
+ ]);
71
+ expect(r.content).toBe("one TWO three");
72
+ expect(r.errors).toHaveLength(2);
73
+ expect(r.errors[0]).toMatchObject({ index: 0, reason: "no-match" });
74
+ expect(r.errors[1]).toMatchObject({ index: 2, reason: "multiple-match" });
75
+ });
76
+ it("create after replaces resets content", () => {
77
+ const r = applyEdits("original", [
78
+ { op: "replace", search: "original", replace: "edited" },
79
+ { op: "create", content: "fresh" },
80
+ ]);
81
+ expect(r.content).toBe("fresh");
82
+ expect(r.errors).toEqual([]);
83
+ });
84
+ });
85
+ //# sourceMappingURL=apply-edits.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-edits.test.js","sourceRoot":"","sources":["../jsr/apply-edits.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAE5D,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;IAC5B,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;QACrF,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;IAC9E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;QACjF,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,CAAC,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QACzE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,IAAI,CAAC,CAAC,EAAE,KAAK,KAAK,EAAE,CAAC;YACnB,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;YACxC,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,iCAAiC,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;QACtE,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAC;IACtE,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,CAAC,GAAG,YAAY,CAAC;YACrB,MAAM,EAAE,uBAAuB;YAC/B,MAAM,EAAE,SAAS;YACjB,OAAO,EAAE,SAAS;SACnB,CAAC,CAAC;QACH,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC,CAAC;IACxF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,CAAC,CAAC;QAChG,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxB,IAAI,CAAC,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC;YAClB,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YACxC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,CAAC,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,mBAAmB,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC;QACrF,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACzB,IAAI,CAAC,CAAC,EAAE,KAAK,KAAK;YAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC;IAC9D,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACtD,MAAM,KAAK,GAAG;YACZ,EAAE,EAAE,EAAE,QAAiB,EAAE,OAAO,EAAE,8BAA8B,EAAE;YAClE,EAAE,EAAE,EAAE,SAAkB,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,eAAe,EAAE;YAC5E,EAAE,EAAE,EAAE,SAAkB,EAAE,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,eAAe,EAAE;SAC7E,CAAC;QACF,MAAM,CAAC,GAAG,UAAU,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QAChC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAC;QACzD,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,wCAAwC,EAAE,GAAG,EAAE;QAChD,MAAM,IAAI,GAAG,aAAa,CAAC;QAC3B,MAAM,CAAC,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC;QACnF,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QACtC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,IAAI,GAAG,eAAe,CAAC;QAC7B,MAAM,CAAC,GAAG,UAAU,CAAC,IAAI,EAAE;YACzB,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,GAAG,EAAE;YAClD,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE;YAChD,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE;SAC7C,CAAC,CAAC;QACH,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QACxC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;QACpE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC,CAAC;IAC5E,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,GAAG,UAAU,CAAC,UAAU,EAAE;YAC/B,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE;YACxD,EAAE,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE;SACnC,CAAC,CAAC;QACH,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAChC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC/B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { stream2array } from "@adviser/cement";
3
+ import { createBlockStream, isCodeBegin, isCodeLine, isCodeEnd } from "./block-stream.js";
4
+ const innerStreamId = "inner";
5
+ const streamId = "test";
6
+ function makeLineEvents(lines) {
7
+ const events = [{ type: "line.begin", streamId: innerStreamId, timestamp: new Date() }];
8
+ lines.forEach((content, i) => {
9
+ events.push({
10
+ type: "line.line",
11
+ streamId: innerStreamId,
12
+ content,
13
+ lineNr: i + 1,
14
+ timestamp: new Date(),
15
+ });
16
+ });
17
+ events.push({ type: "line.end", streamId: innerStreamId, totalLines: lines.length, timestamp: new Date() });
18
+ return events;
19
+ }
20
+ async function runBlockStream(lines) {
21
+ const events = makeLineEvents(lines);
22
+ const input = new ReadableStream({
23
+ start(controller) {
24
+ events.forEach((e) => controller.enqueue(e));
25
+ controller.close();
26
+ },
27
+ });
28
+ let idCounter = 0;
29
+ const createId = () => `id-${++idCounter}`;
30
+ return stream2array(input.pipeThrough(createBlockStream(streamId, innerStreamId, createId)));
31
+ }
32
+ describe("block-stream path-line tracking", () => {
33
+ it("attaches path 'App.jsx' (default) when no path line precedes the fence", async () => {
34
+ const chunks = await runBlockStream(["Some intro prose.", "```jsx", "const x = 1;", "```"]);
35
+ const begin = chunks.find((c) => isCodeBegin(c));
36
+ const end = chunks.find((c) => isCodeEnd(c));
37
+ expect(begin?.path).toBe("App.jsx");
38
+ expect(end?.path).toBe("App.jsx");
39
+ });
40
+ it("attaches the preceding path-line as the path", async () => {
41
+ const chunks = await runBlockStream(["Building a layout.", "App.jsx", "```jsx", "const x = 1;", "```"]);
42
+ const begin = chunks.find((c) => isCodeBegin(c));
43
+ const end = chunks.find((c) => isCodeEnd(c));
44
+ expect(begin?.path).toBe("App.jsx");
45
+ expect(end?.path).toBe("App.jsx");
46
+ });
47
+ it("recognizes nested-path filenames with allowed extensions", async () => {
48
+ const chunks = await runBlockStream(["src/components/Foo.tsx", "```tsx", "export const Foo = () => null;", "```"]);
49
+ const begin = chunks.find((c) => isCodeBegin(c));
50
+ expect(begin?.path).toBe("src/components/Foo.tsx");
51
+ });
52
+ it("ignores a non-path-looking preceding line", async () => {
53
+ const chunks = await runBlockStream(["Here is the code:", "```jsx", "const x = 1;", "```"]);
54
+ const begin = chunks.find((c) => isCodeBegin(c));
55
+ expect(begin?.path).toBe("App.jsx");
56
+ });
57
+ it("stamps path on every code.line within the block", async () => {
58
+ const chunks = await runBlockStream(["App.jsx", "```jsx", "const a = 1;", "const b = 2;", "```"]);
59
+ const lines = chunks.filter((c) => isCodeLine(c));
60
+ expect(lines).toHaveLength(2);
61
+ for (const l of lines)
62
+ expect(l.path).toBe("App.jsx");
63
+ });
64
+ it("uses the most recent non-blank toplevel line, not earlier ones", async () => {
65
+ const chunks = await runBlockStream(["First paragraph.", "App.jsx", "", "```jsx", "const x = 1;", "```"]);
66
+ const begin = chunks.find((c) => isCodeBegin(c));
67
+ expect(begin?.path).toBe("App.jsx");
68
+ });
69
+ it("does not carry a path-line forward across multiple blocks if the second has its own toplevel section", async () => {
70
+ const chunks = await runBlockStream([
71
+ "App.jsx",
72
+ "```jsx",
73
+ "const a = 1;",
74
+ "```",
75
+ "Now some more prose without a path line.",
76
+ "```jsx",
77
+ "const b = 2;",
78
+ "```",
79
+ ]);
80
+ const begins = chunks.filter((c) => isCodeBegin(c));
81
+ expect(begins).toHaveLength(2);
82
+ expect(begins[0].path).toBe("App.jsx");
83
+ expect(begins[1].path).toBe("App.jsx");
84
+ });
85
+ it("rejects a path line whose extension is not in the allowed set", async () => {
86
+ const chunks = await runBlockStream(["foo.exe", "```", "binary", "```"]);
87
+ const begin = chunks.find((c) => isCodeBegin(c));
88
+ expect(begin?.path).toBe("App.jsx");
89
+ });
90
+ });
91
+ //# sourceMappingURL=block-stream-path.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-stream-path.test.js","sourceRoot":"","sources":["../jsr/block-stream-path.test.ts"],"names":[],"mappings":"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"}
package/block-stream.d.ts CHANGED
@@ -180,6 +180,7 @@ export declare const CodeBeginMsg: import("arktype/internal/variants/object.ts")
180
180
  type: "block.code.begin";
181
181
  sectionId: string;
182
182
  lang: string;
183
+ path?: string | undefined;
183
184
  }, {}>;
184
185
  export declare const CodeLineMsg: import("arktype/internal/variants/object.ts").ObjectType<{
185
186
  blockId: string;
@@ -192,6 +193,7 @@ export declare const CodeLineMsg: import("arktype/internal/variants/object.ts").
192
193
  type: "block.code.line";
193
194
  sectionId: string;
194
195
  lang: string;
196
+ path?: string | undefined;
195
197
  }, {}>;
196
198
  export declare const CodeEndMsg: import("arktype/internal/variants/object.ts").ObjectType<{
197
199
  blockId: string;
@@ -207,6 +209,7 @@ export declare const CodeEndMsg: import("arktype/internal/variants/object.ts").O
207
209
  type: "block.code.end";
208
210
  sectionId: string;
209
211
  lang: string;
212
+ path?: string | undefined;
210
213
  }, {}>;
211
214
  export declare const BlockImageMsg: import("arktype/internal/variants/object.ts").ObjectType<{
212
215
  blockId: string;
@@ -299,6 +302,7 @@ export declare const CodeMsg: import("arktype/internal/variants/object.ts").Obje
299
302
  type: "block.code.begin";
300
303
  sectionId: string;
301
304
  lang: string;
305
+ path?: string | undefined;
302
306
  } | {
303
307
  blockId: string;
304
308
  streamId: string;
@@ -310,6 +314,7 @@ export declare const CodeMsg: import("arktype/internal/variants/object.ts").Obje
310
314
  type: "block.code.line";
311
315
  sectionId: string;
312
316
  lang: string;
317
+ path?: string | undefined;
313
318
  } | {
314
319
  blockId: string;
315
320
  streamId: string;
@@ -324,6 +329,7 @@ export declare const CodeMsg: import("arktype/internal/variants/object.ts").Obje
324
329
  type: "block.code.end";
325
330
  sectionId: string;
326
331
  lang: string;
332
+ path?: string | undefined;
327
333
  }, {}>;
328
334
  export declare const LineMsg: import("arktype/internal/variants/object.ts").ObjectType<{
329
335
  blockId: string;
@@ -346,6 +352,7 @@ export declare const LineMsg: import("arktype/internal/variants/object.ts").Obje
346
352
  type: "block.code.line";
347
353
  sectionId: string;
348
354
  lang: string;
355
+ path?: string | undefined;
349
356
  }, {}>;
350
357
  export declare const BeginMsg: import("arktype/internal/variants/object.ts").ObjectType<{
351
358
  blockId: string;
@@ -364,6 +371,7 @@ export declare const BeginMsg: import("arktype/internal/variants/object.ts").Obj
364
371
  type: "block.code.begin";
365
372
  sectionId: string;
366
373
  lang: string;
374
+ path?: string | undefined;
367
375
  }, {}>;
368
376
  export declare const BlockStreamMsg: import("arktype/internal/variants/object.ts").ObjectType<{
369
377
  blockId: string;
@@ -459,6 +467,7 @@ export declare const BlockStreamMsg: import("arktype/internal/variants/object.ts
459
467
  type: "block.code.begin";
460
468
  sectionId: string;
461
469
  lang: string;
470
+ path?: string | undefined;
462
471
  } | {
463
472
  blockId: string;
464
473
  streamId: string;
@@ -470,6 +479,7 @@ export declare const BlockStreamMsg: import("arktype/internal/variants/object.ts
470
479
  type: "block.code.line";
471
480
  sectionId: string;
472
481
  lang: string;
482
+ path?: string | undefined;
473
483
  } | {
474
484
  blockId: string;
475
485
  streamId: string;
@@ -484,6 +494,7 @@ export declare const BlockStreamMsg: import("arktype/internal/variants/object.ts
484
494
  type: "block.code.end";
485
495
  sectionId: string;
486
496
  lang: string;
497
+ path?: string | undefined;
487
498
  } | {
488
499
  blockId: string;
489
500
  streamId: string;
@@ -627,6 +638,7 @@ export declare const BlockMsgs: import("arktype/internal/variants/object.ts").Ob
627
638
  type: "block.code.begin";
628
639
  sectionId: string;
629
640
  lang: string;
641
+ path?: string | undefined;
630
642
  } | {
631
643
  blockId: string;
632
644
  streamId: string;
@@ -638,6 +650,7 @@ export declare const BlockMsgs: import("arktype/internal/variants/object.ts").Ob
638
650
  type: "block.code.line";
639
651
  sectionId: string;
640
652
  lang: string;
653
+ path?: string | undefined;
641
654
  } | {
642
655
  blockId: string;
643
656
  streamId: string;
@@ -652,6 +665,7 @@ export declare const BlockMsgs: import("arktype/internal/variants/object.ts").Ob
652
665
  type: "block.code.end";
653
666
  sectionId: string;
654
667
  lang: string;
668
+ path?: string | undefined;
655
669
  } | {
656
670
  blockId: string;
657
671
  streamId: string;
package/block-stream.js CHANGED
@@ -87,11 +87,13 @@ export const CodeBeginMsg = type({
87
87
  type: "'block.code.begin'",
88
88
  sectionId: "string",
89
89
  lang: "string",
90
+ "path?": "string",
90
91
  }).and(BlockBase);
91
92
  export const CodeLineMsg = type({
92
93
  type: "'block.code.line'",
93
94
  sectionId: "string",
94
95
  lang: "string",
96
+ "path?": "string",
95
97
  })
96
98
  .and(BlockBase)
97
99
  .and(BlockLine);
@@ -99,6 +101,7 @@ export const CodeEndMsg = type({
99
101
  type: "'block.code.end'",
100
102
  sectionId: "string",
101
103
  lang: "string",
104
+ "path?": "string",
102
105
  })
103
106
  .and(BlockBase)
104
107
  .and(BlockStatsBox);
@@ -138,6 +141,8 @@ export const isBlockImage = (msg, streamId) => !(BlockImageMsg(msg) instanceof t
138
141
  export const isBlockStreamMsg = (msg, streamId) => !(BlockStreamMsg(msg) instanceof type.errors) && (!streamId || msg.streamId === streamId);
139
142
  const CODE_FENCE_START = /^```(\w*)$/;
140
143
  const CODE_FENCE_END = /^```$/;
144
+ const PATH_LINE = /^[\w\-./]+\.(?:jsx?|tsx?|mjs|cjs|md|json|html|css)$/;
145
+ const DEFAULT_PATH = "App.jsx";
141
146
  function addStat(target, source) {
142
147
  target.lines += source.lines;
143
148
  target.bytes += source.bytes;
@@ -153,6 +158,8 @@ export function createBlockStream(streamId, innerStreamId, createId) {
153
158
  let sectionStarted = false;
154
159
  let currentLang = "";
155
160
  let currentSectionId = "";
161
+ let currentPath = DEFAULT_PATH;
162
+ let lastToplevelLine = "";
156
163
  const toplevelStat = { lines: 0, bytes: 0, cnt: 0 };
157
164
  const codeStat = { lines: 0, bytes: 0, cnt: 0 };
158
165
  const imageStat = { lines: 0, bytes: 0, cnt: 0 };
@@ -246,6 +253,9 @@ export function createBlockStream(streamId, innerStreamId, createId) {
246
253
  }
247
254
  mode = "code";
248
255
  currentLang = fenceStartMatch[1] || "";
256
+ const trimmedPath = lastToplevelLine.trim();
257
+ currentPath = PATH_LINE.test(trimmedPath) ? trimmedPath : DEFAULT_PATH;
258
+ lastToplevelLine = "";
249
259
  sectionStarted = true;
250
260
  currentSectionId = createId();
251
261
  blockStat = { lines: 0, bytes: 0, cnt: 0 };
@@ -253,6 +263,7 @@ export function createBlockStream(streamId, innerStreamId, createId) {
253
263
  controller.enqueue({
254
264
  type: "block.code.begin",
255
265
  lang: currentLang.toLowerCase(),
266
+ path: currentPath,
256
267
  timestamp: new Date(),
257
268
  sectionId: currentSectionId,
258
269
  blockId,
@@ -277,6 +288,8 @@ export function createBlockStream(streamId, innerStreamId, createId) {
277
288
  });
278
289
  }
279
290
  blockStat.bytes += content.length;
291
+ if (content.trim().length > 0)
292
+ lastToplevelLine = content;
280
293
  controller.enqueue({
281
294
  type: "block.toplevel.line",
282
295
  timestamp: new Date(),
@@ -306,6 +319,7 @@ export function createBlockStream(streamId, innerStreamId, createId) {
306
319
  seq: seq++,
307
320
  blockNr: blockNr++,
308
321
  lang: currentLang.toLowerCase(),
322
+ path: currentPath,
309
323
  stats: blockStat,
310
324
  });
311
325
  mode = "toplevel";
@@ -316,6 +330,7 @@ export function createBlockStream(streamId, innerStreamId, createId) {
316
330
  controller.enqueue({
317
331
  type: "block.code.line",
318
332
  lang: currentLang.toLowerCase(),
333
+ path: currentPath,
319
334
  timestamp: new Date(),
320
335
  sectionId: currentSectionId,
321
336
  lineNr: blockStat.lines++,
@@ -359,6 +374,7 @@ export function createBlockStream(streamId, innerStreamId, createId) {
359
374
  blockId,
360
375
  streamId,
361
376
  lang: currentLang.toLowerCase(),
377
+ path: currentPath,
362
378
  sectionId: currentSectionId,
363
379
  seq: seq++,
364
380
  blockNr: blockNr++,