Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/cli/src/repl/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ import type { ReplHistoryNavigation } from "./navigation.ts";
import { blankBand, EMPTY_BAND } from "./history-rail.ts";
import type { ReplPreparedRail } from "./history-rail.ts";
import { entryReading, NO_READING } from "./source-reading.ts";
import { inertCommandText } from "./command-output.ts";
import type { ReplCommandOutput } from "./command-output.ts";
import type { ReplReading } from "./source-reading.ts";
import { readingRuns, fitPlain } from "./fitting.ts";
import type { ReplPlainFitStore } from "./fitting.ts";
Expand Down Expand Up @@ -185,6 +187,7 @@ export type { ReplAction };

/** What this process holds that the Journal does not. */
export interface ReplLive {
readonly commandOutput?: readonly ReplCommandOutput[];
/** Output no recorded outcome has replaced yet. */
readonly output: string;
/** The question waiting right now, or none. */
Expand Down Expand Up @@ -6147,6 +6150,7 @@ export function readingOf(view: ReplView): ReplReading {
entry,
lifecycle: view.live.lifecycle,
live: livesHere(view.model, view.selection) ? view.live.output : "",
commandOutput: view.live.commandOutput?.find((one) => one.entry === entry.key)?.chunks ?? [],
inspected: view.state.route.at !== undefined,
});
}
Expand Down Expand Up @@ -10975,7 +10979,7 @@ function workComposed(staging: ReplChatStaging, key: string): readonly ReplChatC
}),
);
// The record's own final line break ends its last line rather than adding one.
const recorded = entry.terminal?.output ?? "";
const recorded = inertCommandText(entry.terminal?.output ?? "");
const output = recorded.endsWith("\n") ? recorded.slice(0, -1) : recorded;
if (output !== "") {
found.push(
Expand Down
16 changes: 16 additions & 0 deletions packages/cli/src/repl/command-output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export interface ReplCommandChunk {
readonly channel: "stdout" | "stderr";
readonly text: string;
}

export interface ReplCommandOutput {
readonly entry: string;
readonly chunks: readonly ReplCommandChunk[];
}

export function inertCommandText(text: string): string {
return text.replace(
/[\x00-\x09\x0b-\x1f\x7f-\x9f]/g,
(control) => `\\x${control.charCodeAt(0).toString(16).padStart(2, "0")}`,
);
}
1 change: 1 addition & 0 deletions packages/cli/src/repl/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2451,6 +2451,7 @@ function liveOf(
): ReplLive {
return {
output: session.overlay.output,
commandOutput: session.commandOutput,
question: session.overlay.question,
expansion: session.expansion.state,
pausable: session.controller !== undefined,
Expand Down
50 changes: 50 additions & 0 deletions packages/cli/src/repl/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ import { INLINE_SOURCE_PATH, inlineSource, validateDocument } from "@executablem
import type { DocumentValidationDiagnostic, Json, PermissionMode } from "@executablemd/core";
import type { GeneratedSelection } from "@executablemd/core/api";
import { API, Permissions } from "@executablemd/runtime";
import { Stdio } from "@effectionx/process";
import type { ReplCommandChunk, ReplCommandOutput } from "./command-output.ts";
import { executeInstalled } from "@executablemd/core/host";
import { useGeneratedFallback } from "../evaluation-profile.ts";
import type {
Expand Down Expand Up @@ -391,6 +393,7 @@ export interface ReplSession {
* {@link ReplPrinting} for why the overlay is not this.
*/
readonly printing: ReplPrinting | undefined;
readonly commandOutput: readonly ReplCommandOutput[];
/**
* What this process is doing element by element, for the entry that is open.
*
Expand Down Expand Up @@ -791,6 +794,7 @@ function* start(

/** Text the entry that is running has printed, which no record holds yet. */
let output = "";
const commandOutput = new Map<string, ReplCommandChunk[]>();
/** Whether an entry's execution is still running in this process. */
let live = false;
/**
Expand Down Expand Up @@ -1014,6 +1018,16 @@ function* start(
// the record that outcome carries.
return live && running !== undefined ? { entry: running.key, output } : undefined;
},
get commandOutput() {
return Object.freeze(
[...commandOutput].map(([entry, chunks]) =>
Object.freeze({
entry,
chunks: Object.freeze([...chunks]),
}),
),
);
},
get lifecycle() {
return lifecycle.reading();
},
Expand Down Expand Up @@ -1780,6 +1794,42 @@ function* start(
workspace?: true,
): Operation<Result<unknown>> {
try {
const chunks: ReplCommandChunk[] = [];
const stdout = new TextDecoder();
const stderr = new TextDecoder();
function append(channel: ReplCommandChunk["channel"], text: string): void {
if (text.length === 0) {
return;
}
const last = chunks[chunks.length - 1];
const chunk = Object.freeze({
channel,
text: last?.channel === channel ? last.text + text : text,
});
if (last?.channel === channel) {
chunks[chunks.length - 1] = chunk;
} else {
chunks.push(chunk);
}
commandOutput.set(key, chunks);
outputs.send(output);
}
// Registered before entry resources so their cleanup can still emit.
yield* ensure(() => {
append("stdout", stdout.decode());
append("stderr", stderr.decode());
});
yield* Stdio.around(
{
*stdout([bytes]) {
append("stdout", stdout.decode(bytes, { stream: true }));
},
*stderr([bytes]) {
append("stderr", stderr.decode(bytes, { stream: true }));
},
},
{ at: "min" },
);
const directory = workspace === true ? options.workspace : options.projectCwd;
if (directory !== undefined) {
yield* API.Env.around(
Expand Down
45 changes: 34 additions & 11 deletions packages/cli/src/repl/source-reading.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ import type { ReplPresentationRole, ReplRowStyle } from "./presentation-style.ts
import { tokenRuns } from "./description.ts";
import type { ReplTokenRun } from "./description.ts";
import { sourceRuns } from "./presentation-text.ts";
import { inertCommandText } from "./command-output.ts";
import type { ReplCommandChunk } from "./command-output.ts";

/** What this terminal draws for each phase, glyph and word together. */
const BADGES: Readonly<
Expand Down Expand Up @@ -117,6 +119,7 @@ export interface ReplReadingSection {
/** One entry's whole reading: what it produced, then what produced it. */
export interface ReplReading {
readonly output: ReplReadingSection;
readonly commands?: readonly ReplReadingSection[];
readonly source: ReplReadingSection;
}

Expand Down Expand Up @@ -146,6 +149,10 @@ export function readingLines(reading: ReplReading): readonly ReplReadingLine[] {
return Object.freeze([
caption("output", reading.output),
...reading.output.lines,
...(reading.commands ?? []).flatMap((section, index) => [
caption(`command:${index}`, section),
...section.lines,
]),
caption("source", reading.source),
...reading.source.lines,
]);
Expand All @@ -165,6 +172,7 @@ function caption(key: string, section: ReplReadingSection): ReplReadingLine {

/** What a reading is being built for. */
export interface ReplReadingRequest {
readonly commandOutput?: readonly ReplCommandChunk[];
readonly entry: ReplEntry;
/** This session's observations, or an empty reading for a retained prefix. */
readonly lifecycle: ReplLifecycleReading;
Expand All @@ -184,6 +192,19 @@ export interface ReplReadingRequest {
export function entryReading(request: ReplReadingRequest): ReplReading {
return Object.freeze({
output: outputSection(request),
commands: Object.freeze(
(request.commandOutput ?? []).map((chunk, index) =>
Object.freeze({
caption: `Command ${chunk.channel}`,
captionStyle: styleOf("pane-heading", { inspected: request.inspected }),
lines: plain(
chunk.text,
`command:${index}`,
styleOf("output", { inspected: request.inspected }),
),
}),
),
),
source: sourceSection(request),
});
}
Expand Down Expand Up @@ -257,17 +278,19 @@ function outputSection(request: ReplReadingRequest): ReplReadingSection {
/** Text as lines nothing has been observed in. */
function plain(text: string, prefix: string, style: ReplRowStyle): readonly ReplReadingLine[] {
return Object.freeze(
text.split("\n").map((one, index) =>
Object.freeze({
key: `${prefix}:${index}`,
text: one,
runs: tokenRuns([{ text: one, token: style.role }]),
rail: "rail-pending" as const,
depth: 0,
badge: undefined,
style,
}),
),
inertCommandText(text)
.split("\n")
.map((one, index) =>
Object.freeze({
key: `${prefix}:${index}`,
text: one,
runs: tokenRuns([{ text: one, token: style.role }]),
rail: "rail-pending" as const,
depth: 0,
badge: undefined,
style,
}),
),
);
}

Expand Down
Loading
Loading