Skip to content

Commit d78a91f

Browse files
authored
Merge pull request #952 from taras/agent/repl-slice-942-20261019
✨ Show a window of rendered text with <Slice> (#942)
2 parents 726275b + 73d3c2a commit d78a91f

8 files changed

Lines changed: 774 additions & 0 deletions

File tree

‎architecture.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8456,6 +8456,7 @@ Status is measured against main.
84568456
| `<CodeBlock value={…} />` | Renders a supplied string inside a safe fence, preserving its exact value through return and capture. [Forms and validation](specs/executable-mdx-spec.md#619-showing-text-as-a-code-block-codeblock). | built on the #658 stack |
84578457
| `<Verbose>…</Verbose>` | Renders content only under a true lexical verbosity value; CLI diagnostics remain host presentation. [Verbosity](specs/executable-mdx-spec.md#620-verbose-only-content-verbose). | built on this stack |
84588458
| `<Json value={…} />` | Renders a captured value through native two-space JSON serialization without changing its operand. [Validation, failures and exact capture](specs/executable-mdx-spec.md#612-json-in-both-directions-json-parse-and-safeparse). | built on the #452 stack, with ordinary text capture enabled by #666 |
8459+
| `<Slice offset length>` | Returns a Unicode code-point window of its content's completed rendering, labelled when partial. It calls `content()` once and waits for the whole string for every offset, so it bounds rendered text rather than the producer's work or memory; child captures, enclosed order, nesting as ordinary text, failure and cancellation stay ordinary. [Forms, window arithmetic, the note and exact capture](specs/executable-mdx-spec.md#621-showing-a-text-window-slice). | built on this stack |
84598460
| root failure settlement | a text root installs the fail-capable `output` mode for its whole body, so an uncaught, undecided failure is the document execution's own outcome, whether or not the root declares `<Output>` [Invariant](#3-error-mode-is-lexical). | built on the #453 stack |
84608461
| `<Output>` region `output` mode | an undecided error fails the document execution [Invariant](#3-error-mode-is-lexical). | built on main |
84618462
| `<Output>` rendering selection | chooses which regions of a body render, and buffers a root that declares one; it decides nothing about failure [Interface and evidence](specs/executable-mdx-spec.md#69-component-declared-output-output). | built on main |

‎packages/cli/tests/repl-sidekick-controls.test.ts‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1711,6 +1711,63 @@ describe("Tier R3 — a reply reads the history, not the screen", () => {
17111711
expect(missing.ok ? "" : missing.error.message).toContain('no entry "entry-9"');
17121712
});
17131713

1714+
/**
1715+
* One entry that renders a window of a captured value's JSON.
1716+
*
1717+
* `record` stays the complete structured value the document bound; `excerpt`
1718+
* is text taken from its rendering, and the `<Output>` region is what puts
1719+
* that text in the entry's own output. The emoji is written as an escape so
1720+
* the source in this file and the source the entry runs are the same bytes.
1721+
*/
1722+
const SLICED_JSON = [
1723+
'<Let as="record" value={{ message: "A\u{1F600}BCDEF" }} />',
1724+
'<Slice offset={2} length={8} as="excerpt"><Json value={record} /></Slice>',
1725+
"<Output>{excerpt}</Output>",
1726+
"",
1727+
].join("\n");
1728+
1729+
it("S8: a reply reads a sliced entry's output and its complete root data", function* () {
1730+
const session = yield* opened("sliced", new InMemoryStream());
1731+
yield* ran(session, SLICED_JSON);
1732+
1733+
// The window the entry took, spelled here rather than derived: the JSON of
1734+
// `{ message: "A😀BCDEF" }` is twenty-six code points, and points two
1735+
// through nine of it are the two spaces and the first six of `"message"`.
1736+
const NOTE = "Text excerpt: code points [2, 10) of 26 (zero-based, end exclusive).";
1737+
const EXCERPT = `${NOTE}\n "messa`;
1738+
1739+
const asked = fresh();
1740+
const answer = yield* replyReading(
1741+
'<Output>\n<REPL.EntryOutput entry="entry-1" />\n<REPL.Bindings />\n</Output>\n',
1742+
session,
1743+
asked,
1744+
);
1745+
1746+
const text = rendered(answer);
1747+
// What the entry put on the screen, read back through the existing control:
1748+
// the labelled excerpt, not the whole JSON it was taken from.
1749+
expect(text).toContain(NOTE);
1750+
expect(text).toContain(' "messa');
1751+
expect(text).not.toContain('A\u{1F600}BCDEF"\n}');
1752+
1753+
// The root data the entry handed back: the producer's value complete, and
1754+
// the window exactly as it was captured, note included.
1755+
const bindings = yield* sidekickRequests(reading(session), noSubmission).bindings();
1756+
if (!bindings.ok) {
1757+
throw bindings.error;
1758+
}
1759+
expect(bindings.value).toEqual({
1760+
record: { message: "A\u{1F600}BCDEF" },
1761+
excerpt: EXCERPT,
1762+
});
1763+
1764+
// Both reads succeeded, and the journal holds exactly the two explicit
1765+
// reads the reply wrote — no feedback the slice produced on its own.
1766+
expect(asked.settled.map((one) => one.ok)).toEqual([true, true]);
1767+
expect(asked.began.map((one) => one.component)).toEqual(["REPL.EntryOutput", "REPL.Bindings"]);
1768+
expect(asked.requests).toEqual([]);
1769+
});
1770+
17141771
it("R3: the vocabulary a reply is told about is the entry's, not its own", function* () {
17151772
const session = yield* opened("vocabulary", new InMemoryStream());
17161773
const requested = sidekickRequests(reading(session), noSubmission);
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* `<Slice>` — show a window of completed rendered text
3+
* (specs/executable-mdx-spec.md §6.21).
4+
*
5+
* The content renders once, in full, and the window is taken from the string it
6+
* produced. Reaching the requested number of characters is not a reason to stop
7+
* the work behind them: this bounds what a reader is shown rather than what a
8+
* producer does, so a log the document asked for is still written whole and a
9+
* command it ran still finishes.
10+
*
11+
* Positions count Unicode code points rather than UTF-16 code units or bytes,
12+
* so an astral character is one position and a window can never land between
13+
* the two halves of one.
14+
*
15+
* A partial window carries a note saying which interval of which total it is,
16+
* because an excerpt that did not say so could not be told from the whole of a
17+
* shorter text — and an empty window could not be told from work that produced
18+
* nothing at all. The note is added after the selection and sits outside it, so
19+
* an outer `<Slice>` reads it as the ordinary text it is and may cut it.
20+
*/
21+
22+
import type { Operation } from "effection";
23+
import { content } from "../component-api.ts";
24+
import type { Json } from "../types.ts";
25+
26+
export const props = {
27+
type: "object",
28+
properties: {
29+
offset: { type: "integer", default: 0 },
30+
length: { type: "integer", minimum: 1, default: 4096 },
31+
},
32+
additionalProperties: false,
33+
};
34+
35+
/** What a partial window says about itself, in the document's own text. */
36+
function note(start: number, end: number, total: number): string {
37+
return `Text excerpt: code points [${start}, ${end}) of ${total} (zero-based, end exclusive).`;
38+
}
39+
40+
export default function* Slice(props: Record<string, Json>): Operation<string> {
41+
const offset = Number(props.offset);
42+
const length = Number(props.length);
43+
44+
// Once, and completely. A negative offset is a position from the end, so the
45+
// total has to be known before any window can be resolved — and waiting for
46+
// the same completion when the offset is positive is what keeps one component
47+
// from answering two different questions depending on how it was written.
48+
const body = yield* content();
49+
50+
const points = Array.from(body);
51+
const total = points.length;
52+
const from = offset < 0 ? total + offset : offset;
53+
const start = Math.min(Math.max(from, 0), total);
54+
const end = Math.min(total, start + length);
55+
56+
// A window that omits nothing has nothing to report, so the body is handed
57+
// back exactly as it arrived rather than rebuilt out of its code points.
58+
if (start === 0 && end === total) {
59+
return body;
60+
}
61+
return `${note(start, end, total)}\n${points.slice(start, end).join("")}`;
62+
}

‎packages/core/src/components/components.md‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,46 @@ It chooses a fence the value cannot break out of, so a value that itself
305305
contains fences is still shown rather than escaping into the surrounding
306306
document. `as` captures the exact fenced Markdown instead of emitting it.
307307

308+
## Slice
309+
310+
Shows a window of text that something else rendered.
311+
312+
```mdx
313+
<Slice offset={-4096} length={4096}>
314+
<File path="build.log" />
315+
</Slice>
316+
```
317+
318+
Reach for it when what belongs in the document is part of something too long to
319+
read whole — the tail of a build log, the first screen of a diff, the head of a
320+
file you are deciding about. The content renders completely first and the window
321+
is taken from the text it produced, so the work behind it still happens: the log
322+
is still read, a command inside still finishes, and a value another element
323+
captured is still the whole value. This bounds what a reader is shown, not what
324+
a producer does.
325+
326+
Positions count Unicode code points, so an emoji is one position and a window
327+
never lands inside one. `offset` counts from the start, or from the end when it
328+
is negative — `offset={-4096}` is the last four thousand and ninety-six points.
329+
A position before the beginning starts at the beginning, and one at or past the
330+
end selects nothing. `length` is how many points to take, and has no maximum.
331+
332+
A window that leaves something out says so, on a line of its own before the
333+
text. For `<Slice offset={1} length={3}>A😀BCDEF</Slice>`:
334+
335+
```text
336+
Text excerpt: code points [1, 4) of 7 (zero-based, end exclusive).
337+
😀BC
338+
```
339+
340+
That line is what tells a short excerpt from a short input, and an empty window
341+
from work that produced nothing at all. A window covering the whole input
342+
carries no line and returns the text exactly as it arrived — nothing is trimmed
343+
or normalized on either path.
344+
345+
The note is ordinary text written outside the window it describes, so a `<Slice>`
346+
around another one counts the inner note and may cut through it.
347+
308348
## Json
309349

310350
Renders a value as JSON text.

‎packages/core/src/components/registry.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import Glob, {
3939
import Json, { props as jsonProps } from "./Json.ts";
4040
import Parse, { props as parseProps, returns as parseReturns } from "./Parse.ts";
4141
import SafeParse, { props as safeParseProps, returns as safeParseReturns } from "./SafeParse.ts";
42+
import Slice, { props as sliceProps } from "./Slice.ts";
4243
import Test, { props as testProps } from "./Test.ts";
4344
import { parseJsonObject } from "../json.ts";
4445
import { declaredForms, formDispatcher } from "../invocation-identity.ts";
@@ -241,6 +242,14 @@ export const CORE_REGISTRY: ComponentRegistry = new Map<string, RegistryEntry>([
241242
as: "Optional. Captures the exact fenced Markdown instead of emitting it.",
242243
context: null,
243244
}),
245+
core("Slice", Slice, parseJsonObject(sliceProps), {
246+
description:
247+
"Show a window of rendered text. " +
248+
"`<Slice offset={-4096} length={4096}>…</Slice>` counts Unicode code points and labels " +
249+
"partial excerpts.",
250+
as: "Optional. Captures the resulting text, including any range note, instead of emitting it.",
251+
context: "The content whose completed rendering the window is taken from.",
252+
}),
244253
core(
245254
"Json",
246255
Json,

‎packages/core/tests/component-registration.test.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,7 @@ describe("Tier CR — resolution order", () => {
377377
"Json",
378378
"Parse",
379379
"SafeParse",
380+
"Slice",
380381
"TempDir",
381382
]) {
382383
yield* writeTextFile(join(dir, `${name}.md`), "mine\n");
@@ -408,6 +409,7 @@ describe("Tier CR — resolution order", () => {
408409
"Json",
409410
"Parse",
410411
"SafeParse",
412+
"Slice",
411413
"TempDir",
412414
]) {
413415
const selection = yield* select(name, [dir]);

0 commit comments

Comments
 (0)