@agimon-ai/doompi-runner 0.0.1-alpha.12 → 0.0.1-alpha.13
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/dist/commands/bash/bashTool.cjs +1 -1
- package/dist/commands/bash/bashTool.cjs.map +1 -1
- package/dist/commands/bash/bashTool.mjs +1 -1
- package/dist/commands/bash/bashTool.mjs.map +1 -1
- package/dist/tui/bashRender.cjs +1 -1
- package/dist/tui/bashRender.cjs.map +1 -1
- package/dist/tui/bashRender.d.cts +1 -1
- package/dist/tui/bashRender.d.cts.map +1 -1
- package/dist/tui/bashRender.d.mts +1 -1
- package/dist/tui/bashRender.d.mts.map +1 -1
- package/dist/tui/bashRender.mjs +1 -1
- package/dist/tui/bashRender.mjs.map +1 -1
- package/package.json +9 -9
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const e=require("../../types/config.cjs"),t=require("../../services/AnsiScrub/ansiScrub.cjs");require("../../services/AnsiScrub/index.cjs");const n=require("./responseEnvelope.cjs"),r=require("../../schemas/bashTool.cjs"),i=require("../../tui/bashRender.cjs"),a=1e3,o=`Options:`,s={requested:`Started in the background`,threshold:`Still running after the background threshold, so it was moved to the background`,interactive:`Started on a terminal so it can prompt, which means it runs in the background`},c=`Execute bash commands, with long-running ones supervised in the background`;function l(t=e.getBackgroundThresholdMs()){return[`A command still running after ${Math.round(t/a)} seconds remains active as a background runner. You get its unique id and streaming log path, then inspect it with the doom-runner CLI.`,`Pass background: true for commands you already know will not finish on their own (dev servers, watchers, tails). Waiting the full threshold first wastes a turn.`,`Pass interactive: true only when the command will prompt for confirmation or input. Use Runner Space for terminal input. Interactive logs are noisier, so avoid it otherwise.`,`Do not re-run a command to recover output. Read its saved log with doom-runner logs <id>.`,`Stop runners you no longer need. Every runner is stopped automatically when the session ends.`]}function u(e,t){e.registerTool({name:r.BASH_TOOL_NAME,label:r.BASH_TOOL_LABEL,description:`Execute a bash command in the current working directory. A command that outlives the background threshold keeps running under doom-runner and returns a unique id plus a streaming log path. Completed commands return a bounded log tail with exact file metadata. Optionally provide a timeout in seconds, background: true to background it immediately, or interactive: true if it will prompt.`,promptSnippet:c,promptGuidelines:l(),parameters:r.BashParamsSchema,renderShell:`self`,async execute(e,r,i,s,c){let{command:l,timeout:u,background:f,interactive:p,name:m}=r,h;s&&s(n.textResult(`Starting ${p===!0?`interactive runner`:f===!0?`background runner`:`command`}...`)),f!==!0&&p!==!0&&s&&(h=e=>s(n.textResult(e)));let g;try{g=await t.bashRunService.run({command:l,timeoutMs:u===void 0?void 0:u*a,background:f,interactive:p,name:m,...h?{onOutput:h}:{},sessionId:t.getSessionId()})}catch(e){let t=e instanceof Error?e.message:String(e);throw Error([`Could not execute the command: ${t}`,``,o,`- Check the command, runtime, and working directory, then retry once.`,`- Use a read-only diagnostic to confirm the missing dependency or environment value.`,`- Ask the user if recovery requires changing the requested approach.`].join(`
|
|
2
|
-
`),{cause:e})}return g.kind===`promoted`&&t.onRunnerStarted(g.id),d(g)},renderCall(e,t,n){return i.renderBashCall(e,t)},renderResult(e,t,n,r){return i.renderBashResult(e,t,n)}})}function d(e){if(e.kind===`failed`)throw Error([`Could not start runner "${e.name}": ${e.error}`,``,o,`- Correct the command or missing executable, then retry once.`,`- Use a read-only diagnostic to inspect the runtime environment.`,`- Ask the user if a different execution approach is required.`].join(`
|
|
2
|
+
`),{cause:e})}return g.kind===`promoted`&&t.onRunnerStarted(g.id),d(g)},renderCall(e,t,n){return i.renderBashCall(e,t)},renderResult(e,t,n,r){return i.renderBashResult(e,{...t,isError:r.isError},n)}})}function d(e){if(e.kind===`failed`)throw Error([`Could not start runner "${e.name}": ${e.error}`,``,o,`- Correct the command or missing executable, then retry once.`,`- Use a read-only diagnostic to inspect the runtime environment.`,`- Ask the user if a different execution approach is required.`].join(`
|
|
3
3
|
`));if(e.kind===`promoted`){let t=[`${s[e.reason]} as runner "${e.name}".`,`Runner ID: ${e.id}`,`Backend: ${e.backend}`,`Log (streaming): ${e.logPath}`,`Inspect it with: doom-runner logs ${e.id}`].filter(e=>e.length>0).join(`
|
|
4
4
|
`);return{...n.textResult(t,{id:e.id,runner:e.name,pid:e.pid,logPath:e.logPath,promoted:!0,reason:e.reason})}}let r=n.summarizeLog(e.logPath),i=[];e.timedOut?i.push(`Command stopped: it exceeded the requested timeout.`):e.exitCode===null?e.signal!==null&&i.push(`Terminated by ${e.signal}`):i.push(`Exit: completed with code ${e.exitCode}.`);let a=t.stripAnsi(r.tail),c=[a.length>0?`Log tail (${r.tailLines} lines):\n${a}`:`Log tail: (no output)`,`Runner ID: ${e.id}`,...i,`Log: ${e.logPath}`,`File size: ${n.formatSize(r.bytes)}`,`Lines: ${r.lines.toLocaleString(`en-US`)}`].join(`
|
|
5
5
|
`),l={id:e.id,runner:e.name,exitCode:e.exitCode,logPath:e.logPath,backend:e.backend,fileSize:r.bytes,lines:r.lines,tail:r.tail,tailLines:r.tailLines,...e.timedOut?{timedOut:!0}:{}};if(e.timedOut||e.signal!==null||e.exitCode!==null&&e.exitCode!==0)throw Error([c,``,o,`- Inspect the full log at ${e.logPath}, correct the cause, then retry once.`,`- Run a read-only diagnostic if the output does not explain the failure.`,`- Ask the user if the failure requires changing the requested approach.`].join(`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bashTool.cjs","names":["getBackgroundThresholdMs","BASH_TOOL_NAME","BASH_TOOL_LABEL","BashParamsSchema","textResult","renderBashCall","renderBashResult","summarizeLog","stripAnsi","formatSize"],"sources":["../../../src/commands/bash/bashTool.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { BASH_TOOL_LABEL, BASH_TOOL_NAME, type BashParams, BashParamsSchema } from '../../schemas/bashTool.ts';\nimport { stripAnsi } from '../../services/AnsiScrub/index.ts';\nimport type { BashRunResult, IBashRunService } from '../../services/BashRunService/index.ts';\nimport { renderBashCall, renderBashResult } from '../../tui/bashRender.ts';\nimport { getBackgroundThresholdMs } from '../../types/config.ts';\nimport { formatSize, summarizeLog, type ToolResult, textResult } from './responseEnvelope.ts';\n\nconst MS_PER_SECOND = 1000;\nconst ERROR_OPTIONS_HEADING = 'Options:';\n\nconst PROMOTION_REASONS: Record<'requested' | 'threshold' | 'interactive', string> = {\n requested: 'Started in the background',\n threshold: 'Still running after the background threshold, so it was moved to the background',\n interactive: 'Started on a terminal so it can prompt, which means it runs in the background',\n};\n\nexport const BASH_PROMPT_SNIPPET = 'Execute bash commands, with long-running ones supervised in the background';\n\n/** Written at registration time so the stated threshold matches the configured one. */\nexport function bashPromptGuidelines(thresholdMs = getBackgroundThresholdMs()): string[] {\n return [\n `A command still running after ${Math.round(thresholdMs / MS_PER_SECOND)} seconds remains active as a background runner. You get its unique id and streaming log path, then inspect it with the doom-runner CLI.`,\n 'Pass background: true for commands you already know will not finish on their own (dev servers, watchers, tails). Waiting the full threshold first wastes a turn.',\n 'Pass interactive: true only when the command will prompt for confirmation or input. Use Runner Space for terminal input. Interactive logs are noisier, so avoid it otherwise.',\n 'Do not re-run a command to recover output. Read its saved log with doom-runner logs <id>.',\n 'Stop runners you no longer need. Every runner is stopped automatically when the session ends.',\n ];\n}\n\nexport interface BashToolDependencies {\n bashRunService: IBashRunService;\n getSessionId(): string;\n /** Called after a runner is promoted, so UI state can refresh. */\n onRunnerStarted(id: string): void;\n}\n\n/**\n * Registers a tool named `bash`, replacing pi's built-in.\n *\n * The name is deliberate: hooks, guardrails and doom-pi's dispatcher all key on\n * `bash`, and they keep working only while the replacement keeps the name.\n */\nexport function registerBashTool(pi: ExtensionAPI, dependencies: BashToolDependencies): void {\n pi.registerTool({\n name: BASH_TOOL_NAME,\n label: BASH_TOOL_LABEL,\n description:\n 'Execute a bash command in the current working directory. A command that outlives the background threshold keeps running under doom-runner and returns a unique id plus a streaming log path. Completed commands return a bounded log tail with exact file metadata. Optionally provide a timeout in seconds, background: true to background it immediately, or interactive: true if it will prompt.',\n promptSnippet: BASH_PROMPT_SNIPPET,\n promptGuidelines: bashPromptGuidelines(),\n parameters: BashParamsSchema,\n // Bash output already carries its own status glyphs and ANSI colours. Owning\n // the shell keeps Pi from filling every successful command with the global\n // toolSuccessBg, which overwhelms long logs and diffs.\n renderShell: 'self',\n\n async execute(_toolCallId, params, _signal, onUpdate, _ctx): Promise<ToolResult> {\n const { command, timeout, background, interactive, name } = params as BashParams;\n let onOutput: ((output: string) => void) | undefined;\n if (onUpdate) {\n const mode =\n interactive === true ? 'interactive runner' : background === true ? 'background runner' : 'command';\n onUpdate(textResult(`Starting ${mode}...`));\n }\n if (background !== true && interactive !== true && onUpdate) {\n onOutput = (output) => onUpdate(textResult(output));\n }\n let result: BashRunResult;\n try {\n result = await dependencies.bashRunService.run({\n command,\n timeoutMs: timeout === undefined ? undefined : timeout * MS_PER_SECOND,\n background,\n interactive,\n name,\n ...(onOutput ? { onOutput } : {}),\n sessionId: dependencies.getSessionId(),\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n [\n `Could not execute the command: ${message}`,\n '',\n ERROR_OPTIONS_HEADING,\n '- Check the command, runtime, and working directory, then retry once.',\n '- Use a read-only diagnostic to confirm the missing dependency or environment value.',\n '- Ask the user if recovery requires changing the requested approach.',\n ].join('\\n'),\n { cause: error },\n );\n }\n\n if (result.kind === 'promoted') dependencies.onRunnerStarted(result.id);\n return formatRunResult(result);\n },\n\n // Without these, pi falls back to echoing the raw command and the tail end of\n // the result text, which is the metadata footer rather than the log output.\n renderCall(args, theme, _context) {\n return renderBashCall(args as BashParams, theme);\n },\n\n renderResult(result, options, theme, _context) {\n return renderBashResult(result, options, theme);\n },\n });\n}\n\nexport function formatRunResult(result: BashRunResult): ToolResult {\n if (result.kind === 'failed') {\n throw new Error(\n [\n `Could not start runner \"${result.name}\": ${result.error}`,\n '',\n ERROR_OPTIONS_HEADING,\n '- Correct the command or missing executable, then retry once.',\n '- Use a read-only diagnostic to inspect the runtime environment.',\n '- Ask the user if a different execution approach is required.',\n ].join('\\n'),\n );\n }\n\n if (result.kind === 'promoted') {\n const reason = PROMOTION_REASONS[result.reason];\n const body = [\n `${reason} as runner \"${result.name}\".`,\n `Runner ID: ${result.id}`,\n `Backend: ${result.backend}`,\n `Log (streaming): ${result.logPath}`,\n `Inspect it with: doom-runner logs ${result.id}`,\n ]\n .filter((line) => line.length > 0)\n .join('\\n');\n\n return {\n ...textResult(body, {\n id: result.id,\n runner: result.name,\n pid: result.pid,\n logPath: result.logPath,\n promoted: true,\n reason: result.reason,\n }),\n };\n }\n\n const log = summarizeLog(result.logPath);\n const notes: string[] = [];\n if (result.timedOut) notes.push('Command stopped: it exceeded the requested timeout.');\n else if (result.exitCode !== null) notes.push(`Exit: completed with code ${result.exitCode}.`);\n else if (result.signal !== null) notes.push(`Terminated by ${result.signal}`);\n\n // The model reads plain text; only the renderer gets the coloured copy below.\n const plainTail = stripAnsi(log.tail);\n const text = [\n plainTail.length > 0 ? `Log tail (${log.tailLines} lines):\\n${plainTail}` : 'Log tail: (no output)',\n `Runner ID: ${result.id}`,\n ...notes,\n `Log: ${result.logPath}`,\n `File size: ${formatSize(log.bytes)}`,\n `Lines: ${log.lines.toLocaleString('en-US')}`,\n ].join('\\n');\n const details = {\n id: result.id,\n runner: result.name,\n exitCode: result.exitCode,\n logPath: result.logPath,\n backend: result.backend,\n fileSize: log.bytes,\n lines: log.lines,\n // Carried structurally so renderResult shows the output instead of the footer.\n tail: log.tail,\n tailLines: log.tailLines,\n ...(result.timedOut ? { timedOut: true } : {}),\n };\n if (result.timedOut || result.signal !== null || (result.exitCode !== null && result.exitCode !== 0)) {\n throw new Error(\n [\n text,\n '',\n ERROR_OPTIONS_HEADING,\n `- Inspect the full log at ${result.logPath}, correct the cause, then retry once.`,\n '- Run a read-only diagnostic if the output does not explain the failure.',\n '- Ask the user if the failure requires changing the requested approach.',\n ].join('\\n'),\n );\n }\n return textResult(text, details);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bashTool.cjs","names":["getBackgroundThresholdMs","BASH_TOOL_NAME","BASH_TOOL_LABEL","BashParamsSchema","textResult","renderBashCall","renderBashResult","summarizeLog","stripAnsi","formatSize"],"sources":["../../../src/commands/bash/bashTool.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { BASH_TOOL_LABEL, BASH_TOOL_NAME, type BashParams, BashParamsSchema } from '../../schemas/bashTool.ts';\nimport { stripAnsi } from '../../services/AnsiScrub/index.ts';\nimport type { BashRunResult, IBashRunService } from '../../services/BashRunService/index.ts';\nimport { renderBashCall, renderBashResult } from '../../tui/bashRender.ts';\nimport { getBackgroundThresholdMs } from '../../types/config.ts';\nimport { formatSize, summarizeLog, type ToolResult, textResult } from './responseEnvelope.ts';\n\nconst MS_PER_SECOND = 1000;\nconst ERROR_OPTIONS_HEADING = 'Options:';\n\nconst PROMOTION_REASONS: Record<'requested' | 'threshold' | 'interactive', string> = {\n requested: 'Started in the background',\n threshold: 'Still running after the background threshold, so it was moved to the background',\n interactive: 'Started on a terminal so it can prompt, which means it runs in the background',\n};\n\nexport const BASH_PROMPT_SNIPPET = 'Execute bash commands, with long-running ones supervised in the background';\n\n/** Written at registration time so the stated threshold matches the configured one. */\nexport function bashPromptGuidelines(thresholdMs = getBackgroundThresholdMs()): string[] {\n return [\n `A command still running after ${Math.round(thresholdMs / MS_PER_SECOND)} seconds remains active as a background runner. You get its unique id and streaming log path, then inspect it with the doom-runner CLI.`,\n 'Pass background: true for commands you already know will not finish on their own (dev servers, watchers, tails). Waiting the full threshold first wastes a turn.',\n 'Pass interactive: true only when the command will prompt for confirmation or input. Use Runner Space for terminal input. Interactive logs are noisier, so avoid it otherwise.',\n 'Do not re-run a command to recover output. Read its saved log with doom-runner logs <id>.',\n 'Stop runners you no longer need. Every runner is stopped automatically when the session ends.',\n ];\n}\n\nexport interface BashToolDependencies {\n bashRunService: IBashRunService;\n getSessionId(): string;\n /** Called after a runner is promoted, so UI state can refresh. */\n onRunnerStarted(id: string): void;\n}\n\n/**\n * Registers a tool named `bash`, replacing pi's built-in.\n *\n * The name is deliberate: hooks, guardrails and doom-pi's dispatcher all key on\n * `bash`, and they keep working only while the replacement keeps the name.\n */\nexport function registerBashTool(pi: ExtensionAPI, dependencies: BashToolDependencies): void {\n pi.registerTool({\n name: BASH_TOOL_NAME,\n label: BASH_TOOL_LABEL,\n description:\n 'Execute a bash command in the current working directory. A command that outlives the background threshold keeps running under doom-runner and returns a unique id plus a streaming log path. Completed commands return a bounded log tail with exact file metadata. Optionally provide a timeout in seconds, background: true to background it immediately, or interactive: true if it will prompt.',\n promptSnippet: BASH_PROMPT_SNIPPET,\n promptGuidelines: bashPromptGuidelines(),\n parameters: BashParamsSchema,\n // Bash output already carries its own status glyphs and ANSI colours. Owning\n // the shell keeps Pi from filling every successful command with the global\n // toolSuccessBg, which overwhelms long logs and diffs.\n renderShell: 'self',\n\n async execute(_toolCallId, params, _signal, onUpdate, _ctx): Promise<ToolResult> {\n const { command, timeout, background, interactive, name } = params as BashParams;\n let onOutput: ((output: string) => void) | undefined;\n if (onUpdate) {\n const mode =\n interactive === true ? 'interactive runner' : background === true ? 'background runner' : 'command';\n onUpdate(textResult(`Starting ${mode}...`));\n }\n if (background !== true && interactive !== true && onUpdate) {\n onOutput = (output) => onUpdate(textResult(output));\n }\n let result: BashRunResult;\n try {\n result = await dependencies.bashRunService.run({\n command,\n timeoutMs: timeout === undefined ? undefined : timeout * MS_PER_SECOND,\n background,\n interactive,\n name,\n ...(onOutput ? { onOutput } : {}),\n sessionId: dependencies.getSessionId(),\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n [\n `Could not execute the command: ${message}`,\n '',\n ERROR_OPTIONS_HEADING,\n '- Check the command, runtime, and working directory, then retry once.',\n '- Use a read-only diagnostic to confirm the missing dependency or environment value.',\n '- Ask the user if recovery requires changing the requested approach.',\n ].join('\\n'),\n { cause: error },\n );\n }\n\n if (result.kind === 'promoted') dependencies.onRunnerStarted(result.id);\n return formatRunResult(result);\n },\n\n // Without these, pi falls back to echoing the raw command and the tail end of\n // the result text, which is the metadata footer rather than the log output.\n renderCall(args, theme, _context) {\n return renderBashCall(args as BashParams, theme);\n },\n\n renderResult(result, options, theme, context) {\n return renderBashResult(result, { ...options, isError: context.isError }, theme);\n },\n });\n}\n\nexport function formatRunResult(result: BashRunResult): ToolResult {\n if (result.kind === 'failed') {\n throw new Error(\n [\n `Could not start runner \"${result.name}\": ${result.error}`,\n '',\n ERROR_OPTIONS_HEADING,\n '- Correct the command or missing executable, then retry once.',\n '- Use a read-only diagnostic to inspect the runtime environment.',\n '- Ask the user if a different execution approach is required.',\n ].join('\\n'),\n );\n }\n\n if (result.kind === 'promoted') {\n const reason = PROMOTION_REASONS[result.reason];\n const body = [\n `${reason} as runner \"${result.name}\".`,\n `Runner ID: ${result.id}`,\n `Backend: ${result.backend}`,\n `Log (streaming): ${result.logPath}`,\n `Inspect it with: doom-runner logs ${result.id}`,\n ]\n .filter((line) => line.length > 0)\n .join('\\n');\n\n return {\n ...textResult(body, {\n id: result.id,\n runner: result.name,\n pid: result.pid,\n logPath: result.logPath,\n promoted: true,\n reason: result.reason,\n }),\n };\n }\n\n const log = summarizeLog(result.logPath);\n const notes: string[] = [];\n if (result.timedOut) notes.push('Command stopped: it exceeded the requested timeout.');\n else if (result.exitCode !== null) notes.push(`Exit: completed with code ${result.exitCode}.`);\n else if (result.signal !== null) notes.push(`Terminated by ${result.signal}`);\n\n // The model reads plain text; only the renderer gets the coloured copy below.\n const plainTail = stripAnsi(log.tail);\n const text = [\n plainTail.length > 0 ? `Log tail (${log.tailLines} lines):\\n${plainTail}` : 'Log tail: (no output)',\n `Runner ID: ${result.id}`,\n ...notes,\n `Log: ${result.logPath}`,\n `File size: ${formatSize(log.bytes)}`,\n `Lines: ${log.lines.toLocaleString('en-US')}`,\n ].join('\\n');\n const details = {\n id: result.id,\n runner: result.name,\n exitCode: result.exitCode,\n logPath: result.logPath,\n backend: result.backend,\n fileSize: log.bytes,\n lines: log.lines,\n // Carried structurally so renderResult shows the output instead of the footer.\n tail: log.tail,\n tailLines: log.tailLines,\n ...(result.timedOut ? { timedOut: true } : {}),\n };\n if (result.timedOut || result.signal !== null || (result.exitCode !== null && result.exitCode !== 0)) {\n throw new Error(\n [\n text,\n '',\n ERROR_OPTIONS_HEADING,\n `- Inspect the full log at ${result.logPath}, correct the cause, then retry once.`,\n '- Run a read-only diagnostic if the output does not explain the failure.',\n '- Ask the user if the failure requires changing the requested approach.',\n ].join('\\n'),\n );\n }\n return textResult(text, details);\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import{getBackgroundThresholdMs as e}from"../../types/config.mjs";import{stripAnsi as t}from"../../services/AnsiScrub/ansiScrub.mjs";import"../../services/AnsiScrub/index.mjs";import{formatSize as n,summarizeLog as r,textResult as i}from"./responseEnvelope.mjs";import{BASH_TOOL_LABEL as a,BASH_TOOL_NAME as o,BashParamsSchema as s}from"../../schemas/bashTool.mjs";import{renderBashCall as c,renderBashResult as l}from"../../tui/bashRender.mjs";const u=1e3,d=`Options:`,f={requested:`Started in the background`,threshold:`Still running after the background threshold, so it was moved to the background`,interactive:`Started on a terminal so it can prompt, which means it runs in the background`},p=`Execute bash commands, with long-running ones supervised in the background`;function m(t=e()){return[`A command still running after ${Math.round(t/u)} seconds remains active as a background runner. You get its unique id and streaming log path, then inspect it with the doom-runner CLI.`,`Pass background: true for commands you already know will not finish on their own (dev servers, watchers, tails). Waiting the full threshold first wastes a turn.`,`Pass interactive: true only when the command will prompt for confirmation or input. Use Runner Space for terminal input. Interactive logs are noisier, so avoid it otherwise.`,`Do not re-run a command to recover output. Read its saved log with doom-runner logs <id>.`,`Stop runners you no longer need. Every runner is stopped automatically when the session ends.`]}function h(e,t){e.registerTool({name:o,label:a,description:`Execute a bash command in the current working directory. A command that outlives the background threshold keeps running under doom-runner and returns a unique id plus a streaming log path. Completed commands return a bounded log tail with exact file metadata. Optionally provide a timeout in seconds, background: true to background it immediately, or interactive: true if it will prompt.`,promptSnippet:p,promptGuidelines:m(),parameters:s,renderShell:`self`,async execute(e,n,r,a,o){let{command:s,timeout:c,background:l,interactive:f,name:p}=n,m;a&&a(i(`Starting ${f===!0?`interactive runner`:l===!0?`background runner`:`command`}...`)),l!==!0&&f!==!0&&a&&(m=e=>a(i(e)));let h;try{h=await t.bashRunService.run({command:s,timeoutMs:c===void 0?void 0:c*u,background:l,interactive:f,name:p,...m?{onOutput:m}:{},sessionId:t.getSessionId()})}catch(e){let t=e instanceof Error?e.message:String(e);throw Error([`Could not execute the command: ${t}`,``,d,`- Check the command, runtime, and working directory, then retry once.`,`- Use a read-only diagnostic to confirm the missing dependency or environment value.`,`- Ask the user if recovery requires changing the requested approach.`].join(`
|
|
2
|
-
`),{cause:e})}return h.kind===`promoted`&&t.onRunnerStarted(h.id),g(h)},renderCall(e,t,n){return c(e,t)},renderResult(e,t,n,r){return l(e,t,n)}})}function g(e){if(e.kind===`failed`)throw Error([`Could not start runner "${e.name}": ${e.error}`,``,d,`- Correct the command or missing executable, then retry once.`,`- Use a read-only diagnostic to inspect the runtime environment.`,`- Ask the user if a different execution approach is required.`].join(`
|
|
2
|
+
`),{cause:e})}return h.kind===`promoted`&&t.onRunnerStarted(h.id),g(h)},renderCall(e,t,n){return c(e,t)},renderResult(e,t,n,r){return l(e,{...t,isError:r.isError},n)}})}function g(e){if(e.kind===`failed`)throw Error([`Could not start runner "${e.name}": ${e.error}`,``,d,`- Correct the command or missing executable, then retry once.`,`- Use a read-only diagnostic to inspect the runtime environment.`,`- Ask the user if a different execution approach is required.`].join(`
|
|
3
3
|
`));if(e.kind===`promoted`){let t=[`${f[e.reason]} as runner "${e.name}".`,`Runner ID: ${e.id}`,`Backend: ${e.backend}`,`Log (streaming): ${e.logPath}`,`Inspect it with: doom-runner logs ${e.id}`].filter(e=>e.length>0).join(`
|
|
4
4
|
`);return{...i(t,{id:e.id,runner:e.name,pid:e.pid,logPath:e.logPath,promoted:!0,reason:e.reason})}}let a=r(e.logPath),o=[];e.timedOut?o.push(`Command stopped: it exceeded the requested timeout.`):e.exitCode===null?e.signal!==null&&o.push(`Terminated by ${e.signal}`):o.push(`Exit: completed with code ${e.exitCode}.`);let s=t(a.tail),c=[s.length>0?`Log tail (${a.tailLines} lines):\n${s}`:`Log tail: (no output)`,`Runner ID: ${e.id}`,...o,`Log: ${e.logPath}`,`File size: ${n(a.bytes)}`,`Lines: ${a.lines.toLocaleString(`en-US`)}`].join(`
|
|
5
5
|
`),l={id:e.id,runner:e.name,exitCode:e.exitCode,logPath:e.logPath,backend:e.backend,fileSize:a.bytes,lines:a.lines,tail:a.tail,tailLines:a.tailLines,...e.timedOut?{timedOut:!0}:{}};if(e.timedOut||e.signal!==null||e.exitCode!==null&&e.exitCode!==0)throw Error([c,``,d,`- Inspect the full log at ${e.logPath}, correct the cause, then retry once.`,`- Run a read-only diagnostic if the output does not explain the failure.`,`- Ask the user if the failure requires changing the requested approach.`].join(`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bashTool.mjs","names":[],"sources":["../../../src/commands/bash/bashTool.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { BASH_TOOL_LABEL, BASH_TOOL_NAME, type BashParams, BashParamsSchema } from '../../schemas/bashTool.ts';\nimport { stripAnsi } from '../../services/AnsiScrub/index.ts';\nimport type { BashRunResult, IBashRunService } from '../../services/BashRunService/index.ts';\nimport { renderBashCall, renderBashResult } from '../../tui/bashRender.ts';\nimport { getBackgroundThresholdMs } from '../../types/config.ts';\nimport { formatSize, summarizeLog, type ToolResult, textResult } from './responseEnvelope.ts';\n\nconst MS_PER_SECOND = 1000;\nconst ERROR_OPTIONS_HEADING = 'Options:';\n\nconst PROMOTION_REASONS: Record<'requested' | 'threshold' | 'interactive', string> = {\n requested: 'Started in the background',\n threshold: 'Still running after the background threshold, so it was moved to the background',\n interactive: 'Started on a terminal so it can prompt, which means it runs in the background',\n};\n\nexport const BASH_PROMPT_SNIPPET = 'Execute bash commands, with long-running ones supervised in the background';\n\n/** Written at registration time so the stated threshold matches the configured one. */\nexport function bashPromptGuidelines(thresholdMs = getBackgroundThresholdMs()): string[] {\n return [\n `A command still running after ${Math.round(thresholdMs / MS_PER_SECOND)} seconds remains active as a background runner. You get its unique id and streaming log path, then inspect it with the doom-runner CLI.`,\n 'Pass background: true for commands you already know will not finish on their own (dev servers, watchers, tails). Waiting the full threshold first wastes a turn.',\n 'Pass interactive: true only when the command will prompt for confirmation or input. Use Runner Space for terminal input. Interactive logs are noisier, so avoid it otherwise.',\n 'Do not re-run a command to recover output. Read its saved log with doom-runner logs <id>.',\n 'Stop runners you no longer need. Every runner is stopped automatically when the session ends.',\n ];\n}\n\nexport interface BashToolDependencies {\n bashRunService: IBashRunService;\n getSessionId(): string;\n /** Called after a runner is promoted, so UI state can refresh. */\n onRunnerStarted(id: string): void;\n}\n\n/**\n * Registers a tool named `bash`, replacing pi's built-in.\n *\n * The name is deliberate: hooks, guardrails and doom-pi's dispatcher all key on\n * `bash`, and they keep working only while the replacement keeps the name.\n */\nexport function registerBashTool(pi: ExtensionAPI, dependencies: BashToolDependencies): void {\n pi.registerTool({\n name: BASH_TOOL_NAME,\n label: BASH_TOOL_LABEL,\n description:\n 'Execute a bash command in the current working directory. A command that outlives the background threshold keeps running under doom-runner and returns a unique id plus a streaming log path. Completed commands return a bounded log tail with exact file metadata. Optionally provide a timeout in seconds, background: true to background it immediately, or interactive: true if it will prompt.',\n promptSnippet: BASH_PROMPT_SNIPPET,\n promptGuidelines: bashPromptGuidelines(),\n parameters: BashParamsSchema,\n // Bash output already carries its own status glyphs and ANSI colours. Owning\n // the shell keeps Pi from filling every successful command with the global\n // toolSuccessBg, which overwhelms long logs and diffs.\n renderShell: 'self',\n\n async execute(_toolCallId, params, _signal, onUpdate, _ctx): Promise<ToolResult> {\n const { command, timeout, background, interactive, name } = params as BashParams;\n let onOutput: ((output: string) => void) | undefined;\n if (onUpdate) {\n const mode =\n interactive === true ? 'interactive runner' : background === true ? 'background runner' : 'command';\n onUpdate(textResult(`Starting ${mode}...`));\n }\n if (background !== true && interactive !== true && onUpdate) {\n onOutput = (output) => onUpdate(textResult(output));\n }\n let result: BashRunResult;\n try {\n result = await dependencies.bashRunService.run({\n command,\n timeoutMs: timeout === undefined ? undefined : timeout * MS_PER_SECOND,\n background,\n interactive,\n name,\n ...(onOutput ? { onOutput } : {}),\n sessionId: dependencies.getSessionId(),\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n [\n `Could not execute the command: ${message}`,\n '',\n ERROR_OPTIONS_HEADING,\n '- Check the command, runtime, and working directory, then retry once.',\n '- Use a read-only diagnostic to confirm the missing dependency or environment value.',\n '- Ask the user if recovery requires changing the requested approach.',\n ].join('\\n'),\n { cause: error },\n );\n }\n\n if (result.kind === 'promoted') dependencies.onRunnerStarted(result.id);\n return formatRunResult(result);\n },\n\n // Without these, pi falls back to echoing the raw command and the tail end of\n // the result text, which is the metadata footer rather than the log output.\n renderCall(args, theme, _context) {\n return renderBashCall(args as BashParams, theme);\n },\n\n renderResult(result, options, theme, _context) {\n return renderBashResult(result, options, theme);\n },\n });\n}\n\nexport function formatRunResult(result: BashRunResult): ToolResult {\n if (result.kind === 'failed') {\n throw new Error(\n [\n `Could not start runner \"${result.name}\": ${result.error}`,\n '',\n ERROR_OPTIONS_HEADING,\n '- Correct the command or missing executable, then retry once.',\n '- Use a read-only diagnostic to inspect the runtime environment.',\n '- Ask the user if a different execution approach is required.',\n ].join('\\n'),\n );\n }\n\n if (result.kind === 'promoted') {\n const reason = PROMOTION_REASONS[result.reason];\n const body = [\n `${reason} as runner \"${result.name}\".`,\n `Runner ID: ${result.id}`,\n `Backend: ${result.backend}`,\n `Log (streaming): ${result.logPath}`,\n `Inspect it with: doom-runner logs ${result.id}`,\n ]\n .filter((line) => line.length > 0)\n .join('\\n');\n\n return {\n ...textResult(body, {\n id: result.id,\n runner: result.name,\n pid: result.pid,\n logPath: result.logPath,\n promoted: true,\n reason: result.reason,\n }),\n };\n }\n\n const log = summarizeLog(result.logPath);\n const notes: string[] = [];\n if (result.timedOut) notes.push('Command stopped: it exceeded the requested timeout.');\n else if (result.exitCode !== null) notes.push(`Exit: completed with code ${result.exitCode}.`);\n else if (result.signal !== null) notes.push(`Terminated by ${result.signal}`);\n\n // The model reads plain text; only the renderer gets the coloured copy below.\n const plainTail = stripAnsi(log.tail);\n const text = [\n plainTail.length > 0 ? `Log tail (${log.tailLines} lines):\\n${plainTail}` : 'Log tail: (no output)',\n `Runner ID: ${result.id}`,\n ...notes,\n `Log: ${result.logPath}`,\n `File size: ${formatSize(log.bytes)}`,\n `Lines: ${log.lines.toLocaleString('en-US')}`,\n ].join('\\n');\n const details = {\n id: result.id,\n runner: result.name,\n exitCode: result.exitCode,\n logPath: result.logPath,\n backend: result.backend,\n fileSize: log.bytes,\n lines: log.lines,\n // Carried structurally so renderResult shows the output instead of the footer.\n tail: log.tail,\n tailLines: log.tailLines,\n ...(result.timedOut ? { timedOut: true } : {}),\n };\n if (result.timedOut || result.signal !== null || (result.exitCode !== null && result.exitCode !== 0)) {\n throw new Error(\n [\n text,\n '',\n ERROR_OPTIONS_HEADING,\n `- Inspect the full log at ${result.logPath}, correct the cause, then retry once.`,\n '- Run a read-only diagnostic if the output does not explain the failure.',\n '- Ask the user if the failure requires changing the requested approach.',\n ].join('\\n'),\n );\n }\n return textResult(text, details);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bashTool.mjs","names":[],"sources":["../../../src/commands/bash/bashTool.ts"],"sourcesContent":["import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';\nimport { BASH_TOOL_LABEL, BASH_TOOL_NAME, type BashParams, BashParamsSchema } from '../../schemas/bashTool.ts';\nimport { stripAnsi } from '../../services/AnsiScrub/index.ts';\nimport type { BashRunResult, IBashRunService } from '../../services/BashRunService/index.ts';\nimport { renderBashCall, renderBashResult } from '../../tui/bashRender.ts';\nimport { getBackgroundThresholdMs } from '../../types/config.ts';\nimport { formatSize, summarizeLog, type ToolResult, textResult } from './responseEnvelope.ts';\n\nconst MS_PER_SECOND = 1000;\nconst ERROR_OPTIONS_HEADING = 'Options:';\n\nconst PROMOTION_REASONS: Record<'requested' | 'threshold' | 'interactive', string> = {\n requested: 'Started in the background',\n threshold: 'Still running after the background threshold, so it was moved to the background',\n interactive: 'Started on a terminal so it can prompt, which means it runs in the background',\n};\n\nexport const BASH_PROMPT_SNIPPET = 'Execute bash commands, with long-running ones supervised in the background';\n\n/** Written at registration time so the stated threshold matches the configured one. */\nexport function bashPromptGuidelines(thresholdMs = getBackgroundThresholdMs()): string[] {\n return [\n `A command still running after ${Math.round(thresholdMs / MS_PER_SECOND)} seconds remains active as a background runner. You get its unique id and streaming log path, then inspect it with the doom-runner CLI.`,\n 'Pass background: true for commands you already know will not finish on their own (dev servers, watchers, tails). Waiting the full threshold first wastes a turn.',\n 'Pass interactive: true only when the command will prompt for confirmation or input. Use Runner Space for terminal input. Interactive logs are noisier, so avoid it otherwise.',\n 'Do not re-run a command to recover output. Read its saved log with doom-runner logs <id>.',\n 'Stop runners you no longer need. Every runner is stopped automatically when the session ends.',\n ];\n}\n\nexport interface BashToolDependencies {\n bashRunService: IBashRunService;\n getSessionId(): string;\n /** Called after a runner is promoted, so UI state can refresh. */\n onRunnerStarted(id: string): void;\n}\n\n/**\n * Registers a tool named `bash`, replacing pi's built-in.\n *\n * The name is deliberate: hooks, guardrails and doom-pi's dispatcher all key on\n * `bash`, and they keep working only while the replacement keeps the name.\n */\nexport function registerBashTool(pi: ExtensionAPI, dependencies: BashToolDependencies): void {\n pi.registerTool({\n name: BASH_TOOL_NAME,\n label: BASH_TOOL_LABEL,\n description:\n 'Execute a bash command in the current working directory. A command that outlives the background threshold keeps running under doom-runner and returns a unique id plus a streaming log path. Completed commands return a bounded log tail with exact file metadata. Optionally provide a timeout in seconds, background: true to background it immediately, or interactive: true if it will prompt.',\n promptSnippet: BASH_PROMPT_SNIPPET,\n promptGuidelines: bashPromptGuidelines(),\n parameters: BashParamsSchema,\n // Bash output already carries its own status glyphs and ANSI colours. Owning\n // the shell keeps Pi from filling every successful command with the global\n // toolSuccessBg, which overwhelms long logs and diffs.\n renderShell: 'self',\n\n async execute(_toolCallId, params, _signal, onUpdate, _ctx): Promise<ToolResult> {\n const { command, timeout, background, interactive, name } = params as BashParams;\n let onOutput: ((output: string) => void) | undefined;\n if (onUpdate) {\n const mode =\n interactive === true ? 'interactive runner' : background === true ? 'background runner' : 'command';\n onUpdate(textResult(`Starting ${mode}...`));\n }\n if (background !== true && interactive !== true && onUpdate) {\n onOutput = (output) => onUpdate(textResult(output));\n }\n let result: BashRunResult;\n try {\n result = await dependencies.bashRunService.run({\n command,\n timeoutMs: timeout === undefined ? undefined : timeout * MS_PER_SECOND,\n background,\n interactive,\n name,\n ...(onOutput ? { onOutput } : {}),\n sessionId: dependencies.getSessionId(),\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n [\n `Could not execute the command: ${message}`,\n '',\n ERROR_OPTIONS_HEADING,\n '- Check the command, runtime, and working directory, then retry once.',\n '- Use a read-only diagnostic to confirm the missing dependency or environment value.',\n '- Ask the user if recovery requires changing the requested approach.',\n ].join('\\n'),\n { cause: error },\n );\n }\n\n if (result.kind === 'promoted') dependencies.onRunnerStarted(result.id);\n return formatRunResult(result);\n },\n\n // Without these, pi falls back to echoing the raw command and the tail end of\n // the result text, which is the metadata footer rather than the log output.\n renderCall(args, theme, _context) {\n return renderBashCall(args as BashParams, theme);\n },\n\n renderResult(result, options, theme, context) {\n return renderBashResult(result, { ...options, isError: context.isError }, theme);\n },\n });\n}\n\nexport function formatRunResult(result: BashRunResult): ToolResult {\n if (result.kind === 'failed') {\n throw new Error(\n [\n `Could not start runner \"${result.name}\": ${result.error}`,\n '',\n ERROR_OPTIONS_HEADING,\n '- Correct the command or missing executable, then retry once.',\n '- Use a read-only diagnostic to inspect the runtime environment.',\n '- Ask the user if a different execution approach is required.',\n ].join('\\n'),\n );\n }\n\n if (result.kind === 'promoted') {\n const reason = PROMOTION_REASONS[result.reason];\n const body = [\n `${reason} as runner \"${result.name}\".`,\n `Runner ID: ${result.id}`,\n `Backend: ${result.backend}`,\n `Log (streaming): ${result.logPath}`,\n `Inspect it with: doom-runner logs ${result.id}`,\n ]\n .filter((line) => line.length > 0)\n .join('\\n');\n\n return {\n ...textResult(body, {\n id: result.id,\n runner: result.name,\n pid: result.pid,\n logPath: result.logPath,\n promoted: true,\n reason: result.reason,\n }),\n };\n }\n\n const log = summarizeLog(result.logPath);\n const notes: string[] = [];\n if (result.timedOut) notes.push('Command stopped: it exceeded the requested timeout.');\n else if (result.exitCode !== null) notes.push(`Exit: completed with code ${result.exitCode}.`);\n else if (result.signal !== null) notes.push(`Terminated by ${result.signal}`);\n\n // The model reads plain text; only the renderer gets the coloured copy below.\n const plainTail = stripAnsi(log.tail);\n const text = [\n plainTail.length > 0 ? `Log tail (${log.tailLines} lines):\\n${plainTail}` : 'Log tail: (no output)',\n `Runner ID: ${result.id}`,\n ...notes,\n `Log: ${result.logPath}`,\n `File size: ${formatSize(log.bytes)}`,\n `Lines: ${log.lines.toLocaleString('en-US')}`,\n ].join('\\n');\n const details = {\n id: result.id,\n runner: result.name,\n exitCode: result.exitCode,\n logPath: result.logPath,\n backend: result.backend,\n fileSize: log.bytes,\n lines: log.lines,\n // Carried structurally so renderResult shows the output instead of the footer.\n tail: log.tail,\n tailLines: log.tailLines,\n ...(result.timedOut ? { timedOut: true } : {}),\n };\n if (result.timedOut || result.signal !== null || (result.exitCode !== null && result.exitCode !== 0)) {\n throw new Error(\n [\n text,\n '',\n ERROR_OPTIONS_HEADING,\n `- Inspect the full log at ${result.logPath}, correct the cause, then retry once.`,\n '- Run a read-only diagnostic if the output does not explain the failure.',\n '- Ask the user if the failure requires changing the requested approach.',\n ].join('\\n'),\n );\n }\n return textResult(text, details);\n}\n"],"mappings":"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"}
|
package/dist/tui/bashRender.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("../_virtual/_rolldown/runtime.cjs"),t=require("../commands/bash/responseEnvelope.cjs");let n=require("node:os");n=e.__toESM(n,1);let r=require("@agimon-ai/doompi-ui/toolChrome"),i=require("@earendil-works/pi-coding-agent"),a=require("@earendil-works/pi-tui");var o=class{lines;layout;constructor(e,t){this.lines=e,this.layout=t}render(e){if(e<=0)return[];let t=Math.max(1,e-2),n=this.layout.wrap?this.lines.flatMap(e=>(0,a.wrapTextWithAnsi)(e,t)):this.lines.map(e=>(0,a.truncateToWidth)(e,t,`…`));this.layout.separator&&n.unshift(this.layout.separator(`─`.repeat(t)));let r=n.map(e=>` ${e}`);return this.layout.trailingBlank&&r.push(``),r}invalidate(){}};function s(e,t,n){return new o(e,{wrap:t,separator:e=>n.fg(`borderMuted`,e),trailingBlank:!0})}const c=/\u001b\[[0-9;]*m/;function l(e,t,n){return c.test(e)?e:n.fg(t,e)}function u(e,t=n.default.homedir()){return t.length>0?e.split(t).join(`~`):e}function d(e){let t=e.split(`
|
|
2
2
|
`).map(e=>e.trim()).filter(e=>e.length>0),n=t[0]??``;return t.length>1?`${n} … +${t.length-1} lines`:n}function f(e,t=160){if(e.length<=t)return e;let n=t-3,r=Math.ceil(n/2),i=e.slice(0,r),a=e.slice(e.length-(n-r)),o=i.lastIndexOf(` `);o>i.length/2&&(i=i.slice(0,o));let s=a.indexOf(` `);return s>=0&&s<a.length/2&&(a=a.slice(s+1)),`${i} … ${a}`}function p(e){return u(d(e))}function m(e){let t=[];return e.background===!0&&t.push(`bg`),e.interactive===!0&&t.push(`tty`),e.alarm!==void 0&&t.push(`alarm ${e.alarm}s`),e.timeout!==void 0&&t.push(`${e.timeout}s`),e.name!==void 0&&e.name.length>0&&t.push(e.name),t}function h(e,t,n=i.highlightCode){let a=n(p(e.command),`bash`).join(` `),o=`${(0,r.renderToolBadge)(`bash`,t)} ${a}`,s=m(e);return s.length>0&&(o+=` ${t.fg(`muted`,`· ${s.join(` · `)}`)}`),new r.DoomToolCall(o)}function g(e){let n=[];return e.lines!==void 0&&n.push(`${e.lines.toLocaleString(`en-US`)} lines`),e.fileSize!==void 0&&n.push(t.formatSize(e.fileSize)),e.runner!==void 0&&n.push(e.runner),n.join(` · `)}function _(e,t){let n=(e.tail??``).split(`
|
|
3
3
|
`);for(;n.length>0&&n[n.length-1]?.trim()===``;)n.pop();return t||n.length<=12?n:n.slice(-12)}function v(e){return(e.content??[]).map(e=>e.text??``).join(``)}function y(e,t){let n=e.split(`
|
|
4
|
-
`);for(;n.length>0&&n[n.length-1]?.trim()===``;)n.pop();return n.length<=t?n:n.slice(-t)}function b(e,t,n){let r=e.details??{};if(t.isPartial){let t=y(v(e),12),r=n.fg(`warning`,`◐`)+n.fg(`dim`,` running`);return s([...t.map(e=>l(e,`toolOutput`,n)),r],!1,n)}if(
|
|
4
|
+
`);for(;n.length>0&&n[n.length-1]?.trim()===``;)n.pop();return n.length<=t?n:n.slice(-t)}function b(e,t,n){let r=e.details??{};if(t.isPartial){let t=y(v(e),12),r=n.fg(`warning`,`◐`)+n.fg(`dim`,` running`);return s([...t.map(e=>l(e,`toolOutput`,n)),r],!1,n)}if(t.isError===!0){let i=y(v(e),t.expanded?2**53-1:12),a=g(r),o=a.length>0?` ${a}`:` failed`;return s([...i.map(e=>l(e,`toolOutput`,n)),n.fg(`error`,`✗`)+n.fg(`dim`,o)],t.expanded,n)}if(r.promoted===!0){let e=r.runner??r.id??`runner`,t=r.id===void 0?``:` · ${r.id}`;return s([n.fg(`accent`,`● ${e}`)+n.fg(`muted`,`${t} · background`)],!1,n)}let i=_(r,t.expanded),a=r.exitCode!==void 0&&r.exitCode!==null,o=r.timedOut===!0||a&&r.exitCode!==0,c=o?n.fg(`error`,`✗`):n.fg(`success`,`✓`),u=[];r.timedOut===!0?u.push(`timed out`):o&&u.push(`exit ${r.exitCode}`);let d=g(r);d.length>0&&u.push(d),!t.expanded&&(r.tailLines??0)>i.length&&u.push(`ctrl+o`);let f=i.map(e=>l(e,`toolOutput`,n)),p=u.length>0?` ${u.join(` · `)}`:o?` failed`:i.length===0?` done`:``;return p.length>0&&f.push(c+n.fg(`dim`,p)),s(f,t.expanded,n)}exports.abbreviateHome=u,exports.collapseCommand=d,exports.formatBashCommand=p,exports.formatBashFlags=m,exports.formatResultSummary=g,exports.renderBashCall=h,exports.renderBashResult=b,exports.truncateMiddle=f;
|
|
5
5
|
//# sourceMappingURL=bashRender.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bashRender.cjs","names":["wrapTextWithAnsi","truncateToWidth","os","highlightCode","renderToolBadge","DoomToolCall","formatSize"],"sources":["../../src/tui/bashRender.ts"],"sourcesContent":["import os from 'node:os';\nimport { DoomToolCall, renderToolBadge } from '@agimon-ai/doompi-ui/toolChrome';\nimport { highlightCode, type Theme, type ThemeColor } from '@earendil-works/pi-coding-agent';\nimport { type Component, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';\nimport { formatSize } from '../commands/bash/responseEnvelope.ts';\nimport type { BashParams } from '../schemas/bashTool.ts';\n\nconst ELLIPSIS = '…';\n/** Spaced so the join never glues two words into one unreadable token. */\nconst GAP = ' … ';\n/** Wide enough to keep a realistic command intact, short enough to stay under three wrapped rows. */\nconst MAX_COMMAND_LENGTH = 160;\n\n/**\n * Syntax highlighter for the command line.\n *\n * `highlightCode` reads Pi's global theme singleton rather than the theme passed\n * to a renderer, so it is injected here to keep these functions testable.\n */\nexport type Highlighter = (code: string, lang?: string) => string[];\n/** Lines of log kept in the collapsed result, before `ctrl+o` reveals the rest. */\nconst COLLAPSED_TAIL_LINES = 12;\n/** Live output is unbounded while a command runs, so the running state keeps only a window. */\nconst STREAM_TAIL_LINES = 12;\n\n/**\n * Width-aware tool text.\n *\n * Pi's Text component wraps every long line. That is useful for prose, but it\n * makes shell pipelines and grep output consume several visual rows and blurs\n * the boundary between separate log lines. Collapsed tool output is instead\n * clipped per logical line; expanded output keeps all content by wrapping it.\n */\ninterface BashToolTextLayout {\n wrap: boolean;\n separator?: (line: string) => string;\n trailingBlank?: boolean;\n}\n\nclass BashToolText implements Component {\n constructor(\n private readonly lines: readonly string[],\n private readonly layout: BashToolTextLayout,\n ) {}\n\n render(width: number): string[] {\n if (width <= 0) return [];\n const contentWidth = Math.max(1, width - 2);\n const rendered = this.layout.wrap\n ? this.lines.flatMap((line) => wrapTextWithAnsi(line, contentWidth))\n : this.lines.map((line) => truncateToWidth(line, contentWidth, ELLIPSIS));\n if (this.layout.separator) rendered.unshift(this.layout.separator('─'.repeat(contentWidth)));\n const padded = rendered.map((line) => ` ${line}`);\n if (this.layout.trailingBlank) padded.push('');\n return padded;\n }\n\n invalidate(): void {\n // Content is immutable and rendering reads the current width every time.\n }\n}\n\nfunction renderResultText(lines: readonly string[], wrap: boolean, theme: Theme): Component {\n return new BashToolText(lines, {\n wrap,\n // State belongs in the result summary below; keep this divider structural.\n separator: (line) => theme.fg('borderMuted', line),\n trailingBlank: true,\n });\n}\n\n/** An SGR colour sequence, the only escape the log pipeline is allowed to keep. */\n/* oxlint-disable no-control-regex -- matching SGR requires the ESC control character */\n// biome-ignore lint/suspicious/noControlCharactersInRegex: Matching SGR requires the ESC control character.\nconst SGR_PATTERN = /\\u001b\\[[0-9;]*m/;\n/* oxlint-enable no-control-regex */\n\n/**\n * Applies a theme colour only to lines that carry none of their own.\n *\n * A command that emits colour has already said how its output should look, and\n * wrapping it would reset to the terminal default at the first embedded escape.\n */\nfunction paint(line: string, color: ThemeColor, theme: Theme): string {\n return SGR_PATTERN.test(line) ? line : theme.fg(color, line);\n}\n\n/** Structured view of what `formatRunResult` reports, read only by the renderers. */\nexport interface BashResultDetails {\n id?: string;\n runner?: string;\n exitCode?: number | null;\n logPath?: string;\n fileSize?: number;\n lines?: number;\n timedOut?: boolean;\n promoted?: boolean;\n reason?: string;\n tail?: string;\n tailLines?: number;\n}\n\n/** `/Users/me/x` becomes `~/x`. Absolute home paths are what push these lines onto extra rows. */\nexport function abbreviateHome(text: string, home: string = os.homedir()): string {\n return home.length > 0 ? text.split(home).join('~') : text;\n}\n\n/** Collapse a heredoc or multi-line script to one scannable line plus a count. */\nexport function collapseCommand(command: string): string {\n const lines = command\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n const first = lines[0] ?? '';\n return lines.length > 1 ? `${first} ${ELLIPSIS} +${lines.length - 1} lines` : first;\n}\n\n/** Drop the middle rather than the end, so a long pipeline keeps both its source and its sink. */\nexport function truncateMiddle(text: string, max: number = MAX_COMMAND_LENGTH): string {\n if (text.length <= max) return text;\n const keep = max - GAP.length;\n const head = Math.ceil(keep / 2);\n let start = text.slice(0, head);\n let end = text.slice(text.length - (keep - head));\n\n // Snap both cuts to a token boundary, so neither side ends mid-word. Each is\n // only moved when a boundary exists in the half nearest the cut, otherwise a\n // single long argument would swallow the whole budget.\n const lastSpace = start.lastIndexOf(' ');\n if (lastSpace > start.length / 2) start = start.slice(0, lastSpace);\n const firstSpace = end.indexOf(' ');\n if (firstSpace >= 0 && firstSpace < end.length / 2) end = end.slice(firstSpace + 1);\n\n return `${start}${GAP}${end}`;\n}\n\nexport function formatBashCommand(command: string): string {\n return abbreviateHome(collapseCommand(command));\n}\n\n/** Modifiers worth seeing at a glance, in the widget's `·` idiom. */\nexport function formatBashFlags(args: BashParams): string[] {\n const flags: string[] = [];\n if (args.background === true) flags.push('bg');\n if (args.interactive === true) flags.push('tty');\n if (args.alarm !== undefined) flags.push(`alarm ${args.alarm}s`);\n if (args.timeout !== undefined) flags.push(`${args.timeout}s`);\n if (args.name !== undefined && args.name.length > 0) flags.push(args.name);\n return flags;\n}\n\nexport function renderBashCall(args: BashParams, theme: Theme, highlight: Highlighter = highlightCode): Component {\n // Highlight after collapsing multi-line scripts: ANSI codes would break that transformation.\n const command = highlight(formatBashCommand(args.command), 'bash').join(' ');\n let text = `${renderToolBadge('bash', theme)} ${command}`;\n const flags = formatBashFlags(args);\n if (flags.length > 0) text += ` ${theme.fg('muted', `· ${flags.join(' · ')}`)}`;\n // A command is operator input, not log output: wrap it so the complete\n // pipeline remains inspectable instead of clipping its most important tail.\n return new DoomToolCall(text);\n}\n\n/** The metadata footer, collapsed from five lines to one. */\nexport function formatResultSummary(details: BashResultDetails): string {\n const parts: string[] = [];\n if (details.lines !== undefined) parts.push(`${details.lines.toLocaleString('en-US')} lines`);\n if (details.fileSize !== undefined) parts.push(formatSize(details.fileSize));\n if (details.runner !== undefined) parts.push(details.runner);\n return parts.join(' · ');\n}\n\nfunction tailLines(details: BashResultDetails, expanded: boolean): string[] {\n const lines = (details.tail ?? '').split('\\n');\n while (lines.length > 0 && lines[lines.length - 1]?.trim() === '') lines.pop();\n if (expanded || lines.length <= COLLAPSED_TAIL_LINES) return lines;\n return lines.slice(-COLLAPSED_TAIL_LINES);\n}\n\n/** Concatenated text of a tool result, which is all a thrown failure leaves behind. */\nfunction contentText(result: { content?: Array<{ type: string; text?: string }> }): string {\n return (result.content ?? []).map((block) => block.text ?? '').join('');\n}\n\n/** Last `max` lines, so a chatty command cannot push the transcript off screen. */\nfunction lastLines(text: string, max: number): string[] {\n const all = text.split('\\n');\n while (all.length > 0 && all[all.length - 1]?.trim() === '') all.pop();\n return all.length <= max ? all : all.slice(-max);\n}\n\nexport function renderBashResult(\n result: { content?: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },\n options: { expanded: boolean; isPartial: boolean },\n theme: Theme,\n): Component {\n const details = (result.details ?? {}) as BashResultDetails;\n\n // Running. Details do not exist yet, so the streamed text is the only source,\n // and it is bounded here because it grows without limit while the command runs.\n if (options.isPartial) {\n const streamed = lastLines(contentText(result), STREAM_TAIL_LINES);\n const label = theme.fg('warning', '◐') + theme.fg('dim', ' running');\n return renderResultText([...streamed.map((line) => paint(line, 'toolOutput', theme)), label], false, theme);\n }\n\n // Failed. formatRunResult throws on a bad exit, so Pi hands back the error\n // text with no details at all; without this the block would render a bare tick.\n if (result.isError === true) {\n const body = lastLines(contentText(result), options.expanded ? Number.MAX_SAFE_INTEGER : COLLAPSED_TAIL_LINES);\n const summary = formatResultSummary(details);\n const meta = summary.length > 0 ? ` ${summary}` : ' failed';\n return renderResultText(\n [...body.map((line) => paint(line, 'toolOutput', theme)), theme.fg('error', '✗') + theme.fg('dim', meta)],\n options.expanded,\n theme,\n );\n }\n\n if (details.promoted === true) {\n const label = details.runner ?? details.id ?? 'runner';\n const suffix = details.id === undefined ? '' : ` · ${details.id}`;\n return renderResultText(\n [theme.fg('accent', `● ${label}`) + theme.fg('muted', `${suffix} · background`)],\n false,\n theme,\n );\n }\n\n const body = tailLines(details, options.expanded);\n // An absent exit code means the runner never reported one, which is not a failure.\n const exited = details.exitCode !== undefined && details.exitCode !== null;\n const failed = details.timedOut === true || (exited && details.exitCode !== 0);\n const glyph = failed ? theme.fg('error', '✗') : theme.fg('success', '✓');\n\n const meta: string[] = [];\n if (details.timedOut === true) meta.push('timed out');\n else if (failed) meta.push(`exit ${details.exitCode}`);\n const summary = formatResultSummary(details);\n if (summary.length > 0) meta.push(summary);\n if (!options.expanded && (details.tailLines ?? 0) > body.length) meta.push('ctrl+o');\n\n const lines = body.map((line) => paint(line, 'toolOutput', theme));\n // `dim`, not `muted`: muted sits too close to toolOutput to read as chrome.\n const status = meta.length > 0 ? ` ${meta.join(' · ')}` : failed ? ' failed' : body.length === 0 ? ' done' : '';\n if (status.length > 0) lines.push(glyph + theme.fg('dim', status));\n return renderResultText(lines, options.expanded, theme);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bashRender.cjs","names":["wrapTextWithAnsi","truncateToWidth","os","highlightCode","renderToolBadge","DoomToolCall","formatSize"],"sources":["../../src/tui/bashRender.ts"],"sourcesContent":["import os from 'node:os';\nimport { DoomToolCall, renderToolBadge } from '@agimon-ai/doompi-ui/toolChrome';\nimport { highlightCode, type Theme, type ThemeColor } from '@earendil-works/pi-coding-agent';\nimport { type Component, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';\nimport { formatSize } from '../commands/bash/responseEnvelope.ts';\nimport type { BashParams } from '../schemas/bashTool.ts';\n\nconst ELLIPSIS = '…';\n/** Spaced so the join never glues two words into one unreadable token. */\nconst GAP = ' … ';\n/** Wide enough to keep a realistic command intact, short enough to stay under three wrapped rows. */\nconst MAX_COMMAND_LENGTH = 160;\n\n/**\n * Syntax highlighter for the command line.\n *\n * `highlightCode` reads Pi's global theme singleton rather than the theme passed\n * to a renderer, so it is injected here to keep these functions testable.\n */\nexport type Highlighter = (code: string, lang?: string) => string[];\n/** Lines of log kept in the collapsed result, before `ctrl+o` reveals the rest. */\nconst COLLAPSED_TAIL_LINES = 12;\n/** Live output is unbounded while a command runs, so the running state keeps only a window. */\nconst STREAM_TAIL_LINES = 12;\n\n/**\n * Width-aware tool text.\n *\n * Pi's Text component wraps every long line. That is useful for prose, but it\n * makes shell pipelines and grep output consume several visual rows and blurs\n * the boundary between separate log lines. Collapsed tool output is instead\n * clipped per logical line; expanded output keeps all content by wrapping it.\n */\ninterface BashToolTextLayout {\n wrap: boolean;\n separator?: (line: string) => string;\n trailingBlank?: boolean;\n}\n\nclass BashToolText implements Component {\n constructor(\n private readonly lines: readonly string[],\n private readonly layout: BashToolTextLayout,\n ) {}\n\n render(width: number): string[] {\n if (width <= 0) return [];\n const contentWidth = Math.max(1, width - 2);\n const rendered = this.layout.wrap\n ? this.lines.flatMap((line) => wrapTextWithAnsi(line, contentWidth))\n : this.lines.map((line) => truncateToWidth(line, contentWidth, ELLIPSIS));\n if (this.layout.separator) rendered.unshift(this.layout.separator('─'.repeat(contentWidth)));\n const padded = rendered.map((line) => ` ${line}`);\n if (this.layout.trailingBlank) padded.push('');\n return padded;\n }\n\n invalidate(): void {\n // Content is immutable and rendering reads the current width every time.\n }\n}\n\nfunction renderResultText(lines: readonly string[], wrap: boolean, theme: Theme): Component {\n return new BashToolText(lines, {\n wrap,\n // State belongs in the result summary below; keep this divider structural.\n separator: (line) => theme.fg('borderMuted', line),\n trailingBlank: true,\n });\n}\n\n/** An SGR colour sequence, the only escape the log pipeline is allowed to keep. */\n/* oxlint-disable no-control-regex -- matching SGR requires the ESC control character */\n// biome-ignore lint/suspicious/noControlCharactersInRegex: Matching SGR requires the ESC control character.\nconst SGR_PATTERN = /\\u001b\\[[0-9;]*m/;\n/* oxlint-enable no-control-regex */\n\n/**\n * Applies a theme colour only to lines that carry none of their own.\n *\n * A command that emits colour has already said how its output should look, and\n * wrapping it would reset to the terminal default at the first embedded escape.\n */\nfunction paint(line: string, color: ThemeColor, theme: Theme): string {\n return SGR_PATTERN.test(line) ? line : theme.fg(color, line);\n}\n\n/** Structured view of what `formatRunResult` reports, read only by the renderers. */\nexport interface BashResultDetails {\n id?: string;\n runner?: string;\n exitCode?: number | null;\n logPath?: string;\n fileSize?: number;\n lines?: number;\n timedOut?: boolean;\n promoted?: boolean;\n reason?: string;\n tail?: string;\n tailLines?: number;\n}\n\n/** `/Users/me/x` becomes `~/x`. Absolute home paths are what push these lines onto extra rows. */\nexport function abbreviateHome(text: string, home: string = os.homedir()): string {\n return home.length > 0 ? text.split(home).join('~') : text;\n}\n\n/** Collapse a heredoc or multi-line script to one scannable line plus a count. */\nexport function collapseCommand(command: string): string {\n const lines = command\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n const first = lines[0] ?? '';\n return lines.length > 1 ? `${first} ${ELLIPSIS} +${lines.length - 1} lines` : first;\n}\n\n/** Drop the middle rather than the end, so a long pipeline keeps both its source and its sink. */\nexport function truncateMiddle(text: string, max: number = MAX_COMMAND_LENGTH): string {\n if (text.length <= max) return text;\n const keep = max - GAP.length;\n const head = Math.ceil(keep / 2);\n let start = text.slice(0, head);\n let end = text.slice(text.length - (keep - head));\n\n // Snap both cuts to a token boundary, so neither side ends mid-word. Each is\n // only moved when a boundary exists in the half nearest the cut, otherwise a\n // single long argument would swallow the whole budget.\n const lastSpace = start.lastIndexOf(' ');\n if (lastSpace > start.length / 2) start = start.slice(0, lastSpace);\n const firstSpace = end.indexOf(' ');\n if (firstSpace >= 0 && firstSpace < end.length / 2) end = end.slice(firstSpace + 1);\n\n return `${start}${GAP}${end}`;\n}\n\nexport function formatBashCommand(command: string): string {\n return abbreviateHome(collapseCommand(command));\n}\n\n/** Modifiers worth seeing at a glance, in the widget's `·` idiom. */\nexport function formatBashFlags(args: BashParams): string[] {\n const flags: string[] = [];\n if (args.background === true) flags.push('bg');\n if (args.interactive === true) flags.push('tty');\n if (args.alarm !== undefined) flags.push(`alarm ${args.alarm}s`);\n if (args.timeout !== undefined) flags.push(`${args.timeout}s`);\n if (args.name !== undefined && args.name.length > 0) flags.push(args.name);\n return flags;\n}\n\nexport function renderBashCall(args: BashParams, theme: Theme, highlight: Highlighter = highlightCode): Component {\n // Highlight after collapsing multi-line scripts: ANSI codes would break that transformation.\n const command = highlight(formatBashCommand(args.command), 'bash').join(' ');\n let text = `${renderToolBadge('bash', theme)} ${command}`;\n const flags = formatBashFlags(args);\n if (flags.length > 0) text += ` ${theme.fg('muted', `· ${flags.join(' · ')}`)}`;\n // A command is operator input, not log output: wrap it so the complete\n // pipeline remains inspectable instead of clipping its most important tail.\n return new DoomToolCall(text);\n}\n\n/** The metadata footer, collapsed from five lines to one. */\nexport function formatResultSummary(details: BashResultDetails): string {\n const parts: string[] = [];\n if (details.lines !== undefined) parts.push(`${details.lines.toLocaleString('en-US')} lines`);\n if (details.fileSize !== undefined) parts.push(formatSize(details.fileSize));\n if (details.runner !== undefined) parts.push(details.runner);\n return parts.join(' · ');\n}\n\nfunction tailLines(details: BashResultDetails, expanded: boolean): string[] {\n const lines = (details.tail ?? '').split('\\n');\n while (lines.length > 0 && lines[lines.length - 1]?.trim() === '') lines.pop();\n if (expanded || lines.length <= COLLAPSED_TAIL_LINES) return lines;\n return lines.slice(-COLLAPSED_TAIL_LINES);\n}\n\n/** Concatenated text of a tool result, which is all a thrown failure leaves behind. */\nfunction contentText(result: { content?: Array<{ type: string; text?: string }> }): string {\n return (result.content ?? []).map((block) => block.text ?? '').join('');\n}\n\n/** Last `max` lines, so a chatty command cannot push the transcript off screen. */\nfunction lastLines(text: string, max: number): string[] {\n const all = text.split('\\n');\n while (all.length > 0 && all[all.length - 1]?.trim() === '') all.pop();\n return all.length <= max ? all : all.slice(-max);\n}\n\nexport function renderBashResult(\n result: { content?: Array<{ type: string; text?: string }>; details?: unknown },\n options: { expanded: boolean; isPartial: boolean; isError?: boolean },\n theme: Theme,\n): Component {\n const details = (result.details ?? {}) as BashResultDetails;\n\n // Running. Details do not exist yet, so the streamed text is the only source,\n // and it is bounded here because it grows without limit while the command runs.\n if (options.isPartial) {\n const streamed = lastLines(contentText(result), STREAM_TAIL_LINES);\n const label = theme.fg('warning', '◐') + theme.fg('dim', ' running');\n return renderResultText([...streamed.map((line) => paint(line, 'toolOutput', theme)), label], false, theme);\n }\n\n // Failed. formatRunResult throws on a bad exit, so Pi hands back the error\n // text with no details at all; without this the block would render a bare tick.\n if (options.isError === true) {\n const body = lastLines(contentText(result), options.expanded ? Number.MAX_SAFE_INTEGER : COLLAPSED_TAIL_LINES);\n const summary = formatResultSummary(details);\n const meta = summary.length > 0 ? ` ${summary}` : ' failed';\n return renderResultText(\n [...body.map((line) => paint(line, 'toolOutput', theme)), theme.fg('error', '✗') + theme.fg('dim', meta)],\n options.expanded,\n theme,\n );\n }\n\n if (details.promoted === true) {\n const label = details.runner ?? details.id ?? 'runner';\n const suffix = details.id === undefined ? '' : ` · ${details.id}`;\n return renderResultText(\n [theme.fg('accent', `● ${label}`) + theme.fg('muted', `${suffix} · background`)],\n false,\n theme,\n );\n }\n\n const body = tailLines(details, options.expanded);\n // An absent exit code means the runner never reported one, which is not a failure.\n const exited = details.exitCode !== undefined && details.exitCode !== null;\n const failed = details.timedOut === true || (exited && details.exitCode !== 0);\n const glyph = failed ? theme.fg('error', '✗') : theme.fg('success', '✓');\n\n const meta: string[] = [];\n if (details.timedOut === true) meta.push('timed out');\n else if (failed) meta.push(`exit ${details.exitCode}`);\n const summary = formatResultSummary(details);\n if (summary.length > 0) meta.push(summary);\n if (!options.expanded && (details.tailLines ?? 0) > body.length) meta.push('ctrl+o');\n\n const lines = body.map((line) => paint(line, 'toolOutput', theme));\n // `dim`, not `muted`: muted sits too close to toolOutput to read as chrome.\n const status = meta.length > 0 ? ` ${meta.join(' · ')}` : failed ? ' failed' : body.length === 0 ? ' done' : '';\n if (status.length > 0) lines.push(glyph + theme.fg('dim', status));\n return renderResultText(lines, options.expanded, theme);\n}\n"],"mappings":"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"}
|
|
@@ -41,10 +41,10 @@ declare function renderBashResult(result: {
|
|
|
41
41
|
text?: string;
|
|
42
42
|
}>;
|
|
43
43
|
details?: unknown;
|
|
44
|
-
isError?: boolean;
|
|
45
44
|
}, options: {
|
|
46
45
|
expanded: boolean;
|
|
47
46
|
isPartial: boolean;
|
|
47
|
+
isError?: boolean;
|
|
48
48
|
}, theme: Theme): Component;
|
|
49
49
|
//#endregion
|
|
50
50
|
export { BashResultDetails, Highlighter, abbreviateHome, collapseCommand, formatBashCommand, formatBashFlags, formatResultSummary, renderBashCall, renderBashResult, truncateMiddle };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bashRender.d.cts","names":[],"sources":["../../src/tui/bashRender.ts"],"mappings":";;;;;;;;;;KAmBY,eAAe,cAAc;;UAqExB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;iBAIc,eAAe,cAAc;;iBAK7B,gBAAgB;;iBAUhB,eAAe,cAAc;iBAkB7B,kBAAkB;;iBAKlB,gBAAgB,MAAM;iBAUtB,eAAe,MAAM,YAAY,OAAO,OAAO,YAAW,cAA8B;;iBAYxF,oBAAoB,SAAS;iBA2B7B,iBACd;EAAU,UAAU;IAAQ;IAAc;;EAAkB;
|
|
1
|
+
{"version":3,"file":"bashRender.d.cts","names":[],"sources":["../../src/tui/bashRender.ts"],"mappings":";;;;;;;;;;KAmBY,eAAe,cAAc;;UAqExB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;iBAIc,eAAe,cAAc;;iBAK7B,gBAAgB;;iBAUhB,eAAe,cAAc;iBAkB7B,kBAAkB;;iBAKlB,gBAAgB,MAAM;iBAUtB,eAAe,MAAM,YAAY,OAAO,OAAO,YAAW,cAA8B;;iBAYxF,oBAAoB,SAAS;iBA2B7B,iBACd;EAAU,UAAU;IAAQ;IAAc;;EAAkB;GAC5D;EAAW;EAAmB;EAAoB;GAClD,OAAO,QACN"}
|
|
@@ -41,10 +41,10 @@ declare function renderBashResult(result: {
|
|
|
41
41
|
text?: string;
|
|
42
42
|
}>;
|
|
43
43
|
details?: unknown;
|
|
44
|
-
isError?: boolean;
|
|
45
44
|
}, options: {
|
|
46
45
|
expanded: boolean;
|
|
47
46
|
isPartial: boolean;
|
|
47
|
+
isError?: boolean;
|
|
48
48
|
}, theme: Theme): Component;
|
|
49
49
|
//#endregion
|
|
50
50
|
export { BashResultDetails, Highlighter, abbreviateHome, collapseCommand, formatBashCommand, formatBashFlags, formatResultSummary, renderBashCall, renderBashResult, truncateMiddle };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bashRender.d.mts","names":[],"sources":["../../src/tui/bashRender.ts"],"mappings":";;;;;;;;;;KAmBY,eAAe,cAAc;;UAqExB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;iBAIc,eAAe,cAAc;;iBAK7B,gBAAgB;;iBAUhB,eAAe,cAAc;iBAkB7B,kBAAkB;;iBAKlB,gBAAgB,MAAM;iBAUtB,eAAe,MAAM,YAAY,OAAO,OAAO,YAAW,cAA8B;;iBAYxF,oBAAoB,SAAS;iBA2B7B,iBACd;EAAU,UAAU;IAAQ;IAAc;;EAAkB;
|
|
1
|
+
{"version":3,"file":"bashRender.d.mts","names":[],"sources":["../../src/tui/bashRender.ts"],"mappings":";;;;;;;;;;KAmBY,eAAe,cAAc;;UAqExB;EACf;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;iBAIc,eAAe,cAAc;;iBAK7B,gBAAgB;;iBAUhB,eAAe,cAAc;iBAkB7B,kBAAkB;;iBAKlB,gBAAgB,MAAM;iBAUtB,eAAe,MAAM,YAAY,OAAO,OAAO,YAAW,cAA8B;;iBAYxF,oBAAoB,SAAS;iBA2B7B,iBACd;EAAU,UAAU;IAAQ;IAAc;;EAAkB;GAC5D;EAAW;EAAmB;EAAoB;GAClD,OAAO,QACN"}
|
package/dist/tui/bashRender.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import{formatSize as e}from"../commands/bash/responseEnvelope.mjs";import t from"node:os";import{DoomToolCall as n,renderToolBadge as r}from"@agimon-ai/doompi-ui/toolChrome";import{highlightCode as i}from"@earendil-works/pi-coding-agent";import{truncateToWidth as a,wrapTextWithAnsi as o}from"@earendil-works/pi-tui";var s=class{lines;layout;constructor(e,t){this.lines=e,this.layout=t}render(e){if(e<=0)return[];let t=Math.max(1,e-2),n=this.layout.wrap?this.lines.flatMap(e=>o(e,t)):this.lines.map(e=>a(e,t,`…`));this.layout.separator&&n.unshift(this.layout.separator(`─`.repeat(t)));let r=n.map(e=>` ${e}`);return this.layout.trailingBlank&&r.push(``),r}invalidate(){}};function c(e,t,n){return new s(e,{wrap:t,separator:e=>n.fg(`borderMuted`,e),trailingBlank:!0})}const l=/\u001b\[[0-9;]*m/;function u(e,t,n){return l.test(e)?e:n.fg(t,e)}function d(e,n=t.homedir()){return n.length>0?e.split(n).join(`~`):e}function f(e){let t=e.split(`
|
|
2
2
|
`).map(e=>e.trim()).filter(e=>e.length>0),n=t[0]??``;return t.length>1?`${n} … +${t.length-1} lines`:n}function p(e,t=160){if(e.length<=t)return e;let n=t-3,r=Math.ceil(n/2),i=e.slice(0,r),a=e.slice(e.length-(n-r)),o=i.lastIndexOf(` `);o>i.length/2&&(i=i.slice(0,o));let s=a.indexOf(` `);return s>=0&&s<a.length/2&&(a=a.slice(s+1)),`${i} … ${a}`}function m(e){return d(f(e))}function h(e){let t=[];return e.background===!0&&t.push(`bg`),e.interactive===!0&&t.push(`tty`),e.alarm!==void 0&&t.push(`alarm ${e.alarm}s`),e.timeout!==void 0&&t.push(`${e.timeout}s`),e.name!==void 0&&e.name.length>0&&t.push(e.name),t}function g(e,t,a=i){let o=a(m(e.command),`bash`).join(` `),s=`${r(`bash`,t)} ${o}`,c=h(e);return c.length>0&&(s+=` ${t.fg(`muted`,`· ${c.join(` · `)}`)}`),new n(s)}function _(t){let n=[];return t.lines!==void 0&&n.push(`${t.lines.toLocaleString(`en-US`)} lines`),t.fileSize!==void 0&&n.push(e(t.fileSize)),t.runner!==void 0&&n.push(t.runner),n.join(` · `)}function v(e,t){let n=(e.tail??``).split(`
|
|
3
3
|
`);for(;n.length>0&&n[n.length-1]?.trim()===``;)n.pop();return t||n.length<=12?n:n.slice(-12)}function y(e){return(e.content??[]).map(e=>e.text??``).join(``)}function b(e,t){let n=e.split(`
|
|
4
|
-
`);for(;n.length>0&&n[n.length-1]?.trim()===``;)n.pop();return n.length<=t?n:n.slice(-t)}function x(e,t,n){let r=e.details??{};if(t.isPartial){let t=b(y(e),12),r=n.fg(`warning`,`◐`)+n.fg(`dim`,` running`);return c([...t.map(e=>u(e,`toolOutput`,n)),r],!1,n)}if(
|
|
4
|
+
`);for(;n.length>0&&n[n.length-1]?.trim()===``;)n.pop();return n.length<=t?n:n.slice(-t)}function x(e,t,n){let r=e.details??{};if(t.isPartial){let t=b(y(e),12),r=n.fg(`warning`,`◐`)+n.fg(`dim`,` running`);return c([...t.map(e=>u(e,`toolOutput`,n)),r],!1,n)}if(t.isError===!0){let i=b(y(e),t.expanded?2**53-1:12),a=_(r),o=a.length>0?` ${a}`:` failed`;return c([...i.map(e=>u(e,`toolOutput`,n)),n.fg(`error`,`✗`)+n.fg(`dim`,o)],t.expanded,n)}if(r.promoted===!0){let e=r.runner??r.id??`runner`,t=r.id===void 0?``:` · ${r.id}`;return c([n.fg(`accent`,`● ${e}`)+n.fg(`muted`,`${t} · background`)],!1,n)}let i=v(r,t.expanded),a=r.exitCode!==void 0&&r.exitCode!==null,o=r.timedOut===!0||a&&r.exitCode!==0,s=o?n.fg(`error`,`✗`):n.fg(`success`,`✓`),l=[];r.timedOut===!0?l.push(`timed out`):o&&l.push(`exit ${r.exitCode}`);let d=_(r);d.length>0&&l.push(d),!t.expanded&&(r.tailLines??0)>i.length&&l.push(`ctrl+o`);let f=i.map(e=>u(e,`toolOutput`,n)),p=l.length>0?` ${l.join(` · `)}`:o?` failed`:i.length===0?` done`:``;return p.length>0&&f.push(s+n.fg(`dim`,p)),c(f,t.expanded,n)}export{d as abbreviateHome,f as collapseCommand,m as formatBashCommand,h as formatBashFlags,_ as formatResultSummary,g as renderBashCall,x as renderBashResult,p as truncateMiddle};
|
|
5
5
|
//# sourceMappingURL=bashRender.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bashRender.mjs","names":[],"sources":["../../src/tui/bashRender.ts"],"sourcesContent":["import os from 'node:os';\nimport { DoomToolCall, renderToolBadge } from '@agimon-ai/doompi-ui/toolChrome';\nimport { highlightCode, type Theme, type ThemeColor } from '@earendil-works/pi-coding-agent';\nimport { type Component, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';\nimport { formatSize } from '../commands/bash/responseEnvelope.ts';\nimport type { BashParams } from '../schemas/bashTool.ts';\n\nconst ELLIPSIS = '…';\n/** Spaced so the join never glues two words into one unreadable token. */\nconst GAP = ' … ';\n/** Wide enough to keep a realistic command intact, short enough to stay under three wrapped rows. */\nconst MAX_COMMAND_LENGTH = 160;\n\n/**\n * Syntax highlighter for the command line.\n *\n * `highlightCode` reads Pi's global theme singleton rather than the theme passed\n * to a renderer, so it is injected here to keep these functions testable.\n */\nexport type Highlighter = (code: string, lang?: string) => string[];\n/** Lines of log kept in the collapsed result, before `ctrl+o` reveals the rest. */\nconst COLLAPSED_TAIL_LINES = 12;\n/** Live output is unbounded while a command runs, so the running state keeps only a window. */\nconst STREAM_TAIL_LINES = 12;\n\n/**\n * Width-aware tool text.\n *\n * Pi's Text component wraps every long line. That is useful for prose, but it\n * makes shell pipelines and grep output consume several visual rows and blurs\n * the boundary between separate log lines. Collapsed tool output is instead\n * clipped per logical line; expanded output keeps all content by wrapping it.\n */\ninterface BashToolTextLayout {\n wrap: boolean;\n separator?: (line: string) => string;\n trailingBlank?: boolean;\n}\n\nclass BashToolText implements Component {\n constructor(\n private readonly lines: readonly string[],\n private readonly layout: BashToolTextLayout,\n ) {}\n\n render(width: number): string[] {\n if (width <= 0) return [];\n const contentWidth = Math.max(1, width - 2);\n const rendered = this.layout.wrap\n ? this.lines.flatMap((line) => wrapTextWithAnsi(line, contentWidth))\n : this.lines.map((line) => truncateToWidth(line, contentWidth, ELLIPSIS));\n if (this.layout.separator) rendered.unshift(this.layout.separator('─'.repeat(contentWidth)));\n const padded = rendered.map((line) => ` ${line}`);\n if (this.layout.trailingBlank) padded.push('');\n return padded;\n }\n\n invalidate(): void {\n // Content is immutable and rendering reads the current width every time.\n }\n}\n\nfunction renderResultText(lines: readonly string[], wrap: boolean, theme: Theme): Component {\n return new BashToolText(lines, {\n wrap,\n // State belongs in the result summary below; keep this divider structural.\n separator: (line) => theme.fg('borderMuted', line),\n trailingBlank: true,\n });\n}\n\n/** An SGR colour sequence, the only escape the log pipeline is allowed to keep. */\n/* oxlint-disable no-control-regex -- matching SGR requires the ESC control character */\n// biome-ignore lint/suspicious/noControlCharactersInRegex: Matching SGR requires the ESC control character.\nconst SGR_PATTERN = /\\u001b\\[[0-9;]*m/;\n/* oxlint-enable no-control-regex */\n\n/**\n * Applies a theme colour only to lines that carry none of their own.\n *\n * A command that emits colour has already said how its output should look, and\n * wrapping it would reset to the terminal default at the first embedded escape.\n */\nfunction paint(line: string, color: ThemeColor, theme: Theme): string {\n return SGR_PATTERN.test(line) ? line : theme.fg(color, line);\n}\n\n/** Structured view of what `formatRunResult` reports, read only by the renderers. */\nexport interface BashResultDetails {\n id?: string;\n runner?: string;\n exitCode?: number | null;\n logPath?: string;\n fileSize?: number;\n lines?: number;\n timedOut?: boolean;\n promoted?: boolean;\n reason?: string;\n tail?: string;\n tailLines?: number;\n}\n\n/** `/Users/me/x` becomes `~/x`. Absolute home paths are what push these lines onto extra rows. */\nexport function abbreviateHome(text: string, home: string = os.homedir()): string {\n return home.length > 0 ? text.split(home).join('~') : text;\n}\n\n/** Collapse a heredoc or multi-line script to one scannable line plus a count. */\nexport function collapseCommand(command: string): string {\n const lines = command\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n const first = lines[0] ?? '';\n return lines.length > 1 ? `${first} ${ELLIPSIS} +${lines.length - 1} lines` : first;\n}\n\n/** Drop the middle rather than the end, so a long pipeline keeps both its source and its sink. */\nexport function truncateMiddle(text: string, max: number = MAX_COMMAND_LENGTH): string {\n if (text.length <= max) return text;\n const keep = max - GAP.length;\n const head = Math.ceil(keep / 2);\n let start = text.slice(0, head);\n let end = text.slice(text.length - (keep - head));\n\n // Snap both cuts to a token boundary, so neither side ends mid-word. Each is\n // only moved when a boundary exists in the half nearest the cut, otherwise a\n // single long argument would swallow the whole budget.\n const lastSpace = start.lastIndexOf(' ');\n if (lastSpace > start.length / 2) start = start.slice(0, lastSpace);\n const firstSpace = end.indexOf(' ');\n if (firstSpace >= 0 && firstSpace < end.length / 2) end = end.slice(firstSpace + 1);\n\n return `${start}${GAP}${end}`;\n}\n\nexport function formatBashCommand(command: string): string {\n return abbreviateHome(collapseCommand(command));\n}\n\n/** Modifiers worth seeing at a glance, in the widget's `·` idiom. */\nexport function formatBashFlags(args: BashParams): string[] {\n const flags: string[] = [];\n if (args.background === true) flags.push('bg');\n if (args.interactive === true) flags.push('tty');\n if (args.alarm !== undefined) flags.push(`alarm ${args.alarm}s`);\n if (args.timeout !== undefined) flags.push(`${args.timeout}s`);\n if (args.name !== undefined && args.name.length > 0) flags.push(args.name);\n return flags;\n}\n\nexport function renderBashCall(args: BashParams, theme: Theme, highlight: Highlighter = highlightCode): Component {\n // Highlight after collapsing multi-line scripts: ANSI codes would break that transformation.\n const command = highlight(formatBashCommand(args.command), 'bash').join(' ');\n let text = `${renderToolBadge('bash', theme)} ${command}`;\n const flags = formatBashFlags(args);\n if (flags.length > 0) text += ` ${theme.fg('muted', `· ${flags.join(' · ')}`)}`;\n // A command is operator input, not log output: wrap it so the complete\n // pipeline remains inspectable instead of clipping its most important tail.\n return new DoomToolCall(text);\n}\n\n/** The metadata footer, collapsed from five lines to one. */\nexport function formatResultSummary(details: BashResultDetails): string {\n const parts: string[] = [];\n if (details.lines !== undefined) parts.push(`${details.lines.toLocaleString('en-US')} lines`);\n if (details.fileSize !== undefined) parts.push(formatSize(details.fileSize));\n if (details.runner !== undefined) parts.push(details.runner);\n return parts.join(' · ');\n}\n\nfunction tailLines(details: BashResultDetails, expanded: boolean): string[] {\n const lines = (details.tail ?? '').split('\\n');\n while (lines.length > 0 && lines[lines.length - 1]?.trim() === '') lines.pop();\n if (expanded || lines.length <= COLLAPSED_TAIL_LINES) return lines;\n return lines.slice(-COLLAPSED_TAIL_LINES);\n}\n\n/** Concatenated text of a tool result, which is all a thrown failure leaves behind. */\nfunction contentText(result: { content?: Array<{ type: string; text?: string }> }): string {\n return (result.content ?? []).map((block) => block.text ?? '').join('');\n}\n\n/** Last `max` lines, so a chatty command cannot push the transcript off screen. */\nfunction lastLines(text: string, max: number): string[] {\n const all = text.split('\\n');\n while (all.length > 0 && all[all.length - 1]?.trim() === '') all.pop();\n return all.length <= max ? all : all.slice(-max);\n}\n\nexport function renderBashResult(\n result: { content?: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },\n options: { expanded: boolean; isPartial: boolean },\n theme: Theme,\n): Component {\n const details = (result.details ?? {}) as BashResultDetails;\n\n // Running. Details do not exist yet, so the streamed text is the only source,\n // and it is bounded here because it grows without limit while the command runs.\n if (options.isPartial) {\n const streamed = lastLines(contentText(result), STREAM_TAIL_LINES);\n const label = theme.fg('warning', '◐') + theme.fg('dim', ' running');\n return renderResultText([...streamed.map((line) => paint(line, 'toolOutput', theme)), label], false, theme);\n }\n\n // Failed. formatRunResult throws on a bad exit, so Pi hands back the error\n // text with no details at all; without this the block would render a bare tick.\n if (result.isError === true) {\n const body = lastLines(contentText(result), options.expanded ? Number.MAX_SAFE_INTEGER : COLLAPSED_TAIL_LINES);\n const summary = formatResultSummary(details);\n const meta = summary.length > 0 ? ` ${summary}` : ' failed';\n return renderResultText(\n [...body.map((line) => paint(line, 'toolOutput', theme)), theme.fg('error', '✗') + theme.fg('dim', meta)],\n options.expanded,\n theme,\n );\n }\n\n if (details.promoted === true) {\n const label = details.runner ?? details.id ?? 'runner';\n const suffix = details.id === undefined ? '' : ` · ${details.id}`;\n return renderResultText(\n [theme.fg('accent', `● ${label}`) + theme.fg('muted', `${suffix} · background`)],\n false,\n theme,\n );\n }\n\n const body = tailLines(details, options.expanded);\n // An absent exit code means the runner never reported one, which is not a failure.\n const exited = details.exitCode !== undefined && details.exitCode !== null;\n const failed = details.timedOut === true || (exited && details.exitCode !== 0);\n const glyph = failed ? theme.fg('error', '✗') : theme.fg('success', '✓');\n\n const meta: string[] = [];\n if (details.timedOut === true) meta.push('timed out');\n else if (failed) meta.push(`exit ${details.exitCode}`);\n const summary = formatResultSummary(details);\n if (summary.length > 0) meta.push(summary);\n if (!options.expanded && (details.tailLines ?? 0) > body.length) meta.push('ctrl+o');\n\n const lines = body.map((line) => paint(line, 'toolOutput', theme));\n // `dim`, not `muted`: muted sits too close to toolOutput to read as chrome.\n const status = meta.length > 0 ? ` ${meta.join(' · ')}` : failed ? ' failed' : body.length === 0 ? ' done' : '';\n if (status.length > 0) lines.push(glyph + theme.fg('dim', status));\n return renderResultText(lines, options.expanded, theme);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"bashRender.mjs","names":[],"sources":["../../src/tui/bashRender.ts"],"sourcesContent":["import os from 'node:os';\nimport { DoomToolCall, renderToolBadge } from '@agimon-ai/doompi-ui/toolChrome';\nimport { highlightCode, type Theme, type ThemeColor } from '@earendil-works/pi-coding-agent';\nimport { type Component, truncateToWidth, wrapTextWithAnsi } from '@earendil-works/pi-tui';\nimport { formatSize } from '../commands/bash/responseEnvelope.ts';\nimport type { BashParams } from '../schemas/bashTool.ts';\n\nconst ELLIPSIS = '…';\n/** Spaced so the join never glues two words into one unreadable token. */\nconst GAP = ' … ';\n/** Wide enough to keep a realistic command intact, short enough to stay under three wrapped rows. */\nconst MAX_COMMAND_LENGTH = 160;\n\n/**\n * Syntax highlighter for the command line.\n *\n * `highlightCode` reads Pi's global theme singleton rather than the theme passed\n * to a renderer, so it is injected here to keep these functions testable.\n */\nexport type Highlighter = (code: string, lang?: string) => string[];\n/** Lines of log kept in the collapsed result, before `ctrl+o` reveals the rest. */\nconst COLLAPSED_TAIL_LINES = 12;\n/** Live output is unbounded while a command runs, so the running state keeps only a window. */\nconst STREAM_TAIL_LINES = 12;\n\n/**\n * Width-aware tool text.\n *\n * Pi's Text component wraps every long line. That is useful for prose, but it\n * makes shell pipelines and grep output consume several visual rows and blurs\n * the boundary between separate log lines. Collapsed tool output is instead\n * clipped per logical line; expanded output keeps all content by wrapping it.\n */\ninterface BashToolTextLayout {\n wrap: boolean;\n separator?: (line: string) => string;\n trailingBlank?: boolean;\n}\n\nclass BashToolText implements Component {\n constructor(\n private readonly lines: readonly string[],\n private readonly layout: BashToolTextLayout,\n ) {}\n\n render(width: number): string[] {\n if (width <= 0) return [];\n const contentWidth = Math.max(1, width - 2);\n const rendered = this.layout.wrap\n ? this.lines.flatMap((line) => wrapTextWithAnsi(line, contentWidth))\n : this.lines.map((line) => truncateToWidth(line, contentWidth, ELLIPSIS));\n if (this.layout.separator) rendered.unshift(this.layout.separator('─'.repeat(contentWidth)));\n const padded = rendered.map((line) => ` ${line}`);\n if (this.layout.trailingBlank) padded.push('');\n return padded;\n }\n\n invalidate(): void {\n // Content is immutable and rendering reads the current width every time.\n }\n}\n\nfunction renderResultText(lines: readonly string[], wrap: boolean, theme: Theme): Component {\n return new BashToolText(lines, {\n wrap,\n // State belongs in the result summary below; keep this divider structural.\n separator: (line) => theme.fg('borderMuted', line),\n trailingBlank: true,\n });\n}\n\n/** An SGR colour sequence, the only escape the log pipeline is allowed to keep. */\n/* oxlint-disable no-control-regex -- matching SGR requires the ESC control character */\n// biome-ignore lint/suspicious/noControlCharactersInRegex: Matching SGR requires the ESC control character.\nconst SGR_PATTERN = /\\u001b\\[[0-9;]*m/;\n/* oxlint-enable no-control-regex */\n\n/**\n * Applies a theme colour only to lines that carry none of their own.\n *\n * A command that emits colour has already said how its output should look, and\n * wrapping it would reset to the terminal default at the first embedded escape.\n */\nfunction paint(line: string, color: ThemeColor, theme: Theme): string {\n return SGR_PATTERN.test(line) ? line : theme.fg(color, line);\n}\n\n/** Structured view of what `formatRunResult` reports, read only by the renderers. */\nexport interface BashResultDetails {\n id?: string;\n runner?: string;\n exitCode?: number | null;\n logPath?: string;\n fileSize?: number;\n lines?: number;\n timedOut?: boolean;\n promoted?: boolean;\n reason?: string;\n tail?: string;\n tailLines?: number;\n}\n\n/** `/Users/me/x` becomes `~/x`. Absolute home paths are what push these lines onto extra rows. */\nexport function abbreviateHome(text: string, home: string = os.homedir()): string {\n return home.length > 0 ? text.split(home).join('~') : text;\n}\n\n/** Collapse a heredoc or multi-line script to one scannable line plus a count. */\nexport function collapseCommand(command: string): string {\n const lines = command\n .split('\\n')\n .map((line) => line.trim())\n .filter((line) => line.length > 0);\n const first = lines[0] ?? '';\n return lines.length > 1 ? `${first} ${ELLIPSIS} +${lines.length - 1} lines` : first;\n}\n\n/** Drop the middle rather than the end, so a long pipeline keeps both its source and its sink. */\nexport function truncateMiddle(text: string, max: number = MAX_COMMAND_LENGTH): string {\n if (text.length <= max) return text;\n const keep = max - GAP.length;\n const head = Math.ceil(keep / 2);\n let start = text.slice(0, head);\n let end = text.slice(text.length - (keep - head));\n\n // Snap both cuts to a token boundary, so neither side ends mid-word. Each is\n // only moved when a boundary exists in the half nearest the cut, otherwise a\n // single long argument would swallow the whole budget.\n const lastSpace = start.lastIndexOf(' ');\n if (lastSpace > start.length / 2) start = start.slice(0, lastSpace);\n const firstSpace = end.indexOf(' ');\n if (firstSpace >= 0 && firstSpace < end.length / 2) end = end.slice(firstSpace + 1);\n\n return `${start}${GAP}${end}`;\n}\n\nexport function formatBashCommand(command: string): string {\n return abbreviateHome(collapseCommand(command));\n}\n\n/** Modifiers worth seeing at a glance, in the widget's `·` idiom. */\nexport function formatBashFlags(args: BashParams): string[] {\n const flags: string[] = [];\n if (args.background === true) flags.push('bg');\n if (args.interactive === true) flags.push('tty');\n if (args.alarm !== undefined) flags.push(`alarm ${args.alarm}s`);\n if (args.timeout !== undefined) flags.push(`${args.timeout}s`);\n if (args.name !== undefined && args.name.length > 0) flags.push(args.name);\n return flags;\n}\n\nexport function renderBashCall(args: BashParams, theme: Theme, highlight: Highlighter = highlightCode): Component {\n // Highlight after collapsing multi-line scripts: ANSI codes would break that transformation.\n const command = highlight(formatBashCommand(args.command), 'bash').join(' ');\n let text = `${renderToolBadge('bash', theme)} ${command}`;\n const flags = formatBashFlags(args);\n if (flags.length > 0) text += ` ${theme.fg('muted', `· ${flags.join(' · ')}`)}`;\n // A command is operator input, not log output: wrap it so the complete\n // pipeline remains inspectable instead of clipping its most important tail.\n return new DoomToolCall(text);\n}\n\n/** The metadata footer, collapsed from five lines to one. */\nexport function formatResultSummary(details: BashResultDetails): string {\n const parts: string[] = [];\n if (details.lines !== undefined) parts.push(`${details.lines.toLocaleString('en-US')} lines`);\n if (details.fileSize !== undefined) parts.push(formatSize(details.fileSize));\n if (details.runner !== undefined) parts.push(details.runner);\n return parts.join(' · ');\n}\n\nfunction tailLines(details: BashResultDetails, expanded: boolean): string[] {\n const lines = (details.tail ?? '').split('\\n');\n while (lines.length > 0 && lines[lines.length - 1]?.trim() === '') lines.pop();\n if (expanded || lines.length <= COLLAPSED_TAIL_LINES) return lines;\n return lines.slice(-COLLAPSED_TAIL_LINES);\n}\n\n/** Concatenated text of a tool result, which is all a thrown failure leaves behind. */\nfunction contentText(result: { content?: Array<{ type: string; text?: string }> }): string {\n return (result.content ?? []).map((block) => block.text ?? '').join('');\n}\n\n/** Last `max` lines, so a chatty command cannot push the transcript off screen. */\nfunction lastLines(text: string, max: number): string[] {\n const all = text.split('\\n');\n while (all.length > 0 && all[all.length - 1]?.trim() === '') all.pop();\n return all.length <= max ? all : all.slice(-max);\n}\n\nexport function renderBashResult(\n result: { content?: Array<{ type: string; text?: string }>; details?: unknown },\n options: { expanded: boolean; isPartial: boolean; isError?: boolean },\n theme: Theme,\n): Component {\n const details = (result.details ?? {}) as BashResultDetails;\n\n // Running. Details do not exist yet, so the streamed text is the only source,\n // and it is bounded here because it grows without limit while the command runs.\n if (options.isPartial) {\n const streamed = lastLines(contentText(result), STREAM_TAIL_LINES);\n const label = theme.fg('warning', '◐') + theme.fg('dim', ' running');\n return renderResultText([...streamed.map((line) => paint(line, 'toolOutput', theme)), label], false, theme);\n }\n\n // Failed. formatRunResult throws on a bad exit, so Pi hands back the error\n // text with no details at all; without this the block would render a bare tick.\n if (options.isError === true) {\n const body = lastLines(contentText(result), options.expanded ? Number.MAX_SAFE_INTEGER : COLLAPSED_TAIL_LINES);\n const summary = formatResultSummary(details);\n const meta = summary.length > 0 ? ` ${summary}` : ' failed';\n return renderResultText(\n [...body.map((line) => paint(line, 'toolOutput', theme)), theme.fg('error', '✗') + theme.fg('dim', meta)],\n options.expanded,\n theme,\n );\n }\n\n if (details.promoted === true) {\n const label = details.runner ?? details.id ?? 'runner';\n const suffix = details.id === undefined ? '' : ` · ${details.id}`;\n return renderResultText(\n [theme.fg('accent', `● ${label}`) + theme.fg('muted', `${suffix} · background`)],\n false,\n theme,\n );\n }\n\n const body = tailLines(details, options.expanded);\n // An absent exit code means the runner never reported one, which is not a failure.\n const exited = details.exitCode !== undefined && details.exitCode !== null;\n const failed = details.timedOut === true || (exited && details.exitCode !== 0);\n const glyph = failed ? theme.fg('error', '✗') : theme.fg('success', '✓');\n\n const meta: string[] = [];\n if (details.timedOut === true) meta.push('timed out');\n else if (failed) meta.push(`exit ${details.exitCode}`);\n const summary = formatResultSummary(details);\n if (summary.length > 0) meta.push(summary);\n if (!options.expanded && (details.tailLines ?? 0) > body.length) meta.push('ctrl+o');\n\n const lines = body.map((line) => paint(line, 'toolOutput', theme));\n // `dim`, not `muted`: muted sits too close to toolOutput to read as chrome.\n const status = meta.length > 0 ? ` ${meta.join(' · ')}` : failed ? ' failed' : body.length === 0 ? ' done' : '';\n if (status.length > 0) lines.push(glyph + theme.fg('dim', status));\n return renderResultText(lines, options.expanded, theme);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agimon-ai/doompi-runner",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.13",
|
|
4
4
|
"description": "Pi bash tool replacement with background process supervision",
|
|
5
5
|
"homepage": "https://agimon.ai",
|
|
6
6
|
"license": "MIT",
|
|
@@ -285,10 +285,10 @@
|
|
|
285
285
|
"node-pty": "1.1.0",
|
|
286
286
|
"reflect-metadata": "0.2.2",
|
|
287
287
|
"typebox": "1.1.38",
|
|
288
|
-
"@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.
|
|
289
|
-
"@agimon-ai/doompi-ui": "0.0.1-alpha.
|
|
290
|
-
"@agimon-ai/
|
|
291
|
-
"@agimon-ai/
|
|
288
|
+
"@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.13",
|
|
289
|
+
"@agimon-ai/doompi-ui": "0.0.1-alpha.13",
|
|
290
|
+
"@agimon-ai/doompi-telemetry": "0.0.1-alpha.13",
|
|
291
|
+
"@agimon-ai/foundation-process-registry": "0.29.3"
|
|
292
292
|
},
|
|
293
293
|
"devDependencies": {
|
|
294
294
|
"@earendil-works/pi-coding-agent": "0.84.1",
|
|
@@ -312,10 +312,10 @@
|
|
|
312
312
|
}
|
|
313
313
|
},
|
|
314
314
|
"optionalDependencies": {
|
|
315
|
-
"@agimon-ai/doompi-runner-rmux-
|
|
316
|
-
"@agimon-ai/doompi-runner-rmux-darwin-
|
|
317
|
-
"@agimon-ai/doompi-runner-rmux-linux-x64": "0.0.1-alpha.
|
|
318
|
-
"@agimon-ai/doompi-runner-rmux-
|
|
315
|
+
"@agimon-ai/doompi-runner-rmux-darwin-arm64": "0.0.1-alpha.13",
|
|
316
|
+
"@agimon-ai/doompi-runner-rmux-darwin-x64": "0.0.1-alpha.13",
|
|
317
|
+
"@agimon-ai/doompi-runner-rmux-linux-x64": "0.0.1-alpha.13",
|
|
318
|
+
"@agimon-ai/doompi-runner-rmux-linux-arm64": "0.0.1-alpha.13"
|
|
319
319
|
},
|
|
320
320
|
"engines": {
|
|
321
321
|
"node": ">=22.19.0"
|