feat(coding-agent): removed unused eval helper functions
- Removed deprecated eval prelude helpers `append`, `tree`, `diff`, `sort`, `uniq`, and `counter` from all supported runtimes. - Cleaned up runtime implementations, protocol definitions, and UI rendering logic associated with the removed helpers. - Updated project documentation, prompts, and test suites to reflect the reduced helper API surface. - Recorded functional changes in the package changelog.
This commit is contained in:
+4
-6
@@ -129,16 +129,14 @@ Implemented in `packages/coding-agent/src/eval/js/worker-core.ts`, `packages/cod
|
||||
- Module cache is busted for **local** imports between cells so edits to source files are picked up without restarting the runtime. `__omp_import__` deletes `require.cache[absPath]` before re-importing whenever the original specifier is a filesystem path: relative (`./x`, `../x`, `.`, `..`), POSIX-absolute (`/...`), home-prefixed (`~/...`), or Windows drive-letter (`C:\...` / `C:/...`). Bare specifiers (`react`, `lodash/x`) and URL/scheme specifiers (`node:fs`, `file://...`, `https://...`) are left in cache so package identity stays stable across cells. The cache-bust only fires when the resolved target is an absolute path — unresolved bare-package fallbacks (`resolveImportSpecifier()` returning the original specifier) skip it.
|
||||
- The prelude installs globals:
|
||||
- `display`, `print`, and a `console` bridge
|
||||
- `read`, `write`, `append`, `sort`, `uniq`, `counter`, `diff`, `tree`, `env`, `output`
|
||||
- `read`, `write`, `env`, `output`
|
||||
- `tool.<name>(args)` proxy for arbitrary session tool calls
|
||||
- `completion(prompt, opts?)` for oneshot, stateless model calls (see _Oneshot completion helper_ below)
|
||||
- `agent(prompt, opts?)` for a single subagent call, plus `parallel()` / `pipeline()` bounded-pool helpers (see _Subagent helper_ below)
|
||||
- `log(message)`, `phase(title)`, and `budget` (live token-budget view via async `budget.total()` / `budget.spent()` / `budget.remaining()` / `budget.hard()`)
|
||||
- JS helpers that touch the host/runtime boundary are async and `await`able; pure text helpers (`sort`, `uniq`, `counter`) return synchronously but may still be safely awaited.
|
||||
- JS host/runtime helpers (`read`, `write`, `output`) are async and `await`able; `env` returns synchronously.
|
||||
- JS helper options may be passed either positionally in the Python order or as a trailing options object. `null` and `undefined` skip positional slots:
|
||||
- `await read(path, offset?, limit?)` or `await read(path, { offset?, limit? })`
|
||||
- `await tree(path = ".", maxDepth?, showHidden?)` or `await tree(path, { maxDepth?, showHidden? })`
|
||||
- `sort(text, reverse?, unique?)`, `uniq(text, count?)`, `counter(items, limit?, reverse?)`
|
||||
- `await agent(prompt, agent?, model?, label?, schema?)` or `await agent(prompt, { agent?, model?, label?, schema?, handle? })`
|
||||
- `await parallel([() => agent("a"), () => agent("b")])`
|
||||
- `await pipeline(items, stage1, stage2)`
|
||||
@@ -215,7 +213,7 @@ A single tool call can mix Python and JS cells. Persistence is per language runt
|
||||
## Side Effects
|
||||
|
||||
- Filesystem
|
||||
- JS/Python prelude helpers can read, write, append, diff, and traverse filesystem paths under the session cwd or absolute paths.
|
||||
- JS/Python prelude helpers can read and write filesystem paths under the session cwd or absolute paths.
|
||||
- JS helper `read()` auto-delegates any non-`local://` scheme URI (`agent://`, `artifact://`, `https://`, ...) to `tool.read(...)` (honoring an `offset`/`limit` line selector), resolves `local://` under its mapped root, reads plain/absolute filesystem paths directly, and rejects directory paths.
|
||||
- Output may spill to an artifact file via `OutputSink`.
|
||||
- Network
|
||||
@@ -280,7 +278,7 @@ A single tool call can mix Python and JS cells. Persistence is per language runt
|
||||
- Backend selection is strictly explicit per cell: `language` must be `"py"` or `"js"`. The previous `*** Cell` header parser, the `eval.lark` constrained grammar, and the sniffer-based fallback have all been removed.
|
||||
- `EvalTool.customFormat` no longer exists. Tool calls flow through the standard JSON schema; there is no Lark-constrained sampling path.
|
||||
- `tool.<name>()` exists in both JS and Python. Python calls route through a per-run loopback bridge keyed by the current cell id.
|
||||
- `read()` delegates non-`local://` scheme URIs to `tool.read`, resolves `local://` under its injected root, and resolves plain paths against the session cwd or an absolute filesystem path; `resolveRegularFile()` rejects directory paths. `write()`/`append()` accept `local://` and plain paths but reject any other `scheme://` via `resolveHelperPath()` (`Protocol paths are not supported by write()`).
|
||||
- `read()` delegates non-`local://` scheme URIs to `tool.read`, resolves `local://` under its injected root, and resolves plain paths against the session cwd or an absolute filesystem path; `resolveRegularFile()` rejects directory paths. `write()` accepts `local://` and plain paths but rejects any other `scheme://` via `resolveHelperPath()` (`Protocol paths are not supported by write()`).
|
||||
- Python helper `output(...)` depends on `PI_ARTIFACTS_DIR` or `PI_SESSION_FILE`; it fails outside a session-backed run.
|
||||
- `display()` can produce text and structured outputs from the same value; the renderer prefers markdown over `text/plain` when both exist.
|
||||
- JS static imports are rewritten only at top level. Nested imports stay invalid and surface normal JS syntax/runtime errors.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
- Renamed the eval `agent()` helper parameters `agent_type` → `agent` and `return_handle` → `handle` across every workflow runtime (Python, JavaScript, Ruby, Julia), so the names are identical in every language (no camelCase/snake_case split) and the agent-selection parameter matches the `task` tool's `agent`. The `__agent__` eval bridge wire protocol was renamed to match.
|
||||
@@ -20,6 +19,13 @@
|
||||
- Reinforced routing of fragile, multi-step shell logic to the `eval` tool over `bash`. The system-prompt tool policy, `bash.md`, and `eval.md` now treat loops, conditionals, heredocs, inline `-e`/`-c` scripts, multi-stage pipelines, and quote/JSON escaping as the signal to write an `eval` cell; bash's "compute a fact" carveout is narrowed to single short pipelines, and `eval.md` now actively claims that territory with runtime-templated examples (only enabled backends are advertised).
|
||||
- Made `eval` an essential built-in tool (`loadMode: "essential"`, added to the default essential tool set) so it stays active under `tools.discoveryMode: "all"` instead of being hidden behind `search_tool_bm25`.
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed `append`, `tree`, and `diff` eval helper functions from Python, JavaScript, and Ruby
|
||||
- Removed `sort`, `uniq`, and `counter` text processing eval helpers from Python, JavaScript, and Ruby
|
||||
- Removed the `append(path, content)`, `tree(path, max_depth?, show_hidden?)`, and `diff(a, b)` eval prelude helpers from every workflow runtime (Python, JavaScript, Ruby, Julia), along with their status renderers, icon entries, and tool/`docs` references. Use `write`/`read` for file mutation and `tool.<name>(...)` for richer filesystem operations.
|
||||
- Removed the `sort(text, reverse?, unique?)`, `uniq(text, count?)`, and `counter(items, limit?, reverse?)` eval text helpers from the Python, JavaScript, and Ruby prelude surfaces (Julia never defined them), along with the JS `HelperBundle`/`HelperOptions` members and `docs` references. Sort/dedupe/count inline in cell code instead.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed Escape doing nothing in the Settings text-input fields (e.g. "Python Interpreter") on terminals with the kitty keyboard protocol active (ghostty/kitty). Inside the fullscreen settings overlay the protocol reports Escape as the CSI-u sequence `\x1b[27u`, which the text-input submenu's raw `\x1b` compare missed; `handleInputOrEscape` now decodes Escape via `matchesKey`, matching every other Escape-to-cancel path.
|
||||
|
||||
@@ -4,7 +4,7 @@ import { TempDir } from "@oh-my-pi/pi-utils/temp";
|
||||
import { createHelpers, type HelperContext } from "../js/shared/helpers";
|
||||
|
||||
/**
|
||||
* The eval helpers (`read`/`write`/`append`) must substitute injected on-disk
|
||||
* The eval helpers (`read`/`write`) must substitute injected on-disk
|
||||
* roots for internal-URL schemes. Without it, `write("local://x.md")` hits a
|
||||
* stdlib `path.resolve` that collapses `local://` to `local:/`, creating a junk
|
||||
* `local:` directory under the cwd instead of landing where `read local://x.md`
|
||||
@@ -20,7 +20,7 @@ function makeCtx(cwd: string, roots: Record<string, string>): HelperContext {
|
||||
}
|
||||
|
||||
describe("eval js helpers internal-url resolution", () => {
|
||||
it("writes, reads, and appends local:// under the injected root", async () => {
|
||||
it("writes and reads local:// under the injected root", async () => {
|
||||
using tmp = TempDir.createSync("@eval-helpers-local-");
|
||||
const root = path.join(tmp.path(), "local");
|
||||
const helpers = createHelpers(makeCtx(tmp.path(), { local: root }));
|
||||
@@ -30,9 +30,6 @@ describe("eval js helpers internal-url resolution", () => {
|
||||
expect(await Bun.file(written).text()).toBe("hello");
|
||||
expect(await helpers.read("local://notes/merge-map.md")).toBe("hello");
|
||||
|
||||
await helpers.append("local://notes/merge-map.md", " world");
|
||||
expect(await helpers.read("local://notes/merge-map.md")).toBe("hello world");
|
||||
|
||||
// Regression: no literal `local:` directory created under the cwd.
|
||||
expect(await Bun.file(path.join(tmp.path(), "local:")).exists()).toBe(false);
|
||||
expect(await Bun.file(path.join(tmp.path(), "local:", "notes", "merge-map.md")).exists()).toBe(false);
|
||||
|
||||
@@ -11,35 +11,6 @@ describe.skipIf(!HAS_JULIA)("eval Julia prelude helpers", () => {
|
||||
await disposeJuliaKernelSessionsByOwner(OWNER_ID);
|
||||
});
|
||||
|
||||
it("supports tree keyword options and unified diff", async () => {
|
||||
using tempDir = TempDir.createSync("@omp-eval-julia-helpers-");
|
||||
await Bun.write(path.join(tempDir.path(), "a.txt"), "same\nold\n");
|
||||
await Bun.write(path.join(tempDir.path(), "b.txt"), "same\nnew\n");
|
||||
await Bun.write(path.join(tempDir.path(), "dir", "child.txt"), "child");
|
||||
|
||||
const result = await executeJulia(
|
||||
`
|
||||
d = diff("a.txt", "b.txt")
|
||||
println("DIFF_DELETE=", occursin("-old", d))
|
||||
println("DIFF_ADD=", occursin("+new", d))
|
||||
t = tree(".", max_depth=2)
|
||||
println("TREE_CHILD=", occursin("child.txt", t))
|
||||
nothing
|
||||
`,
|
||||
{
|
||||
cwd: tempDir.path(),
|
||||
sessionId: `julia-prelude-diff:${crypto.randomUUID()}`,
|
||||
kernelOwnerId: OWNER_ID,
|
||||
reset: true,
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.output).toContain("DIFF_DELETE=true");
|
||||
expect(result.output).toContain("DIFF_ADD=true");
|
||||
expect(result.output).toContain("TREE_CHILD=true");
|
||||
}, 30_000);
|
||||
|
||||
it("supports output ranges, JSON queries, metadata, and ANSI stripping", async () => {
|
||||
using tempDir = TempDir.createSync("@omp-eval-julia-output-");
|
||||
const artifactsDir = path.join(tempDir.path(), "session-artifacts");
|
||||
|
||||
@@ -146,221 +146,6 @@ function Base.write(path::AbstractString, content::Any)
|
||||
return resolved
|
||||
end
|
||||
|
||||
function append(path, content)
|
||||
resolved = __omp_resolve_path(string(path))
|
||||
mkpath(dirname(resolved))
|
||||
open(resolved, "a") do f
|
||||
Base.write(f, string(content))
|
||||
end
|
||||
|
||||
Main.emit_frame(Dict(
|
||||
"type" => "display",
|
||||
"id" => Main.current_rid,
|
||||
"bundle" => Dict(
|
||||
"application/x-omp-status" => Dict(
|
||||
"op" => "append",
|
||||
"path" => resolved,
|
||||
"chars" => length(string(content))
|
||||
)
|
||||
)
|
||||
))
|
||||
return resolved
|
||||
end
|
||||
|
||||
function tree(path=".", positional_max_depth=3, positional_show_hidden=false; max_depth=positional_max_depth, show_hidden=positional_show_hidden)
|
||||
base = string(path)
|
||||
resolved = __omp_resolve_path(base)
|
||||
lines = String[]
|
||||
|
||||
function walk(dir, prefix, depth)
|
||||
if depth > max_depth
|
||||
return
|
||||
end
|
||||
entries = try
|
||||
readdir(dir)
|
||||
catch
|
||||
String[]
|
||||
end
|
||||
if !show_hidden
|
||||
entries = filter(e -> !startswith(e, '.'), entries)
|
||||
end
|
||||
sort!(entries, by = e -> (ispath(joinpath(dir, e)) && isdir(joinpath(dir, e)) ? 0 : 1, lowercase(e)))
|
||||
|
||||
for (i, name) in enumerate(entries)
|
||||
full = joinpath(dir, name)
|
||||
is_last = i == length(entries)
|
||||
is_dir = isdir(full)
|
||||
push!(lines, "$(prefix)$(is_last ? "└── " : "├── ")$(name)$(is_dir ? "/" : "")")
|
||||
if is_dir
|
||||
walk(full, prefix * (is_last ? " " : "│ "), depth + 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
walk(resolved, "", 1)
|
||||
out = join(lines, '\n')
|
||||
|
||||
Main.emit_frame(Dict(
|
||||
"type" => "display",
|
||||
"id" => Main.current_rid,
|
||||
"bundle" => Dict(
|
||||
"application/x-omp-status" => Dict(
|
||||
"op" => "tree",
|
||||
"path" => resolved,
|
||||
"lines" => length(lines)
|
||||
)
|
||||
)
|
||||
))
|
||||
return out
|
||||
end
|
||||
|
||||
function __omp_lines_keepends(content::String)
|
||||
parts = split(content, '\n'; keepempty=true)
|
||||
if length(parts) == 1 && isempty(parts[1])
|
||||
return String[]
|
||||
end
|
||||
lines = String[]
|
||||
for i in eachindex(parts)
|
||||
if i < length(parts)
|
||||
push!(lines, string(parts[i], "\n"))
|
||||
elseif !isempty(parts[i])
|
||||
push!(lines, string(parts[i]))
|
||||
end
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
function __omp_diff_ops(a::Vector{String}, b::Vector{String})
|
||||
n = length(a)
|
||||
m = length(b)
|
||||
ops = Vector{Tuple{Symbol, Int, Int}}()
|
||||
if n * m > 4_000_000
|
||||
for i in 1:n
|
||||
push!(ops, (:delete, i, 1))
|
||||
end
|
||||
for j in 1:m
|
||||
push!(ops, (:insert, n + 1, j))
|
||||
end
|
||||
return ops
|
||||
end
|
||||
|
||||
dp = [zeros(Int, m + 1) for _ in 1:(n + 1)]
|
||||
for i in n:-1:1
|
||||
for j in m:-1:1
|
||||
dp[i][j] = a[i] == b[j] ? dp[i + 1][j + 1] + 1 : max(dp[i + 1][j], dp[i][j + 1])
|
||||
end
|
||||
end
|
||||
|
||||
i = 1
|
||||
j = 1
|
||||
while i <= n && j <= m
|
||||
if a[i] == b[j]
|
||||
push!(ops, (:equal, i, j))
|
||||
i += 1
|
||||
j += 1
|
||||
elseif dp[i + 1][j] >= dp[i][j + 1]
|
||||
push!(ops, (:delete, i, j))
|
||||
i += 1
|
||||
else
|
||||
push!(ops, (:insert, i, j))
|
||||
j += 1
|
||||
end
|
||||
end
|
||||
while i <= n
|
||||
push!(ops, (:delete, i, j))
|
||||
i += 1
|
||||
end
|
||||
while j <= m
|
||||
push!(ops, (:insert, i, j))
|
||||
j += 1
|
||||
end
|
||||
return ops
|
||||
end
|
||||
|
||||
function __omp_unified_diff(a::Vector{String}, b::Vector{String}, from_file::String, to_file::String, context::Int=3)
|
||||
ops = __omp_diff_ops(a, b)
|
||||
if !any(op -> op[1] != :equal, ops)
|
||||
return ""
|
||||
end
|
||||
|
||||
entries = [Dict{Symbol, Any}(:tag => tag, :ai => ai, :bi => bi, :text => tag == :insert ? b[bi] : a[ai]) for (tag, ai, bi) in ops]
|
||||
changed = [i for i in eachindex(entries) if entries[i][:tag] != :equal]
|
||||
groups = Vector{Tuple{Int, Int}}()
|
||||
start = nothing
|
||||
prev = nothing
|
||||
for idx in changed
|
||||
if start === nothing
|
||||
start = idx
|
||||
prev = idx
|
||||
elseif idx - prev <= (2 * context) + 1
|
||||
prev = idx
|
||||
else
|
||||
push!(groups, (start, prev))
|
||||
start = idx
|
||||
prev = idx
|
||||
end
|
||||
end
|
||||
if start !== nothing
|
||||
push!(groups, (start, prev))
|
||||
end
|
||||
|
||||
out = IOBuffer()
|
||||
write(out, "--- $from_file\n")
|
||||
write(out, "+++ $to_file\n")
|
||||
for (group_start, group_end) in groups
|
||||
lo = max(group_start - context, 1)
|
||||
hi = min(group_end + context, length(entries))
|
||||
slice = entries[lo:hi]
|
||||
a_start = nothing
|
||||
a_count = 0
|
||||
b_start = nothing
|
||||
b_count = 0
|
||||
for entry in slice
|
||||
if entry[:tag] != :insert
|
||||
if a_start === nothing
|
||||
a_start = entry[:ai]
|
||||
end
|
||||
a_count += 1
|
||||
end
|
||||
if entry[:tag] != :delete
|
||||
if b_start === nothing
|
||||
b_start = entry[:bi]
|
||||
end
|
||||
b_count += 1
|
||||
end
|
||||
end
|
||||
write(out, "@@ -$(a_start === nothing ? 1 : a_start),$a_count +$(b_start === nothing ? 1 : b_start),$b_count @@\n")
|
||||
for entry in slice
|
||||
prefix = entry[:tag] == :equal ? " " : (entry[:tag] == :delete ? "-" : "+")
|
||||
text = string(entry[:text])
|
||||
if !endswith(text, "\n")
|
||||
text *= "\n"
|
||||
end
|
||||
write(out, prefix * text)
|
||||
end
|
||||
end
|
||||
return String(take!(out))
|
||||
end
|
||||
|
||||
function Base.diff(a::AbstractString, b::AbstractString)
|
||||
path_a = __omp_resolve_path(string(a))
|
||||
path_b = __omp_resolve_path(string(b))
|
||||
lines_a = __omp_lines_keepends(open(path_a, "r") do io
|
||||
Base.read(io, String)
|
||||
end)
|
||||
lines_b = __omp_lines_keepends(open(path_b, "r") do io
|
||||
Base.read(io, String)
|
||||
end)
|
||||
out = __omp_unified_diff(lines_a, lines_b, path_a, path_b)
|
||||
__omp_emit_status("diff", Dict{String, Any}(
|
||||
"file_a" => path_a,
|
||||
"file_b" => path_b,
|
||||
"identical" => isempty(out),
|
||||
"preview" => first(out, min(500, length(out)))
|
||||
))
|
||||
return out
|
||||
end
|
||||
|
||||
function __omp_apply_query(data, query)
|
||||
if query === nothing || isempty(string(query))
|
||||
return data
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
|
||||
import * as Diff from "diff";
|
||||
import { ToolError } from "../../../tools/tool-errors";
|
||||
import type { JsStatusEvent } from "./types";
|
||||
|
||||
export interface HelperOptions {
|
||||
path?: string;
|
||||
hidden?: boolean;
|
||||
maxDepth?: number;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
reverse?: boolean;
|
||||
unique?: boolean;
|
||||
count?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,18 +27,12 @@ export interface HelperContext {
|
||||
|
||||
/**
|
||||
* The set of functions exposed to user code via `globalThis.__omp_helpers__`. The JS
|
||||
* prelude reads from this bag and attaches short aliases (`read`, `write`, `tree`, ...)
|
||||
* prelude reads from this bag and attaches short aliases (`read`, `write`, `env`, ...)
|
||||
* onto the global scope.
|
||||
*/
|
||||
export interface HelperBundle {
|
||||
read(rawPath: string, options?: HelperOptions): Promise<string>;
|
||||
writeFile(rawPath: string, data: unknown): Promise<string>;
|
||||
append(rawPath: string, content: string): Promise<string>;
|
||||
sortText(text: string, options?: HelperOptions): string;
|
||||
uniqText(text: string, options?: HelperOptions): string | Array<[number, string]>;
|
||||
counter(items: string | string[], options?: HelperOptions): Array<[number, string]>;
|
||||
diff(rawA: string, rawB: string): Promise<string>;
|
||||
tree(searchPath?: string, options?: HelperOptions): Promise<string>;
|
||||
env(key?: string, value?: string): string | Record<string, string> | undefined;
|
||||
}
|
||||
|
||||
@@ -81,105 +67,6 @@ export function createHelpers(ctx: HelperContext): HelperBundle {
|
||||
ctx.emitStatus({ op: "write", path: filePath, bytes: getDataSize(data) });
|
||||
return filePath;
|
||||
},
|
||||
append: async (rawPath, content) => {
|
||||
const target = resolveHelperPath(ctx, rawPath, "write");
|
||||
// O(1) append; read-all+rewrite both raced concurrent writers and went
|
||||
// quadratic when called in a loop. Bun.write creates parent dirs, so
|
||||
// keep that behavior for the append path too.
|
||||
await fs.promises.mkdir(path.dirname(target), { recursive: true });
|
||||
await fs.promises.appendFile(target, content, "utf-8");
|
||||
ctx.emitStatus({
|
||||
op: "append",
|
||||
path: target,
|
||||
chars: content.length,
|
||||
bytes: utf8Encoder.encode(content).byteLength,
|
||||
});
|
||||
return target;
|
||||
},
|
||||
sortText: (text, options = {}) => {
|
||||
const lines = String(text).split(/\r?\n/);
|
||||
const deduped = options.unique ? Array.from(new Set(lines)) : lines;
|
||||
const sorted = deduped.sort((a, b) => a.localeCompare(b));
|
||||
if (options.reverse) sorted.reverse();
|
||||
const result = sorted.join("\n");
|
||||
ctx.emitStatus({
|
||||
op: "sort",
|
||||
lines: sorted.length,
|
||||
reverse: options.reverse === true,
|
||||
unique: options.unique === true,
|
||||
});
|
||||
return result;
|
||||
},
|
||||
uniqText: (text, options = {}) => {
|
||||
const lines = String(text)
|
||||
.split(/\r?\n/)
|
||||
.filter(line => line.length > 0);
|
||||
const groups: Array<[number, string]> = [];
|
||||
for (const line of lines) {
|
||||
const last = groups.at(-1);
|
||||
if (last && last[1] === line) {
|
||||
last[0] += 1;
|
||||
continue;
|
||||
}
|
||||
groups.push([1, line]);
|
||||
}
|
||||
ctx.emitStatus({ op: "uniq", groups: groups.length, count_mode: options.count === true });
|
||||
if (options.count) return groups;
|
||||
return groups.map(([, line]) => line).join("\n");
|
||||
},
|
||||
counter: (items, options = {}) => {
|
||||
const values = Array.isArray(items) ? items : String(items).split(/\r?\n/).filter(Boolean);
|
||||
const counts = new Map<string, number>();
|
||||
for (const item of values) counts.set(item, (counts.get(item) ?? 0) + 1);
|
||||
const entries = Array.from(counts.entries())
|
||||
.map(([item, count]) => [count, item] as [number, string])
|
||||
.sort((a, b) => (options.reverse === false ? a[0] - b[0] : b[0] - a[0]) || a[1].localeCompare(b[1]));
|
||||
const limited = entries.slice(0, options.limit ?? entries.length);
|
||||
ctx.emitStatus({ op: "counter", unique: counts.size, total: values.length, top: limited.slice(0, 10) });
|
||||
return limited;
|
||||
},
|
||||
diff: async (rawA, rawB) => {
|
||||
const fileA = resolvePath(ctx, rawA);
|
||||
const fileB = resolvePath(ctx, rawB);
|
||||
const [a, b] = await Promise.all([Bun.file(fileA).text(), Bun.file(fileB).text()]);
|
||||
const result = Diff.createTwoFilesPatch(fileA, fileB, a, b, "", "", { context: 3 });
|
||||
ctx.emitStatus({
|
||||
op: "diff",
|
||||
file_a: fileA,
|
||||
file_b: fileB,
|
||||
identical: a === b,
|
||||
preview: result.slice(0, 500),
|
||||
});
|
||||
return result;
|
||||
},
|
||||
tree: async (searchPath = ".", options = {}) => {
|
||||
const root = resolvePath(ctx, searchPath);
|
||||
const maxDepth = options.maxDepth ?? 3;
|
||||
const showHidden = options.hidden ?? false;
|
||||
const lines: string[] = [`${root}/`];
|
||||
let entryCount = 0;
|
||||
const walk = async (dir: string, prefix: string, depth: number): Promise<void> => {
|
||||
if (depth > maxDepth) return;
|
||||
const entries = (await fs.promises.readdir(dir, { withFileTypes: true }))
|
||||
.filter(entry => showHidden || !entry.name.startsWith("."))
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
for (let index = 0; index < entries.length; index++) {
|
||||
const entry = entries[index];
|
||||
const isLast = index === entries.length - 1;
|
||||
const connector = isLast ? "└── " : "├── ";
|
||||
const suffix = entry.isDirectory() ? "/" : "";
|
||||
lines.push(`${prefix}${connector}${entry.name}${suffix}`);
|
||||
entryCount += 1;
|
||||
if (entry.isDirectory()) {
|
||||
await walk(path.join(dir, entry.name), `${prefix}${isLast ? " " : "│ "}`, depth + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
await walk(root, "", 1);
|
||||
const result = lines.join("\n");
|
||||
ctx.emitStatus({ op: "tree", path: root, entries: entryCount, preview: result.slice(0, 1000) });
|
||||
return result;
|
||||
},
|
||||
env: (key, value) => {
|
||||
if (!key) {
|
||||
const merged = Object.fromEntries(Object.entries(getMergedEnv(ctx)).sort(([a], [b]) => a.localeCompare(b)));
|
||||
|
||||
@@ -63,23 +63,6 @@ if (!globalThis.__omp_js_prelude_loaded__) {
|
||||
return callHelper("read", path, options);
|
||||
};
|
||||
const write = async (path, data) => callHelper("writeFile", path, data);
|
||||
const append = (path, content) => callHelper("append", path, content);
|
||||
const sort = (text, opts, ...rest) =>
|
||||
callHelper("sortText", text, optionsArg("sort", opts, rest, ["reverse", "unique"], "{ reverse, unique }"));
|
||||
const uniq = (text, opts, ...rest) => callHelper("uniqText", text, optionsArg("uniq", opts, rest, ["count"], "{ count }"));
|
||||
const counter = (items, opts, ...rest) =>
|
||||
callHelper("counter", items, optionsArg("counter", opts, rest, ["limit", "reverse"], "{ limit, reverse }"));
|
||||
const diff = (a, b) => callHelper("diff", a, b);
|
||||
const tree = (path = ".", opts, ...rest) => {
|
||||
if (isPlainObject(path) && opts === undefined && rest.length === 0) {
|
||||
return callHelper("tree", ".", path);
|
||||
}
|
||||
return callHelper(
|
||||
"tree",
|
||||
isNil(path) ? "." : path,
|
||||
optionsArg("tree", opts, rest, ["maxDepth", "showHidden"], "{ maxDepth, showHidden }"),
|
||||
);
|
||||
};
|
||||
const env = (key, value) => callHelper("env", key, value);
|
||||
|
||||
const tool = new Proxy(
|
||||
@@ -306,11 +289,5 @@ if (!globalThis.__omp_js_prelude_loaded__) {
|
||||
globalThis.__pool = __pool;
|
||||
globalThis.read = read;
|
||||
globalThis.write = write;
|
||||
globalThis.append = append;
|
||||
globalThis.sort = sort;
|
||||
globalThis.uniq = uniq;
|
||||
globalThis.counter = counter;
|
||||
globalThis.diff = diff;
|
||||
globalThis.tree = tree;
|
||||
globalThis.env = env;
|
||||
}
|
||||
|
||||
@@ -72,12 +72,6 @@ const PRELUDE_GLOBAL_KEYS = [
|
||||
"__pool",
|
||||
"read",
|
||||
"write",
|
||||
"append",
|
||||
"sort",
|
||||
"uniq",
|
||||
"counter",
|
||||
"diff",
|
||||
"tree",
|
||||
"env",
|
||||
];
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Structured status payload emitted by helpers (`read`, `write`, `tree`, etc.) and the
|
||||
* Structured status payload emitted by helpers (`read`, `write`, `env`, etc.) and the
|
||||
* tool-call bridge. Surfaces to the model as part of `displays` so it has machine-readable
|
||||
* context about what side effects happened.
|
||||
*/
|
||||
|
||||
@@ -7,7 +7,7 @@ export interface SessionSnapshot {
|
||||
sessionId: string;
|
||||
/**
|
||||
* On-disk roots the helpers substitute for internal-URL schemes
|
||||
* (e.g. `{ local: "/…/artifacts/local" }`). Lets `read`/`write`/`append`
|
||||
* (e.g. `{ local: "/…/artifacts/local" }`). Lets `read`/`write`
|
||||
* accept `local://…` paths instead of writing a literal `local:/` directory.
|
||||
*/
|
||||
localRoots?: Record<string, string>;
|
||||
|
||||
@@ -71,7 +71,7 @@ export interface PythonExecutorOptions {
|
||||
artifactPath?: string;
|
||||
artifactId?: string;
|
||||
/**
|
||||
* On-disk roots the prelude helpers (`read`/`write`/`append`) substitute for
|
||||
* On-disk roots the prelude helpers (`read`/`write`) substitute for
|
||||
* internal-URL schemes (e.g. `{ local: "/…/artifacts/local" }`). Exported to
|
||||
* the kernel as `PI_EVAL_LOCAL_ROOTS` (JSON) so `write("local://x")` lands
|
||||
* where `read local://x` resolves instead of a literal `local:/` directory.
|
||||
|
||||
@@ -115,103 +115,6 @@ if "__omp_prelude_loaded__" not in globals():
|
||||
_emit_status("write", path=str(p), chars=len(content))
|
||||
return p
|
||||
|
||||
def append(path: str | Path, content: str) -> Path:
|
||||
"""Append to file."""
|
||||
p = _resolve_omp_path(path)
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
with p.open("a", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
_emit_status("append", path=str(p), chars=len(content))
|
||||
return p
|
||||
|
||||
def sort(text: str, *, reverse: bool = False, unique: bool = False) -> str:
|
||||
"""Sort lines of text."""
|
||||
lines = text.splitlines()
|
||||
if unique:
|
||||
lines = list(dict.fromkeys(lines))
|
||||
lines = sorted(lines, reverse=reverse)
|
||||
out = "\n".join(lines)
|
||||
_emit_status("sort", lines=len(lines), unique=unique, reverse=reverse)
|
||||
return out
|
||||
|
||||
def uniq(text: str, *, count: bool = False) -> str | list[tuple[int, str]]:
|
||||
"""Remove duplicate adjacent lines (like uniq)."""
|
||||
lines = text.splitlines()
|
||||
if not lines:
|
||||
_emit_status("uniq", groups=0)
|
||||
return [] if count else ""
|
||||
groups: list[tuple[int, str]] = []
|
||||
current = lines[0]
|
||||
current_count = 1
|
||||
for line in lines[1:]:
|
||||
if line == current:
|
||||
current_count += 1
|
||||
continue
|
||||
groups.append((current_count, current))
|
||||
current = line
|
||||
current_count = 1
|
||||
groups.append((current_count, current))
|
||||
_emit_status("uniq", groups=len(groups), count_mode=count)
|
||||
if count:
|
||||
return groups
|
||||
return "\n".join(line for _, line in groups)
|
||||
|
||||
def counter(
|
||||
items: str | list,
|
||||
*,
|
||||
limit: int | None = None,
|
||||
reverse: bool = True,
|
||||
) -> list[tuple[int, str]]:
|
||||
"""Count occurrences and sort by frequency. Like sort | uniq -c | sort -rn.
|
||||
|
||||
items: text (splits into lines) or list of strings
|
||||
reverse: True for descending (most common first), False for ascending
|
||||
Returns: [(count, item), ...] sorted by count
|
||||
"""
|
||||
from collections import Counter
|
||||
if isinstance(items, str):
|
||||
items = items.splitlines()
|
||||
counts = Counter(items)
|
||||
sorted_items = sorted(counts.items(), key=lambda x: (x[1], x[0]), reverse=reverse)
|
||||
if limit is not None:
|
||||
sorted_items = sorted_items[:limit]
|
||||
result = [(count, item) for item, count in sorted_items]
|
||||
_emit_status("counter", unique=len(counts), total=sum(counts.values()), top=result[:10])
|
||||
return result
|
||||
def tree(path: str | Path = ".", *, max_depth: int = 3, show_hidden: bool = False) -> str:
|
||||
"""Return directory tree."""
|
||||
base = Path(path)
|
||||
lines = []
|
||||
def walk(p: Path, prefix: str, depth: int):
|
||||
if depth > max_depth:
|
||||
return
|
||||
items = sorted(p.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
|
||||
items = [i for i in items if show_hidden or not i.name.startswith(".")]
|
||||
for i, item in enumerate(items):
|
||||
is_last = i == len(items) - 1
|
||||
connector = "└── " if is_last else "├── "
|
||||
suffix = "/" if item.is_dir() else ""
|
||||
lines.append(f"{prefix}{connector}{item.name}{suffix}")
|
||||
if item.is_dir():
|
||||
ext = " " if is_last else "│ "
|
||||
walk(item, prefix + ext, depth + 1)
|
||||
lines.append(str(base) + "/")
|
||||
walk(base, "", 1)
|
||||
out = "\n".join(lines)
|
||||
_emit_status("tree", path=str(base), entries=len(lines) - 1, preview=out[:1000])
|
||||
return out
|
||||
|
||||
def diff(a: str | Path, b: str | Path) -> str:
|
||||
"""Compare two files, return unified diff."""
|
||||
import difflib
|
||||
path_a, path_b = Path(a), Path(b)
|
||||
lines_a = path_a.read_text(encoding="utf-8").splitlines(keepends=True)
|
||||
lines_b = path_b.read_text(encoding="utf-8").splitlines(keepends=True)
|
||||
result = difflib.unified_diff(lines_a, lines_b, fromfile=str(path_a), tofile=str(path_b))
|
||||
out = "".join(result)
|
||||
_emit_status("diff", file_a=str(path_a), file_b=str(path_b), identical=not out, preview=out[:500])
|
||||
return out
|
||||
|
||||
def output(
|
||||
*ids: str,
|
||||
format: str = "raw",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# OMP Ruby prelude helpers (loaded once into the runner's TOPLEVEL_BINDING).
|
||||
#
|
||||
# Mirrors eval/py/prelude.py: defines the cross-runtime helper surface
|
||||
# (display/read/write/append/tree/diff/env/output, the `tool` bridge proxy,
|
||||
# (display/read/write/env/output, the `tool` bridge proxy,
|
||||
# completion/agent/parallel/pipeline/log/phase/budget). Host-side helpers reach
|
||||
# the coding-agent over the same loopback HTTP tool bridge the Python prelude
|
||||
# uses (PI_TOOL_BRIDGE_URL/TOKEN/SESSION). Path helpers honor PI_EVAL_LOCAL_ROOTS
|
||||
@@ -112,191 +112,6 @@ unless defined?($__omp_prelude_loaded) && $__omp_prelude_loaded
|
||||
resolved.to_s
|
||||
end
|
||||
|
||||
def append(path, content)
|
||||
resolved = __omp_resolve_path(path)
|
||||
require "fileutils"
|
||||
FileUtils.mkdir_p(File.dirname(resolved.to_s))
|
||||
File.open(resolved.to_s, "a") { |f| f.write(content.to_s) }
|
||||
__omp_emit_status("append", "path" => resolved.to_s, "chars" => content.to_s.length)
|
||||
resolved.to_s
|
||||
end
|
||||
|
||||
def tree(path = ".", max_depth: 3, show_hidden: false)
|
||||
base = path.to_s
|
||||
lines = []
|
||||
walk = lambda do |dir, prefix, depth|
|
||||
return if depth > max_depth
|
||||
entries = (Dir.children(dir) rescue [])
|
||||
entries = entries.reject { |e| e.start_with?(".") } unless show_hidden
|
||||
entries = entries.sort_by { |e| [File.directory?(File.join(dir, e)) ? 0 : 1, e.downcase] }
|
||||
entries.each_with_index do |name, i|
|
||||
full = File.join(dir, name)
|
||||
is_last = i == entries.length - 1
|
||||
is_dir = File.directory?(full)
|
||||
lines << "#{prefix}#{is_last ? "└── " : "├── "}#{name}#{is_dir ? "/" : ""}"
|
||||
walk.call(full, prefix + (is_last ? " " : "│ "), depth + 1) if is_dir
|
||||
end
|
||||
end
|
||||
lines << "#{base}/"
|
||||
walk.call(base, "", 1)
|
||||
out = lines.join("\n")
|
||||
__omp_emit_status("tree", "path" => base, "entries" => lines.length - 1, "preview" => __omp_scrub(out[0, 1000].to_s))
|
||||
out
|
||||
end
|
||||
|
||||
def diff(a, b)
|
||||
pa = a.to_s
|
||||
pb = b.to_s
|
||||
lines_a = File.read(pa, encoding: Encoding::UTF_8).lines
|
||||
lines_b = File.read(pb, encoding: Encoding::UTF_8).lines
|
||||
out = __omp_unified_diff(lines_a, lines_b, pa, pb)
|
||||
__omp_emit_status("diff", "file_a" => pa, "file_b" => pb, "identical" => out.empty?, "preview" => __omp_scrub(out[0, 500].to_s))
|
||||
out
|
||||
end
|
||||
|
||||
# LCS-based op list ([:equal/:delete/:insert, aIndex, bIndex]) per consumed line.
|
||||
def __omp_diff_ops(a, b)
|
||||
n = a.length
|
||||
m = b.length
|
||||
if n * m > 4_000_000
|
||||
# Too large for the DP table — fall back to a coarse all-delete/all-insert.
|
||||
ops = []
|
||||
n.times { |i| ops << [:delete, i, 0] }
|
||||
m.times { |j| ops << [:insert, n, j] }
|
||||
return ops
|
||||
end
|
||||
dp = Array.new(n + 1) { Array.new(m + 1, 0) }
|
||||
(n - 1).downto(0) do |i|
|
||||
row = dp[i]
|
||||
nrow = dp[i + 1]
|
||||
(m - 1).downto(0) do |j|
|
||||
row[j] = a[i] == b[j] ? nrow[j + 1] + 1 : (nrow[j] >= row[j + 1] ? nrow[j] : row[j + 1])
|
||||
end
|
||||
end
|
||||
ops = []
|
||||
i = 0
|
||||
j = 0
|
||||
while i < n && j < m
|
||||
if a[i] == b[j]
|
||||
ops << [:equal, i, j]; i += 1; j += 1
|
||||
elsif dp[i + 1][j] >= dp[i][j + 1]
|
||||
ops << [:delete, i, j]; i += 1
|
||||
else
|
||||
ops << [:insert, i, j]; j += 1
|
||||
end
|
||||
end
|
||||
ops << [:delete, i, j].tap { i += 1 } while i < n
|
||||
ops << [:insert, i, j].tap { j += 1 } while j < m
|
||||
ops
|
||||
end
|
||||
|
||||
def __omp_unified_diff(a, b, from_file, to_file, context = 3)
|
||||
ops = __omp_diff_ops(a, b)
|
||||
return "" unless ops.any? { |tag, _, _| tag != :equal }
|
||||
|
||||
entries = ops.map do |tag, ai, bi|
|
||||
{ tag: tag, ai: ai, bi: bi, text: (tag == :insert ? b[bi] : a[ai]) }
|
||||
end
|
||||
changed = entries.each_index.select { |k| entries[k][:tag] != :equal }
|
||||
|
||||
groups = []
|
||||
start = nil
|
||||
prev = nil
|
||||
changed.each do |k|
|
||||
if start.nil?
|
||||
start = k
|
||||
prev = k
|
||||
elsif k - prev <= (2 * context) + 1
|
||||
prev = k
|
||||
else
|
||||
groups << [start, prev]
|
||||
start = k
|
||||
prev = k
|
||||
end
|
||||
end
|
||||
groups << [start, prev] unless start.nil?
|
||||
|
||||
out = +""
|
||||
out << "--- #{from_file}\n"
|
||||
out << "+++ #{to_file}\n"
|
||||
groups.each do |gs, ge|
|
||||
lo = [gs - context, 0].max
|
||||
hi = [ge + context, entries.length - 1].min
|
||||
slice = entries[lo..hi]
|
||||
a_start = nil
|
||||
a_count = 0
|
||||
b_start = nil
|
||||
b_count = 0
|
||||
slice.each do |e|
|
||||
if e[:tag] != :insert
|
||||
a_start ||= e[:ai]
|
||||
a_count += 1
|
||||
end
|
||||
if e[:tag] != :delete
|
||||
b_start ||= e[:bi]
|
||||
b_count += 1
|
||||
end
|
||||
end
|
||||
out << "@@ -#{(a_start || 0) + 1},#{a_count} +#{(b_start || 0) + 1},#{b_count} @@\n"
|
||||
slice.each do |e|
|
||||
prefix = e[:tag] == :equal ? " " : (e[:tag] == :delete ? "-" : "+")
|
||||
text = e[:text].to_s
|
||||
text = "#{text}\n" unless text.end_with?("\n")
|
||||
out << "#{prefix}#{text}"
|
||||
end
|
||||
end
|
||||
out
|
||||
end
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Text helpers (sort / uniq / counter)
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def sort(text, reverse: false, unique: false)
|
||||
lines = text.to_s.lines.map(&:chomp)
|
||||
lines = lines.uniq if unique
|
||||
lines = lines.sort
|
||||
lines = lines.reverse if reverse
|
||||
out = lines.join("\n")
|
||||
__omp_emit_status("sort", "lines" => lines.length, "unique" => unique, "reverse" => reverse)
|
||||
out
|
||||
end
|
||||
|
||||
def uniq(text, count: false)
|
||||
lines = text.to_s.lines.map(&:chomp)
|
||||
if lines.empty?
|
||||
__omp_emit_status("uniq", "groups" => 0)
|
||||
return count ? [] : ""
|
||||
end
|
||||
groups = []
|
||||
current = lines[0]
|
||||
run = 1
|
||||
lines[1..].each do |line|
|
||||
if line == current
|
||||
run += 1
|
||||
else
|
||||
groups << [run, current]
|
||||
current = line
|
||||
run = 1
|
||||
end
|
||||
end
|
||||
groups << [run, current]
|
||||
__omp_emit_status("uniq", "groups" => groups.length, "count_mode" => count)
|
||||
count ? groups : groups.map { |_, l| l }.join("\n")
|
||||
end
|
||||
|
||||
def counter(items, limit: nil, reverse: true)
|
||||
arr = items.is_a?(String) ? items.lines.map(&:chomp) : items.to_a
|
||||
counts = Hash.new(0)
|
||||
arr.each { |i| counts[i] += 1 }
|
||||
sorted = counts.sort_by { |item, c| [c, item] }
|
||||
sorted = sorted.reverse if reverse
|
||||
sorted = sorted.first(limit) if limit
|
||||
result = sorted.map { |item, c| [c, item] }
|
||||
__omp_emit_status("counter", "unique" => counts.size, "total" => arr.length, "top" => result.first(10))
|
||||
result
|
||||
end
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Task/agent output reader
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@@ -157,7 +157,7 @@ export function resolveLocalUrlToPath(
|
||||
}
|
||||
|
||||
/**
|
||||
* On-disk roots the eval helpers (`read`/`write`/`append`) substitute for
|
||||
* On-disk roots the eval helpers (`read`/`write`) substitute for
|
||||
* internal-URL schemes so e.g. `write("local://x.md")` lands where a later
|
||||
* `read local://x.md` resolves — instead of a literal `local:/` directory under
|
||||
* the cwd (a stdlib `pathlib.Path`/`path.resolve` collapses `local://` to
|
||||
|
||||
@@ -15,8 +15,8 @@ Fields:
|
||||
|
||||
{{#if py}}Live event loop: use top-level `await` directly; `asyncio.run(…)` raises "cannot be called from a running event loop".{{/if}}
|
||||
{{#if js}}JS runs under **Bun**: Bun globals/APIs are available (`Bun.file`, `Bun.write`, `Bun.$`, `fetch`, `Buffer`); top-level `await`/`return` work directly.{{/if}}
|
||||
{{#if rb}}Ruby: synchronous; helper options are keyword args (e.g. `tree(".", max_depth: 2)`); the last expression auto-displays unless it is `nil`, an assignment, or a definition (like IRB).{{/if}}
|
||||
{{#if jl}}Julia: synchronous; helper options are standard keyword args (e.g. `tree(max_depth=2)`); the last expression auto-displays unless it is an assignment or a definition (like the Julia REPL).{{/if}}
|
||||
{{#if rb}}Ruby: synchronous; helper options are keyword args (e.g. `output("id", limit: 2)`); the last expression auto-displays unless it is `nil`, an assignment, or a definition (like IRB).{{/if}}
|
||||
{{#if jl}}Julia: synchronous; helper options are standard keyword args (e.g. `output("id", limit=2)`); the last expression auto-displays unless it is an assignment or a definition (like the Julia REPL).{{/if}}
|
||||
On error, fix and re-run only the failing step — prior calls' state survives.
|
||||
</instruction>
|
||||
|
||||
@@ -31,12 +31,6 @@ read(path, offset?=1, limit?=None) → str
|
||||
File as text; offset/limit 1-indexed lines. Accepts `local://…`.
|
||||
write(path, content) → str
|
||||
Write file (creates parents) → resolved path. `local://…` persists across turns/subagents.
|
||||
append(path, content) → str
|
||||
Append → resolved path. Accepts `local://…`.
|
||||
tree(path?=".", max_depth?=3, show_hidden?=False) → str
|
||||
Directory tree.
|
||||
diff(a, b) → str
|
||||
Unified diff of two files.
|
||||
env(key?=None, value?=None) → str | None | dict
|
||||
No args → full env dict; one → value of `key`; two → set `key=value`, return value.
|
||||
output(*ids, format?="raw", query?=None, offset?=None, limit?=None) → str | dict | list[dict]
|
||||
|
||||
@@ -800,7 +800,7 @@ export class WorkerCore {
|
||||
displays.push({ type: "text", text: safeJsonStringify(output.data) });
|
||||
return;
|
||||
}
|
||||
// status — surface as compact JSON so helper side effects (read/write/tree) appear in
|
||||
// status — surface as compact JSON so helper side effects (read/write/env) appear in
|
||||
// the cell result alongside explicit display() output.
|
||||
displays.push({ type: "text", text: safeJsonStringify(output.event) });
|
||||
}
|
||||
|
||||
@@ -241,14 +241,12 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
|
||||
const opIcons: Record<string, AvailableIcon> = {
|
||||
read: "icon.file",
|
||||
write: "icon.file",
|
||||
append: "icon.file",
|
||||
cat: "icon.file",
|
||||
touch: "icon.file",
|
||||
ls: "icon.folder",
|
||||
cd: "icon.folder",
|
||||
pwd: "icon.folder",
|
||||
mkdir: "icon.folder",
|
||||
tree: "icon.folder",
|
||||
git_status: "icon.git",
|
||||
git_diff: "icon.git",
|
||||
git_log: "icon.git",
|
||||
@@ -280,7 +278,6 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
|
||||
if (data.path) parts.push(`from ${shortenPath(String(data.path))}`);
|
||||
break;
|
||||
case "write":
|
||||
case "append":
|
||||
parts.push(`${data.chars ?? data.bytes ?? 0} chars`);
|
||||
if (data.path) parts.push(`to ${shortenPath(String(data.path))}`);
|
||||
break;
|
||||
@@ -319,13 +316,6 @@ function formatStatusEvent(event: EvalStatusEvent, theme: Theme): string {
|
||||
parts.push(`${data.lines} line${(data.lines as number) !== 1 ? "s" : ""}`);
|
||||
if (data.staged) parts.push("(staged)");
|
||||
break;
|
||||
case "diff":
|
||||
if (data.identical) {
|
||||
parts.push("files identical");
|
||||
} else {
|
||||
parts.push("files differ");
|
||||
}
|
||||
break;
|
||||
case "batch":
|
||||
parts.push(`${data.files} file${(data.files as number) !== 1 ? "s" : ""} processed`);
|
||||
break;
|
||||
@@ -415,8 +405,6 @@ function formatStatusEventExpanded(event: EvalStatusEvent, theme: Theme): string
|
||||
case "cat":
|
||||
case "head":
|
||||
case "tail":
|
||||
case "tree":
|
||||
case "diff":
|
||||
case "git_diff":
|
||||
case "sh":
|
||||
if (data.preview) addPreview(String(data.preview));
|
||||
|
||||
@@ -144,13 +144,10 @@ describe.skipIf(!SHOULD_RUN)("ruby runner subprocess", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("exposes prelude file + text helpers", async () => {
|
||||
it("exposes prelude file helpers", async () => {
|
||||
using tempDir = TempDir.createSync("@ruby-runner-prelude-");
|
||||
const kernel = await RubyKernel.start({ cwd: tempDir.path() });
|
||||
try {
|
||||
const sorted = await executeRubyWithKernel(kernel, 'sort("b\\na\\nb", unique: true)', {});
|
||||
expect(sorted.output).toContain("a\nb");
|
||||
|
||||
const written = await executeRubyWithKernel(kernel, 'write("note.txt", "hello"); read("note.txt")', {});
|
||||
expect(written.output).toContain("hello");
|
||||
} finally {
|
||||
|
||||
Reference in New Issue
Block a user