Files
LLeMbas-CLI/src/tool/show.ts
T
HomerandClaude Opus 5.5 fbefe7f844
ci / check (push) Waiting to run
Harness spec 3.2.0 in the CLI: effort levels, reasoning round-trip, no absolute paths
Levels (models/schema.json levels): provider/effort.ts levelsOf/levelFields/
readLevel — `off` only with thinking_switch and sends enable_thinking false
and no effort, `default` for a model that takes no efforts and sends
nothing, a level not offered is read as the start level. The engine's
effort is a level; /effort shows nothing for one level, a Thinking switch
for [off, X], a list otherwise; the status bar follows. /v1/models'
thinking_switch, efforts [] and capacity.parallel are read
(lembas/webui.ts); session/capacity.ts counts parallel slots.

Reasoning (context.md §8): an assistant message of the current turn
carries reasoning_content (provider/common.ts turnReasoning,
openai-chat.ts); step-boundary deliveries, nudges and the wrap-up are
marked turn: false and do not start a turn.

Paths (context.md §11): the <env> block names the project and, started
below the root, that directory relative to it (assemble.ts envLines);
instruction files and @ attachments are named from the root or as ~/…;
file tools and bash resolve relative paths against the project root.

The team rules (team/team.ts) and conformance runners for levels,
reasoning, paths and team; the team runtime follows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:36:55 +00:00

77 lines
4.1 KiB
TypeScript

// show (harness spec 3.0.0): a file opened in the user's viewer beside the conversation. A terminal
// has no such viewer, so the tool is offered only in a session a client opened over the link and
// only when that client said it can show files (`show: true`, harness/acp.md `_lembas/show`): the
// device reads the file and sends it, since the client cannot reach this disk.
import { statSync } from "node:fs"
import { basename, extname, relative } from "node:path"
import { z } from "zod"
import { described, TOOL_SPECS } from "../harness.ts"
import { absPath, defineTool, readText, showPath, ToolError } from "./tool.ts"
/** The most a file may be to be sent (acp.md: at most 1 MB; a larger one is refused before sending). */
export const SHOW_MAX_BYTES = 1024 * 1024
/** The answer when nothing can be shown (spec `unavailable`), word for word. */
export const SHOW_UNAVAILABLE = TOOL_SPECS.show!.unavailable!
/** What `_lembas/show` carries for one file. */
export interface ShowRequest {
path: string
kind: "markdown" | "code" | "text" | "html"
language?: string
line?: number
text: string
}
export type ShowReply = { shown: true } | { shown: false; reason: string }
// A known source extension is code, with its language; the list is the viewer's highlighters.
const LANGUAGES: Record<string, string> = {
ts: "typescript", tsx: "tsx", js: "javascript", mjs: "javascript", cjs: "javascript", jsx: "jsx", json: "json", py: "python", rs: "rust", go: "go",
rb: "ruby", php: "php", java: "java", kt: "kotlin", swift: "swift", c: "c", h: "c", cc: "cpp", cpp: "cpp", hpp: "cpp", cs: "csharp",
sh: "bash", bash: "bash", zsh: "bash", fish: "fish", css: "css", scss: "scss", sql: "sql", yaml: "yaml", yml: "yaml", toml: "toml",
xml: "xml", svg: "xml", lua: "lua", zig: "zig", dart: "dart", vue: "vue", svelte: "svelte",
}
/** The viewer's kind for a file, by its name (acp.md): `.md` markdown, `.html`/`.htm` html, a known
* source extension code, anything else text. */
export function showKind(path: string): { kind: ShowRequest["kind"]; language?: string } {
const ext = extname(path).slice(1).toLowerCase()
if (ext === "md" || ext === "markdown") return { kind: "markdown" }
if (ext === "html" || ext === "htm") return { kind: "html" }
const language = LANGUAGES[ext] ?? (basename(path) === "Dockerfile" ? "dockerfile" : basename(path) === "Makefile" ? "makefile" : undefined)
return language ? { kind: "code", language } : { kind: "text" }
}
export const showTool = defineTool({
...described("show"),
access: "show",
requires: "display",
schema: z.object({ path: z.string(), line: z.number().int().min(1).optional() }),
permission: (a, ctx) => {
const abs = absPath(a.path, ctx)
return { permission: "show", class: "interact", patterns: [showPath(abs, ctx)], paths: [abs] }
},
async run(a, ctx) {
// Offered with a display; one that went away since answers the spec's sentence.
if (!ctx.show) return { output: SHOW_UNAVAILABLE, isError: true }
const abs = absPath(a.path, ctx)
let size: number
try {
const st = statSync(abs)
if (!st.isFile()) throw new ToolError(st.isDirectory() ? `Is a directory: ${abs}` : `${showPath(abs, ctx)} is not a file.`)
size = st.size
} catch (e) {
if (e instanceof ToolError) throw e
throw new ToolError(`No such file or directory: ${abs}`)
}
if (size > SHOW_MAX_BYTES) throw new ToolError(`${showPath(abs, ctx)} is ${Math.round(size / 1024)} KB; at most 1 MB can be shown. Give the user the path instead.`)
const text = await readText(abs)
if (text.includes("\u0000")) throw new ToolError(`${showPath(abs, ctx)} is not a text file; it cannot be shown. Give the user the path instead.`)
const rel = relative(ctx.root, abs) || basename(abs)
const reply = await ctx.show({ path: rel, ...showKind(abs), ...(a.line ? { line: a.line } : {}), text }, ctx.callId)
if (!reply.shown) return { output: `Not shown: ${reply.reason}`, isError: true }
return { output: `Shown to the user: ${showPath(abs, ctx)}${a.line ? ` at line ${a.line}` : ""}.`, title: showPath(abs, ctx) }
},
})