chore: remove deprecated calc tool as it is completely useless with eval
This commit is contained in:
@@ -233,11 +233,10 @@ Stealth's on by default, so pages see a normal user instead of a headless bot. T
|
||||
|
||||
**Misc**
|
||||
|
||||
- `calc` — deterministic arithmetic — no model in the loop.
|
||||
- `resolve` — apply or discard a queued preview action.
|
||||
- `search_tool_bm25` — BM25 over the hidden tool index; activates top matches mid-session.
|
||||
|
||||
Setting-gated, off by default: `github`, `calc`, `inspect_image`, `render_mermaid`, `checkpoint`, `rewind`, `search_tool_bm25`, `retain`, `recall`, `reflect`. Flip them on once, scoped per project.
|
||||
Setting-gated, off by default: `github`, `inspect_image`, `render_mermaid`, `checkpoint`, `rewind`, `search_tool_bm25`, `retain`, `recall`, `reflect`. Flip them on once, scoped per project.
|
||||
|
||||
[Full reference →](https://omp.sh/docs/tools)
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
# calc
|
||||
|
||||
> Evaluates one or more arithmetic expressions and returns formatted numeric results.
|
||||
|
||||
## Source
|
||||
- Entry: `packages/coding-agent/src/tools/calculator.ts`
|
||||
- Model-facing prompt: `packages/coding-agent/src/prompts/tools/calculator.md`
|
||||
- Key collaborators:
|
||||
- `packages/coding-agent/src/tui.ts` — status lines and tree-list rendering
|
||||
- `packages/coding-agent/src/tools/render-utils.ts` — preview limits and formatting helpers
|
||||
|
||||
## Inputs
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `calculations` | `Calculation[]` | Yes | Batch of expressions to evaluate in order. |
|
||||
|
||||
### `Calculation`
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `expression` | `string` | Yes | Arithmetic expression string. |
|
||||
| `prefix` | `string` | Yes | Prepended verbatim to the rendered numeric result. |
|
||||
| `suffix` | `string` | Yes | Appended verbatim to the rendered numeric result. |
|
||||
|
||||
## Outputs
|
||||
- Single-shot result.
|
||||
- `content[0].text` is the newline-joined `prefix + value + suffix` string for each calculation.
|
||||
- `details.results` is an array of `{ expression, value, output }`.
|
||||
- On renderer fallback, if `details` is missing but `content[0].text` exists, the TUI tries to pair each output line with the original expressions from call args.
|
||||
|
||||
## Flow
|
||||
1. `execute()` wraps evaluation in `untilAborted(...)`.
|
||||
2. For each entry, `evaluateExpression(...)` tokenizes the expression, parses it with a recursive-descent parser, rejects non-finite outputs, and normalizes `-0` to `0`.
|
||||
3. `tokenizeExpression(...)` accepts whitespace, parentheses, operators, and number literals; any other character throws immediately.
|
||||
4. `ExpressionParser` applies precedence in this order: `+ -`, `* / %`, unary `+ -`, exponentiation `**`, parentheses/literals.
|
||||
5. Exponentiation is right-associative (`2 ** 3 ** 2` parses as `2 ** (3 ** 2)`).
|
||||
6. Each numeric result is formatted with `String(value)` and wrapped with the provided `prefix` and `suffix`.
|
||||
7. The tool returns text output plus structured `details`.
|
||||
|
||||
## Side Effects
|
||||
- Background work / cancellation
|
||||
- Supports abort via `untilAborted(...)`.
|
||||
- Session state
|
||||
- None.
|
||||
- Filesystem / Network / Subprocesses
|
||||
- None.
|
||||
|
||||
## Limits & Caps
|
||||
- Supported operators: `+`, `-`, `*`, `/`, `%`, `**` (`packages/coding-agent/src/tools/calculator.ts`).
|
||||
- Supported numeric literals:
|
||||
- decimal integers/floats, including leading-dot forms like `.5`
|
||||
- scientific notation like `1e10`, `2.5E-3`
|
||||
- hexadecimal `0x...`
|
||||
- binary `0b...`
|
||||
- octal `0o...`
|
||||
- Results must be finite; `Infinity` and `NaN` are rejected.
|
||||
- The renderer collapses long result lists using `PREVIEW_LIMITS.COLLAPSED_ITEMS` from `packages/coding-agent/src/tools/render-utils.ts`.
|
||||
|
||||
## Errors
|
||||
- Invalid characters: e.g. `Invalid character "x" in expression`.
|
||||
- Malformed numbers: invalid prefixed literal, invalid exponent, invalid number.
|
||||
- Syntax errors: `Unexpected token in expression`, `Unexpected end of expression`, `Missing closing parenthesis`, `Expression is empty`.
|
||||
- Non-finite arithmetic: `Expression result is not a finite number`.
|
||||
- Any evaluation error aborts the whole batch; the tool does not return partial successes.
|
||||
|
||||
## Notes
|
||||
- Despite the schema example showing `sqrt(16)`, the parser does not support functions, identifiers, units, or constants; only numeric literals, operators, and parentheses are accepted.
|
||||
- Precision is plain JavaScript `number` semantics throughout, including floating-point rounding behavior.
|
||||
- `/` and `%` use JavaScript numeric operators directly; there is no integer-only mode or unit handling.
|
||||
- Unary operators bind tighter than `*`/`/`/`%` but looser than exponentiation because unary parsing delegates to `#parsePower()`.
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed the `calc` tool (deterministic arithmetic evaluator) and its `calc.enabled` setting. The model can compute via `eval` instead.
|
||||
|
||||
## [15.5.14] - 2026-05-29
|
||||
### Added
|
||||
|
||||
|
||||
@@ -2017,16 +2017,6 @@ export const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
|
||||
"calc.enabled": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
ui: {
|
||||
tab: "tools",
|
||||
label: "Calculator",
|
||||
description: "Enable the calculator tool for basic calculations",
|
||||
},
|
||||
},
|
||||
|
||||
"tts.enabled": {
|
||||
type: "boolean",
|
||||
default: false,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1227,17 +1227,6 @@
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderCalc(name, args, result, ctx) {
|
||||
let html = toolHead('calc');
|
||||
const exprs = args.expressions || (args.expression ? [args.expression] : []);
|
||||
for (const e of exprs) html += codeBlock(String(e), 'plaintext');
|
||||
if (result) {
|
||||
const output = ctx.getResultText();
|
||||
if (output) html += formatExpandableOutput(output, 6);
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
function renderJob(name, args, result, ctx) {
|
||||
const badges = [];
|
||||
const pollIds = Array.isArray(args.poll) ? args.poll : Array.isArray(args.jobs) ? args.jobs : Array.isArray(args.jobIds) ? args.jobIds : [];
|
||||
@@ -1558,8 +1547,6 @@
|
||||
yield: renderYield,
|
||||
report_finding: renderReportFinding,
|
||||
report_tool_issue: renderReportToolIssue,
|
||||
calc: renderCalc,
|
||||
calculator: renderCalc,
|
||||
await: renderJob,
|
||||
poll: renderJob,
|
||||
cancel_job: renderJob,
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
Performs basic calculations.
|
||||
|
||||
<instruction>
|
||||
- Supports +, -, *, /, %, ** and parentheses
|
||||
- Supports decimal, hex (0x), binary (0b), and octal (0o) literals
|
||||
</instruction>
|
||||
|
||||
<output>
|
||||
Returns each calculation result with its prefix and suffix applied.
|
||||
</output>
|
||||
@@ -1,541 +0,0 @@
|
||||
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
||||
import type { Component } from "@oh-my-pi/pi-tui";
|
||||
import { Text } from "@oh-my-pi/pi-tui";
|
||||
import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
|
||||
import * as z from "zod/v4";
|
||||
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
||||
import type { Theme } from "../modes/theme/theme";
|
||||
import calculatorDescription from "../prompts/tools/calculator.md" with { type: "text" };
|
||||
import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
||||
import type { ToolSession } from ".";
|
||||
import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS, TRUNCATE_LENGTHS } from "./render-utils";
|
||||
|
||||
// =============================================================================
|
||||
// Token Types
|
||||
// =============================================================================
|
||||
|
||||
/** Supported arithmetic operators (** is exponentiation). */
|
||||
type Operator = "+" | "-" | "*" | "/" | "%" | "**";
|
||||
|
||||
/**
|
||||
* Lexer token variants:
|
||||
* - number: parsed numeric value with original string for error messages
|
||||
* - operator: arithmetic operator
|
||||
* - paren: grouping parenthesis
|
||||
*/
|
||||
type Token =
|
||||
| { type: "number"; value: number; raw: string }
|
||||
| { type: "operator"; value: Operator }
|
||||
| { type: "paren"; value: "(" | ")" };
|
||||
|
||||
const calculatorSchema = z.object({
|
||||
calculations: z
|
||||
.array(
|
||||
z.object({
|
||||
expression: z.string().describe("math expression"),
|
||||
prefix: z.string().describe("prefix text"),
|
||||
suffix: z.string().describe("suffix text"),
|
||||
}),
|
||||
)
|
||||
.describe("calculations to evaluate"),
|
||||
});
|
||||
|
||||
export interface CalculatorToolDetails {
|
||||
results: Array<{ expression: string; value: number; output: string }>;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Character classification helpers for numeric literal parsing
|
||||
// =============================================================================
|
||||
|
||||
function isDigit(ch: string): boolean {
|
||||
return ch >= "0" && ch <= "9";
|
||||
}
|
||||
|
||||
function isHexDigit(ch: string): boolean {
|
||||
return (ch >= "0" && ch <= "9") || (ch >= "a" && ch <= "f") || (ch >= "A" && ch <= "F");
|
||||
}
|
||||
|
||||
function isBinaryDigit(ch: string): boolean {
|
||||
return ch === "0" || ch === "1";
|
||||
}
|
||||
|
||||
function isOctalDigit(ch: string): boolean {
|
||||
return ch >= "0" && ch <= "7";
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Tokenizer
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Tokenize a math expression into numbers, operators, and parentheses.
|
||||
*
|
||||
* Number formats supported:
|
||||
* - Decimal: 123, 3.14, .5
|
||||
* - Scientific: 1e10, 2.5E-3
|
||||
* - Hexadecimal: 0xFF
|
||||
* - Binary: 0b1010
|
||||
* - Octal: 0o755
|
||||
*/
|
||||
function tokenizeExpression(expression: string): Token[] {
|
||||
const tokens: Token[] = [];
|
||||
let i = 0;
|
||||
|
||||
while (i < expression.length) {
|
||||
const ch = expression[i];
|
||||
|
||||
// Skip whitespace
|
||||
if (ch.trim() === "") {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "(" || ch === ")") {
|
||||
tokens.push({ type: "paren", value: ch });
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check ** before single * to handle exponentiation
|
||||
if (ch === "*" && expression[i + 1] === "*") {
|
||||
tokens.push({ type: "operator", value: "**" });
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%") {
|
||||
tokens.push({ type: "operator", value: ch });
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Number parsing: starts with digit or decimal point followed by digit
|
||||
const next = expression[i + 1];
|
||||
const numberStart = isDigit(ch) || (ch === "." && next !== undefined && isDigit(next));
|
||||
if (!numberStart) {
|
||||
throw new Error(`Invalid character "${ch}" in expression`);
|
||||
}
|
||||
|
||||
const start = i;
|
||||
|
||||
// Handle prefixed literals (0x, 0b, 0o)
|
||||
if (ch === "0" && next !== undefined) {
|
||||
const prefix = next.toLowerCase();
|
||||
if (prefix === "x" || prefix === "b" || prefix === "o") {
|
||||
i += 2; // Skip "0x" / "0b" / "0o"
|
||||
let hasDigit = false;
|
||||
while (i < expression.length) {
|
||||
const digit = expression[i];
|
||||
const valid =
|
||||
prefix === "x" ? isHexDigit(digit) : prefix === "b" ? isBinaryDigit(digit) : isOctalDigit(digit);
|
||||
if (!valid) break;
|
||||
hasDigit = true;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if (!hasDigit) {
|
||||
throw new Error(`Invalid numeric literal starting at "${expression.slice(start, i)}"`);
|
||||
}
|
||||
|
||||
const raw = expression.slice(start, i);
|
||||
const value = Number(raw); // JS Number() handles 0x/0b/0o natively
|
||||
if (!Number.isFinite(value)) {
|
||||
throw new Error(`Invalid number "${raw}"`);
|
||||
}
|
||||
tokens.push({ type: "number", value, raw });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse decimal number: integer part
|
||||
let hasDigits = false;
|
||||
while (i < expression.length && isDigit(expression[i])) {
|
||||
hasDigits = true;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
// Fractional part
|
||||
if (expression[i] === ".") {
|
||||
i += 1;
|
||||
while (i < expression.length && isDigit(expression[i])) {
|
||||
hasDigits = true;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasDigits) {
|
||||
throw new Error(`Invalid number starting at "${expression.slice(start, i + 1)}"`);
|
||||
}
|
||||
|
||||
// Scientific notation exponent (e.g., 1e10, 2.5E-3)
|
||||
if (expression[i] === "e" || expression[i] === "E") {
|
||||
i += 1;
|
||||
if (expression[i] === "+" || expression[i] === "-") {
|
||||
i += 1;
|
||||
}
|
||||
|
||||
let hasExponentDigits = false;
|
||||
while (i < expression.length && isDigit(expression[i])) {
|
||||
hasExponentDigits = true;
|
||||
i += 1;
|
||||
}
|
||||
|
||||
if (!hasExponentDigits) {
|
||||
throw new Error(`Invalid exponent in "${expression.slice(start, i)}"`);
|
||||
}
|
||||
}
|
||||
|
||||
const raw = expression.slice(start, i);
|
||||
const value = Number(raw);
|
||||
if (!Number.isFinite(value)) {
|
||||
throw new Error(`Invalid number "${raw}"`);
|
||||
}
|
||||
tokens.push({ type: "number", value, raw });
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Recursive Descent Parser
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Recursive descent parser for arithmetic expressions.
|
||||
*
|
||||
* Operator precedence (lowest to highest):
|
||||
* 1. Addition, subtraction (+, -)
|
||||
* 2. Multiplication, division, modulo (*, /, %)
|
||||
* 3. Unary plus/minus (+x, -x)
|
||||
* 4. Exponentiation (**)
|
||||
* 5. Parentheses and literals
|
||||
*
|
||||
* Each precedence level has its own parse method. Lower precedence methods
|
||||
* call higher precedence methods, building the AST implicitly through
|
||||
* the call stack.
|
||||
*/
|
||||
class ExpressionParser {
|
||||
#index = 0;
|
||||
|
||||
constructor(private readonly tokens: Token[]) {}
|
||||
|
||||
/** Parse the full expression and ensure all tokens are consumed. */
|
||||
parse(): number {
|
||||
const value = this.#parseExpression();
|
||||
if (this.#index < this.tokens.length) {
|
||||
throw new Error("Unexpected token in expression");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse addition and subtraction (lowest precedence).
|
||||
* Left-associative: 1 - 2 - 3 = (1 - 2) - 3
|
||||
*/
|
||||
#parseExpression(): number {
|
||||
let value = this.#parseTerm();
|
||||
while (true) {
|
||||
if (this.#matchOperator("+")) {
|
||||
value += this.#parseTerm();
|
||||
continue;
|
||||
}
|
||||
if (this.#matchOperator("-")) {
|
||||
value -= this.#parseTerm();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse multiplication, division, and modulo.
|
||||
* Left-associative: 8 / 4 / 2 = (8 / 4) / 2
|
||||
*/
|
||||
#parseTerm(): number {
|
||||
let value = this.#parseUnary();
|
||||
while (true) {
|
||||
if (this.#matchOperator("*")) {
|
||||
value *= this.#parseUnary();
|
||||
continue;
|
||||
}
|
||||
if (this.#matchOperator("/")) {
|
||||
value /= this.#parseUnary();
|
||||
continue;
|
||||
}
|
||||
if (this.#matchOperator("%")) {
|
||||
value %= this.#parseUnary();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse unary + and - operators.
|
||||
* Recursive to handle chained unary: --x, +-x
|
||||
*/
|
||||
#parseUnary(): number {
|
||||
if (this.#matchOperator("+")) {
|
||||
return this.#parseUnary();
|
||||
}
|
||||
if (this.#matchOperator("-")) {
|
||||
return -this.#parseUnary();
|
||||
}
|
||||
return this.#parsePower();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse exponentiation operator.
|
||||
* Right-associative: 2 ** 3 ** 2 = 2 ** (3 ** 2) = 512
|
||||
* Achieved by recursive call to parsePower for the right operand.
|
||||
*/
|
||||
#parsePower(): number {
|
||||
let value = this.#parsePrimary();
|
||||
if (this.#matchOperator("**")) {
|
||||
value = value ** this.#parsePower(); // Right-associative via recursion
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse primary expressions: number literals and parenthesized subexpressions.
|
||||
* Parentheses restart parsing at lowest precedence (parseExpression).
|
||||
*/
|
||||
#parsePrimary(): number {
|
||||
const token = this.#peek();
|
||||
if (!token) {
|
||||
throw new Error("Unexpected end of expression");
|
||||
}
|
||||
|
||||
if (token.type === "number") {
|
||||
this.#index += 1;
|
||||
return token.value;
|
||||
}
|
||||
|
||||
if (token.type === "paren" && token.value === "(") {
|
||||
this.#index += 1;
|
||||
const value = this.#parseExpression(); // Reset to lowest precedence
|
||||
if (!this.#matchParen(")")) {
|
||||
throw new Error("Missing closing parenthesis");
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new Error("Unexpected token in expression");
|
||||
}
|
||||
|
||||
/** Consume operator if it matches, advancing the token index. */
|
||||
#matchOperator(value: Operator): boolean {
|
||||
const token = this.tokens[this.#index];
|
||||
if (token && token.type === "operator" && token.value === value) {
|
||||
this.#index += 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Consume parenthesis if it matches, advancing the token index. */
|
||||
#matchParen(value: "(" | ")"): boolean {
|
||||
const token = this.tokens[this.#index];
|
||||
if (token && token.type === "paren" && token.value === value) {
|
||||
this.#index += 1;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Look at current token without consuming it. */
|
||||
#peek(): Token | undefined {
|
||||
return this.tokens[this.#index];
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Expression Evaluator
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* Evaluate a math expression string and return the numeric result.
|
||||
*
|
||||
* Pipeline: expression string -> tokens -> parse tree (implicit) -> value
|
||||
*
|
||||
* @throws Error on syntax errors, empty expressions, or non-finite results (Infinity, NaN)
|
||||
*/
|
||||
function evaluateExpression(expression: string): number {
|
||||
const tokens = tokenizeExpression(expression);
|
||||
if (tokens.length === 0) {
|
||||
throw new Error("Expression is empty");
|
||||
}
|
||||
const parser = new ExpressionParser(tokens);
|
||||
const value = parser.parse();
|
||||
if (!Number.isFinite(value)) {
|
||||
throw new Error("Expression result is not a finite number");
|
||||
}
|
||||
// Normalize -0 to 0 for consistent output
|
||||
return Object.is(value, -0) ? 0 : value;
|
||||
}
|
||||
|
||||
function formatResult(value: number): string {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// Tool Class
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
type CalculatorParams = z.infer<typeof calculatorSchema>;
|
||||
|
||||
/**
|
||||
* Calculator tool for evaluating mathematical expressions.
|
||||
*
|
||||
* Supports decimal, hex (0x), binary (0b), octal (0o) literals,
|
||||
* standard arithmetic operators, and parentheses.
|
||||
*/
|
||||
export class CalculatorTool implements AgentTool<typeof calculatorSchema, CalculatorToolDetails> {
|
||||
readonly name = "calc";
|
||||
readonly approval = "read" as const;
|
||||
readonly label = "Calc";
|
||||
readonly summary = "Evaluate a mathematical expression";
|
||||
readonly loadMode = "discoverable";
|
||||
readonly description: string;
|
||||
readonly parameters = calculatorSchema;
|
||||
readonly strict = true;
|
||||
|
||||
constructor(_session: ToolSession) {
|
||||
this.description = prompt.render(calculatorDescription);
|
||||
}
|
||||
|
||||
async execute(
|
||||
_toolCallId: string,
|
||||
{ calculations }: CalculatorParams,
|
||||
signal?: AbortSignal,
|
||||
): Promise<AgentToolResult<CalculatorToolDetails>> {
|
||||
return untilAborted(signal, async () => {
|
||||
const results = calculations.map(calc => {
|
||||
const value = evaluateExpression(calc.expression);
|
||||
const output = `${calc.prefix}${formatResult(value)}${calc.suffix}`;
|
||||
return { expression: calc.expression, value, output };
|
||||
});
|
||||
|
||||
const outputText = results.map(result => result.output).join("\n");
|
||||
return {
|
||||
content: [{ type: "text", text: outputText }],
|
||||
details: { results },
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// TUI Renderer
|
||||
// =============================================================================
|
||||
|
||||
interface CalculatorRenderArgs {
|
||||
calculations?: Array<{ expression: string; prefix?: string; suffix?: string }>;
|
||||
}
|
||||
|
||||
const COLLAPSED_LIST_LIMIT = PREVIEW_LIMITS.COLLAPSED_ITEMS;
|
||||
|
||||
/**
|
||||
* TUI renderer for calculator tool calls and results.
|
||||
* Handles both collapsed (preview) and expanded (full) display modes.
|
||||
*/
|
||||
export const calculatorToolRenderer = {
|
||||
/**
|
||||
* Render the tool call header showing the first expression and count.
|
||||
* Format: "Calc <expression> (N calcs)"
|
||||
*/
|
||||
renderCall(args: CalculatorRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
||||
const count = args.calculations?.length ?? 0;
|
||||
const firstExpression = args.calculations?.[0]?.expression;
|
||||
const description = firstExpression ? truncateToWidth(firstExpression, TRUNCATE_LENGTHS.TITLE) : undefined;
|
||||
const meta = count > 0 ? [formatCount("calc", count)] : [];
|
||||
const text = renderStatusLine({ icon: "pending", title: "Calc", description, meta }, uiTheme);
|
||||
return new Text(text, 0, 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Render calculation results as a tree list.
|
||||
* Collapsed mode shows first N items with expand hint; expanded shows all.
|
||||
*/
|
||||
renderResult(
|
||||
result: { content: Array<{ type: string; text?: string }>; details?: CalculatorToolDetails; isError?: boolean },
|
||||
options: RenderResultOptions,
|
||||
uiTheme: Theme,
|
||||
args?: CalculatorRenderArgs,
|
||||
): Component {
|
||||
const details = result.details;
|
||||
const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
|
||||
if (result.isError) {
|
||||
const header = renderStatusLine({ icon: "error", title: "Calc" }, uiTheme);
|
||||
const renderedLines = [header, formatErrorMessage(textContent, uiTheme)];
|
||||
return {
|
||||
render() {
|
||||
return renderedLines;
|
||||
},
|
||||
invalidate() {},
|
||||
};
|
||||
}
|
||||
|
||||
// Prefer structured details; fall back to parsing text content
|
||||
let outputs = details?.results?.map(entry => `${entry.expression} = ${entry.output}`) ?? [];
|
||||
if (outputs.length === 0 && textContent.trim()) {
|
||||
const rawOutputs = textContent.split("\n").filter(line => line.trim().length > 0);
|
||||
const expressions = args?.calculations?.map(calc => calc.expression) ?? [];
|
||||
if (expressions.length === rawOutputs.length && expressions.length > 0) {
|
||||
outputs = rawOutputs.map((output, index) => `${expressions[index]} = ${output}`);
|
||||
} else {
|
||||
outputs = rawOutputs;
|
||||
}
|
||||
}
|
||||
|
||||
if (outputs.length === 0) {
|
||||
const header = renderStatusLine({ icon: "warning", title: "Calc" }, uiTheme);
|
||||
const renderedLines = [header, formatEmptyMessage("No results", uiTheme)];
|
||||
return {
|
||||
render() {
|
||||
return renderedLines;
|
||||
},
|
||||
invalidate() {},
|
||||
};
|
||||
}
|
||||
|
||||
const description = args?.calculations?.[0]?.expression
|
||||
? truncateToWidth(args.calculations[0].expression, TRUNCATE_LENGTHS.TITLE)
|
||||
: undefined;
|
||||
const header = renderStatusLine(
|
||||
{ icon: "success", title: "Calc", description, meta: [formatCount("result", outputs.length)] },
|
||||
uiTheme,
|
||||
);
|
||||
|
||||
let cached: RenderCache | undefined;
|
||||
|
||||
return {
|
||||
render(width) {
|
||||
const { expanded } = options;
|
||||
const key = new Hasher().bool(expanded).u32(width).digest();
|
||||
if (cached?.key === key) return cached.lines;
|
||||
const treeLines = renderTreeList(
|
||||
{
|
||||
items: outputs,
|
||||
expanded,
|
||||
maxCollapsed: COLLAPSED_LIST_LIMIT,
|
||||
itemType: "result",
|
||||
renderItem: output => uiTheme.fg("toolOutput", output),
|
||||
},
|
||||
uiTheme,
|
||||
);
|
||||
const lines = [header, ...treeLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
||||
cached = { key, lines };
|
||||
return lines;
|
||||
},
|
||||
invalidate() {
|
||||
cached = undefined;
|
||||
},
|
||||
};
|
||||
},
|
||||
mergeCallAndResult: true,
|
||||
};
|
||||
@@ -28,7 +28,6 @@ import { AstEditTool } from "./ast-edit";
|
||||
import { AstGrepTool } from "./ast-grep";
|
||||
import { BashTool } from "./bash";
|
||||
import { BrowserTool } from "./browser";
|
||||
import { CalculatorTool } from "./calculator";
|
||||
import { type CheckpointState, CheckpointTool, RewindTool } from "./checkpoint";
|
||||
import { DebugTool } from "./debug";
|
||||
import { EvalTool } from "./eval";
|
||||
@@ -69,7 +68,6 @@ export * from "./ast-edit";
|
||||
export * from "./ast-grep";
|
||||
export * from "./bash";
|
||||
export * from "./browser";
|
||||
export * from "./calculator";
|
||||
export * from "./checkpoint";
|
||||
export * from "./debug";
|
||||
export * from "./eval";
|
||||
@@ -286,7 +284,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
||||
ask: AskTool.createIf,
|
||||
debug: DebugTool.createIf,
|
||||
eval: s => new EvalTool(s),
|
||||
calc: s => new CalculatorTool(s),
|
||||
ssh: loadSshTool,
|
||||
github: GithubTool.createIf,
|
||||
find: s => new FindTool(s),
|
||||
@@ -455,7 +452,6 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
||||
if (name === "web_search") return session.settings.get("web_search.enabled");
|
||||
// search_tool_bm25 is allowed when either legacy mcp.discoveryMode or new tools.discoveryMode is active.
|
||||
if (name === "search_tool_bm25") return discoveryActive;
|
||||
if (name === "calc") return session.settings.get("calc.enabled");
|
||||
if (name === "browser") return session.settings.get("browser.enabled");
|
||||
if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
|
||||
if (name === "irc") {
|
||||
|
||||
@@ -16,7 +16,6 @@ import { astEditToolRenderer } from "./ast-edit";
|
||||
import { astGrepToolRenderer } from "./ast-grep";
|
||||
import { bashToolRenderer } from "./bash";
|
||||
import { browserToolRenderer } from "./browser/render";
|
||||
import { calculatorToolRenderer } from "./calculator";
|
||||
import { debugToolRenderer } from "./debug";
|
||||
import { evalToolRenderer } from "./eval";
|
||||
import { findToolRenderer } from "./find";
|
||||
@@ -54,7 +53,6 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
||||
recipe: recipeToolRenderer as ToolRenderer,
|
||||
debug: debugToolRenderer as ToolRenderer,
|
||||
eval: evalToolRenderer as ToolRenderer,
|
||||
calc: calculatorToolRenderer as ToolRenderer,
|
||||
edit: editToolRenderer as ToolRenderer,
|
||||
apply_patch: editToolRenderer as ToolRenderer,
|
||||
find: findToolRenderer as ToolRenderer,
|
||||
|
||||
@@ -24,7 +24,6 @@ const allToolsSettings = Settings.isolated({
|
||||
"lsp.enabled": true,
|
||||
"inspect_image.enabled": true,
|
||||
"web_search.enabled": true,
|
||||
"calc.enabled": true,
|
||||
"browser.enabled": true,
|
||||
"checkpoint.enabled": true,
|
||||
"irc.enabled": true,
|
||||
|
||||
@@ -205,7 +205,6 @@ describe("createTools", () => {
|
||||
"web_search.enabled": false,
|
||||
"browser.enabled": false,
|
||||
"inspect_image.enabled": false,
|
||||
"calc.enabled": false,
|
||||
}),
|
||||
});
|
||||
const tools = await createTools(session);
|
||||
@@ -219,7 +218,6 @@ describe("createTools", () => {
|
||||
expect(names).not.toContain("web_search");
|
||||
expect(names).not.toContain("browser");
|
||||
expect(names).not.toContain("inspect_image");
|
||||
expect(names).not.toContain("calc");
|
||||
});
|
||||
|
||||
it("always includes resolve regardless of plan-mode setting", async () => {
|
||||
|
||||
Reference in New Issue
Block a user