mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(coding-agent): collapse grouped tool calls
This commit is contained in:
parent
4bbe2959bd
commit
2676945130
12 changed files with 234 additions and 49 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added optional `toolGroup` metadata on `AgentTool` for UI grouping.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed context token estimates to count user image attachments consistently with tool result images ([#4983](https://github.com/earendil-works/pi/issues/4983)).
|
||||
|
|
|
|||
|
|
@ -361,6 +361,8 @@ export type AgentToolUpdateCallback<T = any> = (partialResult: AgentToolResult<T
|
|||
export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any> extends Tool<TParameters> {
|
||||
/** Human-readable label for UI display. */
|
||||
label: string;
|
||||
/** Optional freeform group identifier for collapsed UI rendering. */
|
||||
toolGroup?: string;
|
||||
/**
|
||||
* Optional compatibility shim for raw tool-call arguments before schema validation.
|
||||
* Must return an object that matches `TParameters`.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added collapsed tool grouping for consecutive `read`/`find` tool calls.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed user message transcript rendering to preserve user-authored ordered-list markers ([#5013](https://github.com/earendil-works/pi/issues/5013)).
|
||||
|
|
|
|||
|
|
@ -418,6 +418,8 @@ export interface ToolRenderContext<TState = any, TArgs = any> {
|
|||
showImages: boolean;
|
||||
/** Whether the current result is an error. */
|
||||
isError: boolean;
|
||||
/** True when renderCall is producing a collapsed grouped-tool summary. */
|
||||
toolGroupSummary?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -436,6 +438,8 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
|
|||
promptGuidelines?: string[];
|
||||
/** Parameter schema (TypeBox) */
|
||||
parameters: TParams;
|
||||
/** Optional freeform group identifier for collapsed tool-call aggregation. */
|
||||
toolGroup?: string;
|
||||
/** Controls whether ToolExecutionComponent renders the standard colored shell or the tool renders its own framing. */
|
||||
renderShell?: "default" | "self";
|
||||
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@ export function createFindToolDefinition(
|
|||
description: `Search for files by glob pattern. Returns matching file paths relative to the search directory. Respects .gitignore. Output is truncated to ${DEFAULT_LIMIT} results or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first).`,
|
||||
promptSnippet: "Find files by glob pattern (respects .gitignore)",
|
||||
parameters: findSchema,
|
||||
toolGroup: "explore",
|
||||
async execute(
|
||||
_toolCallId,
|
||||
{ pattern, path: searchDir, limit }: { pattern: string; path?: string; limit?: number },
|
||||
|
|
|
|||
|
|
@ -144,8 +144,9 @@ function formatCompactReadCall(
|
|||
classification: CompactReadClassification,
|
||||
args: ReadRenderArgs | undefined,
|
||||
theme: Theme,
|
||||
showExpandHint = true,
|
||||
): string {
|
||||
const expandHint = theme.fg("dim", ` (${keyText("app.tools.expand")} to expand)`);
|
||||
const expandHint = showExpandHint ? theme.fg("dim", ` (${keyText("app.tools.expand")} to expand)`) : "";
|
||||
if (classification.kind === "skill") {
|
||||
return (
|
||||
theme.fg("customMessageLabel", `\x1b[1m[skill]\x1b[22m `) +
|
||||
|
|
@ -216,6 +217,7 @@ export function createReadToolDefinition(
|
|||
promptSnippet: "Read file contents",
|
||||
promptGuidelines: ["Use read to examine files instead of cat or sed."],
|
||||
parameters: readSchema,
|
||||
toolGroup: "explore",
|
||||
async execute(
|
||||
_toolCallId,
|
||||
{ path, offset, limit }: { path: string; offset?: number; limit?: number },
|
||||
|
|
@ -344,7 +346,9 @@ export function createReadToolDefinition(
|
|||
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
||||
const classification = !context.expanded ? getCompactReadClassification(args, context.cwd) : undefined;
|
||||
text.setText(
|
||||
classification ? formatCompactReadCall(classification, args, theme) : formatReadCall(args, theme),
|
||||
classification
|
||||
? formatCompactReadCall(classification, args, theme, !context.toolGroupSummary)
|
||||
: formatReadCall(args, theme),
|
||||
);
|
||||
return text;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export function wrapToolDefinition<TDetails = unknown>(
|
|||
label: definition.label,
|
||||
description: definition.description,
|
||||
parameters: definition.parameters,
|
||||
toolGroup: definition.toolGroup,
|
||||
prepareArguments: definition.prepareArguments,
|
||||
executionMode: definition.executionMode,
|
||||
execute: (toolCallId, params, signal, onUpdate) =>
|
||||
|
|
@ -38,6 +39,7 @@ export function createToolDefinitionFromAgentTool(tool: AgentTool<any>): ToolDef
|
|||
label: tool.label,
|
||||
description: tool.description,
|
||||
parameters: tool.parameters as any,
|
||||
toolGroup: tool.toolGroup,
|
||||
prepareArguments: tool.prepareArguments,
|
||||
executionMode: tool.executionMode,
|
||||
execute: async (toolCallId, params, signal, onUpdate) => tool.execute(toolCallId, params, signal, onUpdate),
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export { SkillInvocationMessageComponent } from "./skill-invocation-message.ts";
|
|||
export { ThemeSelectorComponent } from "./theme-selector.ts";
|
||||
export { ThinkingSelectorComponent } from "./thinking-selector.ts";
|
||||
export { ToolExecutionComponent, type ToolExecutionOptions } from "./tool-execution.ts";
|
||||
export { ToolGroupComponent } from "./tool-group.ts";
|
||||
export { TreeSelectorComponent } from "./tree-selector.ts";
|
||||
export { UserMessageComponent } from "./user-message.ts";
|
||||
export { UserMessageSelectorComponent } from "./user-message-selector.ts";
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { ToolDefinition, ToolRenderContext } from "../../../core/extensions
|
|||
import { createAllToolDefinitions, type ToolName } from "../../../core/tools/index.ts";
|
||||
import { getTextOutput as getRenderedTextOutput } from "../../../core/tools/render-utils.ts";
|
||||
import { convertToPng } from "../../../utils/image-convert.ts";
|
||||
import { theme } from "../theme/theme.ts";
|
||||
import { type ThemeBg, theme } from "../theme/theme.ts";
|
||||
|
||||
export interface ToolExecutionOptions {
|
||||
showImages?: boolean;
|
||||
|
|
@ -28,6 +28,7 @@ export class ToolExecutionComponent extends Container {
|
|||
private isPartial = true;
|
||||
private toolDefinition?: ToolDefinition<any, any>;
|
||||
private builtInToolDefinition?: ToolDefinition<any, any>;
|
||||
readonly toolGroup: string | undefined;
|
||||
private ui: TUI;
|
||||
private cwd: string;
|
||||
private executionStarted = false;
|
||||
|
|
@ -55,6 +56,7 @@ export class ToolExecutionComponent extends Container {
|
|||
this.args = args;
|
||||
this.toolDefinition = toolDefinition;
|
||||
this.builtInToolDefinition = createAllToolDefinitions(cwd)[toolName as ToolName];
|
||||
this.toolGroup = this.toolDefinition?.toolGroup ?? this.builtInToolDefinition?.toolGroup;
|
||||
this.showImages = options.showImages ?? true;
|
||||
this.imageWidthCells = options.imageWidthCells ?? 60;
|
||||
this.ui = ui;
|
||||
|
|
@ -112,7 +114,7 @@ export class ToolExecutionComponent extends Container {
|
|||
return this.toolDefinition.renderShell ?? this.builtInToolDefinition.renderShell ?? "default";
|
||||
}
|
||||
|
||||
private getRenderContext(lastComponent: Component | undefined): ToolRenderContext {
|
||||
private getRenderContext(lastComponent: Component | undefined, toolGroupSummary = false): ToolRenderContext {
|
||||
return {
|
||||
args: this.args,
|
||||
toolCallId: this.toolCallId,
|
||||
|
|
@ -129,6 +131,7 @@ export class ToolExecutionComponent extends Container {
|
|||
expanded: this.expanded,
|
||||
showImages: this.showImages,
|
||||
isError: this.result?.isError ?? false,
|
||||
toolGroupSummary,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -213,11 +216,32 @@ export class ToolExecutionComponent extends Container {
|
|||
this.updateDisplay();
|
||||
}
|
||||
|
||||
getBackgroundColor(): ThemeBg {
|
||||
if (this.isPartial) return "toolPendingBg";
|
||||
if (this.result?.isError) return "toolErrorBg";
|
||||
return "toolSuccessBg";
|
||||
}
|
||||
|
||||
override invalidate(): void {
|
||||
super.invalidate();
|
||||
this.updateDisplay();
|
||||
}
|
||||
|
||||
renderCallSummary(width: number): string[] {
|
||||
const callRenderer = this.getCallRenderer();
|
||||
let component: Component;
|
||||
if (!callRenderer) {
|
||||
component = this.createCallFallback();
|
||||
} else {
|
||||
try {
|
||||
component = callRenderer(this.args, theme, this.getRenderContext(undefined, true));
|
||||
} catch {
|
||||
component = this.createCallFallback();
|
||||
}
|
||||
}
|
||||
return component.render(width);
|
||||
}
|
||||
|
||||
override render(width: number): string[] {
|
||||
if (this.hideComponent) {
|
||||
return [];
|
||||
|
|
@ -226,11 +250,7 @@ export class ToolExecutionComponent extends Container {
|
|||
}
|
||||
|
||||
private updateDisplay(): void {
|
||||
const bgFn = this.isPartial
|
||||
? (text: string) => theme.bg("toolPendingBg", text)
|
||||
: this.result?.isError
|
||||
? (text: string) => theme.bg("toolErrorBg", text)
|
||||
: (text: string) => theme.bg("toolSuccessBg", text);
|
||||
const bgFn = (text: string) => theme.bg(this.getBackgroundColor(), text);
|
||||
|
||||
let hasContent = false;
|
||||
this.hideComponent = false;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
import { Box, type Component } from "@earendil-works/pi-tui";
|
||||
import { type ThemeBg, theme } from "../theme/theme.ts";
|
||||
import { keyText } from "./keybinding-hints.ts";
|
||||
import type { ToolExecutionComponent } from "./tool-execution.ts";
|
||||
|
||||
export class ToolGroupComponent implements Component {
|
||||
readonly toolGroup: string;
|
||||
private readonly tools: ToolExecutionComponent[] = [];
|
||||
private expanded = false;
|
||||
|
||||
constructor(toolGroup: string, tools: ToolExecutionComponent[] = []) {
|
||||
this.toolGroup = toolGroup;
|
||||
for (const tool of tools) {
|
||||
this.addTool(tool);
|
||||
}
|
||||
}
|
||||
|
||||
addTool(tool: ToolExecutionComponent): void {
|
||||
tool.setExpanded(this.expanded);
|
||||
this.tools.push(tool);
|
||||
}
|
||||
|
||||
setExpanded(expanded: boolean): void {
|
||||
this.expanded = expanded;
|
||||
for (const tool of this.tools) {
|
||||
tool.setExpanded(expanded);
|
||||
}
|
||||
}
|
||||
|
||||
setShowImages(show: boolean): void {
|
||||
for (const tool of this.tools) {
|
||||
tool.setShowImages(show);
|
||||
}
|
||||
}
|
||||
|
||||
setImageWidthCells(width: number): void {
|
||||
for (const tool of this.tools) {
|
||||
tool.setImageWidthCells(width);
|
||||
}
|
||||
}
|
||||
|
||||
invalidate(): void {
|
||||
for (const tool of this.tools) {
|
||||
tool.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
render(width: number): string[] {
|
||||
if (this.tools.length === 0) return [];
|
||||
|
||||
const safeWidth = Math.max(1, width);
|
||||
if (this.expanded) {
|
||||
return this.tools.flatMap((tool) => tool.render(safeWidth));
|
||||
}
|
||||
|
||||
const box = new Box(1, 1, (text) => theme.bg(this.getBackgroundColor(), text));
|
||||
box.addChild({
|
||||
render: (contentWidth) => this.renderCollapsed(contentWidth),
|
||||
invalidate: () => {},
|
||||
});
|
||||
return [" ".repeat(safeWidth), ...box.render(safeWidth)];
|
||||
}
|
||||
|
||||
private renderCollapsed(width: number): string[] {
|
||||
const lines = this.tools.flatMap((tool) =>
|
||||
tool.renderCallSummary(width).map((line) => line.replace(/[ \t]+$/g, "")),
|
||||
);
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
if (lines[i]!.trim().length > 0) {
|
||||
lines[i] += theme.fg("dim", ` (${keyText("app.tools.expand")} to expand)`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
private getBackgroundColor(): ThemeBg {
|
||||
const colors = this.tools.map((tool) => tool.getBackgroundColor());
|
||||
if (colors.includes("toolErrorBg")) return "toolErrorBg";
|
||||
if (colors.includes("toolPendingBg")) return "toolPendingBg";
|
||||
return "toolSuccessBg";
|
||||
}
|
||||
}
|
||||
|
|
@ -118,6 +118,7 @@ import { SessionSelectorComponent } from "./components/session-selector.ts";
|
|||
import { SettingsSelectorComponent } from "./components/settings-selector.ts";
|
||||
import { SkillInvocationMessageComponent } from "./components/skill-invocation-message.ts";
|
||||
import { ToolExecutionComponent } from "./components/tool-execution.ts";
|
||||
import { ToolGroupComponent } from "./components/tool-group.ts";
|
||||
import { TreeSelectorComponent } from "./components/tree-selector.ts";
|
||||
import { UserMessageComponent } from "./components/user-message.ts";
|
||||
import { UserMessageSelectorComponent } from "./components/user-message-selector.ts";
|
||||
|
|
@ -1620,6 +1621,47 @@ export class InteractiveMode {
|
|||
return this.session.getToolDefinition(toolName);
|
||||
}
|
||||
|
||||
private createToolExecutionComponent(toolName: string, toolCallId: string, args: unknown): ToolExecutionComponent {
|
||||
const component = new ToolExecutionComponent(
|
||||
toolName,
|
||||
toolCallId,
|
||||
args,
|
||||
{
|
||||
showImages: this.settingsManager.getShowImages(),
|
||||
imageWidthCells: this.settingsManager.getImageWidthCells(),
|
||||
},
|
||||
this.getRegisteredToolDefinition(toolName),
|
||||
this.ui,
|
||||
this.sessionManager.getCwd(),
|
||||
);
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
return component;
|
||||
}
|
||||
|
||||
private appendToolExecutionComponent(component: ToolExecutionComponent): void {
|
||||
const toolGroup = component.toolGroup?.trim();
|
||||
if (!toolGroup) {
|
||||
this.chatContainer.addChild(component);
|
||||
return;
|
||||
}
|
||||
|
||||
const children = this.chatContainer.children;
|
||||
const lastChild = children[children.length - 1];
|
||||
if (lastChild instanceof ToolGroupComponent && lastChild.toolGroup === toolGroup) {
|
||||
lastChild.addTool(component);
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastChild instanceof ToolExecutionComponent && lastChild.toolGroup?.trim() === toolGroup) {
|
||||
const group = new ToolGroupComponent(toolGroup, [lastChild, component]);
|
||||
group.setExpanded(this.toolOutputExpanded);
|
||||
children[children.length - 1] = group;
|
||||
return;
|
||||
}
|
||||
|
||||
this.chatContainer.addChild(component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up keyboard shortcuts registered by extensions.
|
||||
*/
|
||||
|
|
@ -2729,20 +2771,12 @@ export class InteractiveMode {
|
|||
for (const content of this.streamingMessage.content) {
|
||||
if (content.type === "toolCall") {
|
||||
if (!this.pendingTools.has(content.id)) {
|
||||
const component = new ToolExecutionComponent(
|
||||
const component = this.createToolExecutionComponent(
|
||||
content.name,
|
||||
content.id,
|
||||
content.arguments,
|
||||
{
|
||||
showImages: this.settingsManager.getShowImages(),
|
||||
imageWidthCells: this.settingsManager.getImageWidthCells(),
|
||||
},
|
||||
this.getRegisteredToolDefinition(content.name),
|
||||
this.ui,
|
||||
this.sessionManager.getCwd(),
|
||||
);
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(component);
|
||||
this.appendToolExecutionComponent(component);
|
||||
this.pendingTools.set(content.id, component);
|
||||
} else {
|
||||
const component = this.pendingTools.get(content.id);
|
||||
|
|
@ -2798,20 +2832,8 @@ export class InteractiveMode {
|
|||
case "tool_execution_start": {
|
||||
let component = this.pendingTools.get(event.toolCallId);
|
||||
if (!component) {
|
||||
component = new ToolExecutionComponent(
|
||||
event.toolName,
|
||||
event.toolCallId,
|
||||
event.args,
|
||||
{
|
||||
showImages: this.settingsManager.getShowImages(),
|
||||
imageWidthCells: this.settingsManager.getImageWidthCells(),
|
||||
},
|
||||
this.getRegisteredToolDefinition(event.toolName),
|
||||
this.ui,
|
||||
this.sessionManager.getCwd(),
|
||||
);
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(component);
|
||||
component = this.createToolExecutionComponent(event.toolName, event.toolCallId, event.args);
|
||||
this.appendToolExecutionComponent(component);
|
||||
this.pendingTools.set(event.toolCallId, component);
|
||||
}
|
||||
component.markExecutionStarted();
|
||||
|
|
@ -3139,20 +3161,8 @@ export class InteractiveMode {
|
|||
// Render tool call components
|
||||
for (const content of message.content) {
|
||||
if (content.type === "toolCall") {
|
||||
const component = new ToolExecutionComponent(
|
||||
content.name,
|
||||
content.id,
|
||||
content.arguments,
|
||||
{
|
||||
showImages: this.settingsManager.getShowImages(),
|
||||
imageWidthCells: this.settingsManager.getImageWidthCells(),
|
||||
},
|
||||
this.getRegisteredToolDefinition(content.name),
|
||||
this.ui,
|
||||
this.sessionManager.getCwd(),
|
||||
);
|
||||
component.setExpanded(this.toolOutputExpanded);
|
||||
this.chatContainer.addChild(component);
|
||||
const component = this.createToolExecutionComponent(content.name, content.id, content.arguments);
|
||||
this.appendToolExecutionComponent(component);
|
||||
|
||||
if (message.stopReason === "aborted" || message.stopReason === "error") {
|
||||
let errorMessage: string;
|
||||
|
|
@ -3874,7 +3884,7 @@ export class InteractiveMode {
|
|||
onShowImagesChange: (enabled) => {
|
||||
this.settingsManager.setShowImages(enabled);
|
||||
for (const child of this.chatContainer.children) {
|
||||
if (child instanceof ToolExecutionComponent) {
|
||||
if (child instanceof ToolExecutionComponent || child instanceof ToolGroupComponent) {
|
||||
child.setShowImages(enabled);
|
||||
}
|
||||
}
|
||||
|
|
@ -3882,7 +3892,7 @@ export class InteractiveMode {
|
|||
onImageWidthCellsChange: (width) => {
|
||||
this.settingsManager.setImageWidthCells(width);
|
||||
for (const child of this.chatContainer.children) {
|
||||
if (child instanceof ToolExecutionComponent) {
|
||||
if (child instanceof ToolExecutionComponent || child instanceof ToolGroupComponent) {
|
||||
child.setImageWidthCells(width);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import { beforeAll, describe, expect, test } from "vitest";
|
|||
import { getReadmePath } from "../src/config.ts";
|
||||
import type { ToolDefinition } from "../src/core/extensions/types.ts";
|
||||
import { type BashOperations, createBashToolDefinition } from "../src/core/tools/bash.ts";
|
||||
import { createFindToolDefinition } from "../src/core/tools/find.ts";
|
||||
import { createReadTool, createReadToolDefinition } from "../src/core/tools/read.ts";
|
||||
import { createWriteToolDefinition } from "../src/core/tools/write.ts";
|
||||
import { ToolExecutionComponent } from "../src/modes/interactive/components/tool-execution.ts";
|
||||
import { ToolGroupComponent } from "../src/modes/interactive/components/tool-group.ts";
|
||||
import { initTheme } from "../src/modes/interactive/theme/theme.ts";
|
||||
import { stripAnsi } from "../src/utils/ansi.ts";
|
||||
|
||||
|
|
@ -88,6 +90,11 @@ describe("ToolExecutionComponent parity", () => {
|
|||
expect(rendered).not.toContain(":1");
|
||||
});
|
||||
|
||||
test("assigns explore tool group to read and find tools", () => {
|
||||
expect(createReadToolDefinition(process.cwd()).toolGroup).toBe("explore");
|
||||
expect(createFindToolDefinition(process.cwd()).toolGroup).toBe("explore");
|
||||
});
|
||||
|
||||
test("preserves legacy file_path rendering compatibility for built-in tools", () => {
|
||||
const component = new ToolExecutionComponent(
|
||||
"read",
|
||||
|
|
@ -457,6 +464,49 @@ describe("ToolExecutionComponent parity", () => {
|
|||
});
|
||||
}
|
||||
|
||||
test("renders collapsed tool groups as one card with the hint on the last summary", () => {
|
||||
const readDefinition = createReadToolDefinition(process.cwd());
|
||||
const skillRead = new ToolExecutionComponent(
|
||||
"read",
|
||||
"tool-group-skill",
|
||||
{ path: join(process.cwd(), "attio", "SKILL.md") },
|
||||
{},
|
||||
readDefinition,
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
const rangedRead = new ToolExecutionComponent(
|
||||
"read",
|
||||
"tool-group-ranged-read",
|
||||
{ path: "packages/ai/CHANGELOG.md", offset: 1, limit: 90 },
|
||||
{},
|
||||
readDefinition,
|
||||
createFakeTui(),
|
||||
process.cwd(),
|
||||
);
|
||||
const group = new ToolGroupComponent("explore");
|
||||
group.addTool(skillRead);
|
||||
group.addTool(rangedRead);
|
||||
|
||||
const rendered = stripAnsi(group.render(120).join("\n"));
|
||||
const renderedLines = rendered.split("\n");
|
||||
expect(rendered).not.toContain("Explore");
|
||||
expect(rendered).toContain("[skill] attio");
|
||||
expect(rendered).toContain("read packages/ai/CHANGELOG.md:1-90");
|
||||
expect(rendered.match(/to expand/g)?.length ?? 0).toBe(1);
|
||||
expect(renderedLines.some((line) => line.startsWith(" [skill] attio") && !line.includes("to expand"))).toBe(true);
|
||||
expect(
|
||||
renderedLines.some(
|
||||
(line) => line.startsWith(" read packages/ai/CHANGELOG.md:1-90") && line.includes("to expand"),
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
group.setExpanded(true);
|
||||
const expanded = stripAnsi(group.render(120).join("\n"));
|
||||
expect(expanded).not.toContain("Explore");
|
||||
expect(expanded).not.toContain("to expand");
|
||||
});
|
||||
|
||||
for (const scenario of [
|
||||
{ title: "SKILL.md", path: join(process.cwd(), "attio", "SKILL.md"), compact: "[skill] attio:120-329" },
|
||||
{ title: "Pi documentation", path: getReadmePath(), compact: "read docs README.md:120-329" },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue