@oh-my-pi/pi-coding-agent 4.3.1 → 4.4.5
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/CHANGELOG.md +49 -0
- package/package.json +5 -6
- package/src/core/frontmatter.ts +98 -0
- package/src/core/keybindings.ts +1 -1
- package/src/core/prompt-templates.ts +5 -34
- package/src/core/sdk.ts +3 -0
- package/src/core/skills.ts +3 -3
- package/src/core/slash-commands.ts +14 -5
- package/src/core/tools/bash.ts +27 -11
- package/src/core/tools/calculator.ts +1 -1
- package/src/core/tools/edit.ts +2 -2
- package/src/core/tools/exa/render.ts +23 -11
- package/src/core/tools/index.test.ts +2 -0
- package/src/core/tools/index.ts +3 -0
- package/src/core/tools/jtd-to-json-schema.ts +1 -6
- package/src/core/tools/ls.ts +5 -2
- package/src/core/tools/lsp/config.ts +2 -2
- package/src/core/tools/lsp/render.ts +33 -12
- package/src/core/tools/notebook.ts +1 -1
- package/src/core/tools/output.ts +1 -1
- package/src/core/tools/read.ts +15 -49
- package/src/core/tools/render-utils.ts +65 -28
- package/src/core/tools/renderers.ts +2 -0
- package/src/core/tools/schema-validation.test.ts +501 -0
- package/src/core/tools/task/agents.ts +6 -2
- package/src/core/tools/task/commands.ts +9 -3
- package/src/core/tools/task/discovery.ts +3 -2
- package/src/core/tools/task/render.ts +10 -7
- package/src/core/tools/todo-write.ts +256 -0
- package/src/core/tools/web-fetch.ts +4 -2
- package/src/core/tools/web-scrapers/choosealicense.ts +2 -2
- package/src/core/tools/web-search/render.ts +13 -10
- package/src/core/tools/write.ts +2 -2
- package/src/discovery/builtin.ts +4 -4
- package/src/discovery/cline.ts +4 -3
- package/src/discovery/codex.ts +3 -3
- package/src/discovery/cursor.ts +2 -2
- package/src/discovery/github.ts +3 -2
- package/src/discovery/helpers.test.ts +14 -10
- package/src/discovery/helpers.ts +2 -39
- package/src/discovery/windsurf.ts +3 -3
- package/src/modes/interactive/components/custom-editor.ts +4 -11
- package/src/modes/interactive/components/index.ts +2 -1
- package/src/modes/interactive/components/read-tool-group.ts +118 -0
- package/src/modes/interactive/components/todo-display.ts +112 -0
- package/src/modes/interactive/components/tool-execution.ts +20 -4
- package/src/modes/interactive/controllers/command-controller.ts +2 -2
- package/src/modes/interactive/controllers/event-controller.ts +91 -32
- package/src/modes/interactive/controllers/input-controller.ts +19 -13
- package/src/modes/interactive/interactive-mode.ts +103 -3
- package/src/modes/interactive/theme/theme.ts +4 -0
- package/src/modes/interactive/types.ts +14 -2
- package/src/modes/interactive/utils/ui-helpers.ts +55 -26
- package/src/prompts/system/system-prompt.md +177 -126
- package/src/prompts/tools/todo-write.md +187 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,55 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [4.4.5] - 2026-01-11
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Removed `format: "date-time"` from timestamp type conversion in JTD to JSON Schema transformation
|
|
10
|
+
- Reorganized system prompt to display context, environment, and tools sections before discipline guidelines
|
|
11
|
+
- Updated system prompt to show file paths more clearly in output
|
|
12
|
+
- Improved YAML frontmatter parsing with better error messages including source file information
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Fixed frontmatter parsing to properly report source location when YAML parsing fails
|
|
17
|
+
|
|
18
|
+
## [4.4.4] - 2026-01-11
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- Added `todo_write` tool for creating and managing structured task lists during coding sessions
|
|
22
|
+
- Added persistent todo panel above the editor that displays task progress
|
|
23
|
+
- Added `Ctrl+T` keybinding to toggle todo list expansion
|
|
24
|
+
- Added grouped display for consecutive Read tool calls, showing multiple file reads in a compact tree view
|
|
25
|
+
- Added `todo_write` tool and persistent todo panel above the editor
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Changed `Ctrl+Enter` to insert a newline when not streaming (previously `Alt+Enter`)
|
|
30
|
+
- Changed `Ctrl+T` from toggling thinking block visibility to toggling todo list expansion
|
|
31
|
+
- Changed system prompt to use more direct, field-oriented language with emphasis on verification and assumptions
|
|
32
|
+
- Changed temporary model selector keybinding from Ctrl+Y to Alt+P
|
|
33
|
+
- Changed expand hint text from "Ctrl+O to expand" to "Ctrl+O for more"
|
|
34
|
+
- Changed Read tool result display to hide content by default, showing only file path and status
|
|
35
|
+
- Changed `Ctrl+T` to toggle todo panel expansion
|
|
36
|
+
|
|
37
|
+
### Removed
|
|
38
|
+
|
|
39
|
+
- Removed `yaml` package dependency in favor of Bun's built-in YAML parser
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Fixed Alt+Enter to insert a newline when not streaming, instead of submitting the message
|
|
44
|
+
- Fixed Alt+Enter inserting a new line when not streaming instead of submitting a message
|
|
45
|
+
- Fixed Cursor provider to avoid advertising the Edit tool, relying on full-file Write operations instead
|
|
46
|
+
- Fixed prompt template loading to strip leading HTML comment metadata blocks
|
|
47
|
+
|
|
48
|
+
## [4.3.2] - 2026-01-11
|
|
49
|
+
### Changed
|
|
50
|
+
|
|
51
|
+
- Increased default bash output preview from 5 to 10 lines when collapsed
|
|
52
|
+
- Updated expanded bash output view to show full untruncated output when available
|
|
53
|
+
|
|
5
54
|
## [4.3.1] - 2026-01-11
|
|
6
55
|
|
|
7
56
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.4.5",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-ai": "4.
|
|
43
|
-
"@oh-my-pi/pi-agent-core": "4.
|
|
44
|
-
"@oh-my-pi/pi-git-tool": "4.
|
|
45
|
-
"@oh-my-pi/pi-tui": "4.
|
|
42
|
+
"@oh-my-pi/pi-ai": "4.4.5",
|
|
43
|
+
"@oh-my-pi/pi-agent-core": "4.4.5",
|
|
44
|
+
"@oh-my-pi/pi-git-tool": "4.4.5",
|
|
45
|
+
"@oh-my-pi/pi-tui": "4.4.5",
|
|
46
46
|
"@openai/agents": "^0.3.7",
|
|
47
47
|
"@sinclair/typebox": "^0.34.46",
|
|
48
48
|
"ajv": "^8.17.1",
|
|
@@ -63,7 +63,6 @@
|
|
|
63
63
|
"strip-ansi": "^7.1.2",
|
|
64
64
|
"winston": "^3.17.0",
|
|
65
65
|
"winston-daily-rotate-file": "^5.0.0",
|
|
66
|
-
"yaml": "^2.8.2",
|
|
67
66
|
"zod": "^4.3.5"
|
|
68
67
|
},
|
|
69
68
|
"devDependencies": {
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { YAML } from "bun";
|
|
2
|
+
import { logger } from "./logger";
|
|
3
|
+
|
|
4
|
+
function stripHtmlComments(content: string): string {
|
|
5
|
+
return content.replace(/<!--[\s\S]*?-->/g, "");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function toError(value: unknown): Error {
|
|
9
|
+
return value instanceof Error ? value : new Error(String(`YAML: ${value}`));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function truncate(content: string, maxLength: number): string {
|
|
13
|
+
return content.length > maxLength ? `${content.slice(0, maxLength)}...` : content;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class FrontmatterError extends Error {
|
|
17
|
+
constructor(
|
|
18
|
+
error: Error,
|
|
19
|
+
public readonly source?: unknown,
|
|
20
|
+
) {
|
|
21
|
+
super(`Failed to parse YAML frontmatter: ${error.message}`, { cause: error });
|
|
22
|
+
this.name = "FrontmatterError";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
toString(): string {
|
|
26
|
+
// Format the error with stack and detail, including the error message, stack, and source if present
|
|
27
|
+
const details: string[] = [this.message];
|
|
28
|
+
if (this.source !== undefined) {
|
|
29
|
+
details.push(`Source: ${JSON.stringify(this.source)}`);
|
|
30
|
+
}
|
|
31
|
+
if (this.cause && typeof this.cause === "object" && "stack" in this.cause && this.cause.stack) {
|
|
32
|
+
details.push(`Stack:\n${this.cause.stack}`);
|
|
33
|
+
} else if (this.stack) {
|
|
34
|
+
details.push(`Stack:\n${this.stack}`);
|
|
35
|
+
}
|
|
36
|
+
return details.join("\n\n");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface FrontmatterOptions {
|
|
41
|
+
/** Source of the content */
|
|
42
|
+
source?: unknown;
|
|
43
|
+
/** Fallback frontmatter values */
|
|
44
|
+
fallback?: Record<string, unknown>;
|
|
45
|
+
/** Normalize the content */
|
|
46
|
+
normalize?: boolean;
|
|
47
|
+
/** Level of error handling */
|
|
48
|
+
level?: "off" | "warn" | "fatal";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse YAML frontmatter from markdown content
|
|
53
|
+
* Returns { frontmatter, body } where body has frontmatter stripped
|
|
54
|
+
*/
|
|
55
|
+
export function parseFrontmatter(
|
|
56
|
+
content: string,
|
|
57
|
+
options?: FrontmatterOptions,
|
|
58
|
+
): { frontmatter: Record<string, unknown>; body: string } {
|
|
59
|
+
const { source, fallback, normalize = true, level = "warn" } = options ?? {};
|
|
60
|
+
const frontmatter: Record<string, unknown> = Object.assign({}, fallback);
|
|
61
|
+
|
|
62
|
+
const normalized = normalize ? stripHtmlComments(content.replace(/\r\n/g, "\n").replace(/\r/g, "\n")) : content;
|
|
63
|
+
if (!normalized.startsWith("---")) {
|
|
64
|
+
return { frontmatter, body: normalized };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const endIndex = normalized.indexOf("\n---", 3);
|
|
68
|
+
if (endIndex === -1) {
|
|
69
|
+
return { frontmatter, body: normalized };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const metadata = normalized.slice(4, endIndex);
|
|
73
|
+
const body = normalized.slice(endIndex + 4).trim();
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Replace tabs with spaces for YAML compatibility, use failsafe mode for robustness
|
|
77
|
+
const loaded = YAML.parse(metadata.replaceAll("\t", " ")) as Record<string, unknown> | null;
|
|
78
|
+
return { frontmatter: Object.assign(frontmatter, loaded), body: body };
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const err = new FrontmatterError(toError(error), source ?? `Inline '${truncate(content, 64)}'`);
|
|
81
|
+
if (level === "warn" || level === "fatal") {
|
|
82
|
+
logger.warn("Failed to parse YAML frontmatter", { err: err.toString() });
|
|
83
|
+
}
|
|
84
|
+
if (level === "fatal") {
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Simple YAML parsing - just key: value pairs
|
|
89
|
+
for (const line of metadata.split("\n")) {
|
|
90
|
+
const match = line.match(/^(\w+):\s*(.*)$/);
|
|
91
|
+
if (match) {
|
|
92
|
+
frontmatter[match[1]] = match[2].trim();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { frontmatter, body: body };
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/core/keybindings.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { join, resolve } from "node:path";
|
|
2
2
|
import Handlebars from "handlebars";
|
|
3
3
|
import { CONFIG_DIR_NAME, getPromptsDir } from "../config";
|
|
4
|
+
import { parseFrontmatter } from "./frontmatter";
|
|
4
5
|
import { logger } from "./logger";
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -280,36 +281,6 @@ function optimizePromptLayout(input: string): string {
|
|
|
280
281
|
return s.trim();
|
|
281
282
|
}
|
|
282
283
|
|
|
283
|
-
/**
|
|
284
|
-
* Parse YAML frontmatter from markdown content
|
|
285
|
-
* Returns { frontmatter, content } where content has frontmatter stripped
|
|
286
|
-
*/
|
|
287
|
-
function parseFrontmatter(content: string): { frontmatter: Record<string, string>; content: string } {
|
|
288
|
-
const frontmatter: Record<string, string> = {};
|
|
289
|
-
|
|
290
|
-
if (!content.startsWith("---")) {
|
|
291
|
-
return { frontmatter, content };
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const endIndex = content.indexOf("\n---", 3);
|
|
295
|
-
if (endIndex === -1) {
|
|
296
|
-
return { frontmatter, content };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const frontmatterBlock = content.slice(4, endIndex);
|
|
300
|
-
const remainingContent = content.slice(endIndex + 4).trim();
|
|
301
|
-
|
|
302
|
-
// Simple YAML parsing - just key: value pairs
|
|
303
|
-
for (const line of frontmatterBlock.split("\n")) {
|
|
304
|
-
const match = line.match(/^(\w+):\s*(.*)$/);
|
|
305
|
-
if (match) {
|
|
306
|
-
frontmatter[match[1]] = match[2].trim();
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
return { frontmatter, content: remainingContent };
|
|
311
|
-
}
|
|
312
|
-
|
|
313
284
|
/**
|
|
314
285
|
* Parse command arguments respecting quoted strings (bash-style)
|
|
315
286
|
* Returns array of arguments
|
|
@@ -413,7 +384,7 @@ async function loadTemplatesFromDir(
|
|
|
413
384
|
|
|
414
385
|
if (entry.endsWith(".md")) {
|
|
415
386
|
const rawContent = await file.text();
|
|
416
|
-
const { frontmatter,
|
|
387
|
+
const { frontmatter, body } = parseFrontmatter(rawContent, { source: fullPath });
|
|
417
388
|
|
|
418
389
|
const name = entry.split("/").pop()!.slice(0, -3); // Remove .md extension
|
|
419
390
|
|
|
@@ -429,9 +400,9 @@ async function loadTemplatesFromDir(
|
|
|
429
400
|
}
|
|
430
401
|
|
|
431
402
|
// Get description from frontmatter or first non-empty line
|
|
432
|
-
let description = frontmatter.description || "";
|
|
403
|
+
let description = String(frontmatter.description || "");
|
|
433
404
|
if (!description) {
|
|
434
|
-
const firstLine =
|
|
405
|
+
const firstLine = body.split("\n").find((line) => line.trim());
|
|
435
406
|
if (firstLine) {
|
|
436
407
|
// Truncate if too long
|
|
437
408
|
description = firstLine.slice(0, 60);
|
|
@@ -445,7 +416,7 @@ async function loadTemplatesFromDir(
|
|
|
445
416
|
templates.push({
|
|
446
417
|
name,
|
|
447
418
|
description,
|
|
448
|
-
content,
|
|
419
|
+
content: body,
|
|
449
420
|
source: sourceStr,
|
|
450
421
|
});
|
|
451
422
|
}
|
package/src/core/sdk.ts
CHANGED
|
@@ -855,6 +855,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
855
855
|
toolRegistry.set(tool.name, wrapToolWithExtensions(tool, extensionRunner));
|
|
856
856
|
}
|
|
857
857
|
}
|
|
858
|
+
if (model?.provider === "cursor") {
|
|
859
|
+
toolRegistry.delete("edit");
|
|
860
|
+
}
|
|
858
861
|
time("combineTools");
|
|
859
862
|
|
|
860
863
|
let cursorEventEmitter: ((event: AgentEvent) => void) | undefined;
|
package/src/core/skills.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { skillCapability } from "../capability/skill";
|
|
|
6
6
|
import type { SourceMeta } from "../capability/types";
|
|
7
7
|
import type { Skill as CapabilitySkill, SkillFrontmatter as ImportedSkillFrontmatter } from "../discovery";
|
|
8
8
|
import { loadCapability } from "../discovery";
|
|
9
|
-
import { parseFrontmatter } from "
|
|
9
|
+
import { parseFrontmatter } from "./frontmatter";
|
|
10
10
|
import { logger } from "./logger";
|
|
11
11
|
import type { SkillsSettings } from "./settings-manager";
|
|
12
12
|
|
|
@@ -54,7 +54,7 @@ export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkills
|
|
|
54
54
|
if (seenPaths.has(skillFile)) return;
|
|
55
55
|
try {
|
|
56
56
|
const content = readFileSync(skillFile, "utf-8");
|
|
57
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
57
|
+
const { frontmatter } = parseFrontmatter(content, { source: skillFile });
|
|
58
58
|
const name = (frontmatter.name as string) || dirName;
|
|
59
59
|
const description = frontmatter.description as string;
|
|
60
60
|
|
|
@@ -118,7 +118,7 @@ function scanDirectoryForSkills(dir: string): LoadSkillsResult {
|
|
|
118
118
|
if (seenPaths.has(skillFile)) return;
|
|
119
119
|
try {
|
|
120
120
|
const content = readFileSync(skillFile, "utf-8");
|
|
121
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
121
|
+
const { frontmatter } = parseFrontmatter(content, { source: skillFile });
|
|
122
122
|
const name = (frontmatter.name as string) || dirName;
|
|
123
123
|
const description = frontmatter.description as string;
|
|
124
124
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { slashCommandCapability } from "../capability/slash-command";
|
|
2
2
|
import type { SlashCommand } from "../discovery";
|
|
3
3
|
import { loadCapability } from "../discovery";
|
|
4
|
-
import { parseFrontmatter } from "
|
|
4
|
+
import { parseFrontmatter } from "./frontmatter";
|
|
5
5
|
import { renderPromptTemplate } from "./prompt-templates";
|
|
6
6
|
import { EMBEDDED_COMMAND_TEMPLATES } from "./tools/task/commands";
|
|
7
7
|
|
|
@@ -19,8 +19,11 @@ export interface FileSlashCommand {
|
|
|
19
19
|
|
|
20
20
|
const EMBEDDED_SLASH_COMMANDS = EMBEDDED_COMMAND_TEMPLATES;
|
|
21
21
|
|
|
22
|
-
function parseCommandTemplate(
|
|
23
|
-
|
|
22
|
+
function parseCommandTemplate(
|
|
23
|
+
content: string,
|
|
24
|
+
options: { source: string; level?: "off" | "warn" | "fatal" },
|
|
25
|
+
): { description: string; body: string } {
|
|
26
|
+
const { frontmatter, body } = parseFrontmatter(content, options);
|
|
24
27
|
const frontmatterDesc = typeof frontmatter.description === "string" ? frontmatter.description.trim() : "";
|
|
25
28
|
|
|
26
29
|
// Get description from frontmatter or first non-empty line
|
|
@@ -112,7 +115,10 @@ export async function loadSlashCommands(options: LoadSlashCommandsOptions = {}):
|
|
|
112
115
|
const result = await loadCapability<SlashCommand>(slashCommandCapability.id, { cwd: options.cwd });
|
|
113
116
|
|
|
114
117
|
const fileCommands: FileSlashCommand[] = result.items.map((cmd) => {
|
|
115
|
-
const { description, body } = parseCommandTemplate(cmd.content
|
|
118
|
+
const { description, body } = parseCommandTemplate(cmd.content, {
|
|
119
|
+
source: cmd.path ?? `slash-command:${cmd.name}`,
|
|
120
|
+
level: cmd.level === "native" ? "fatal" : "warn",
|
|
121
|
+
});
|
|
116
122
|
|
|
117
123
|
// Format source label: "via ProviderName Level"
|
|
118
124
|
const capitalizedLevel = cmd.level.charAt(0).toUpperCase() + cmd.level.slice(1);
|
|
@@ -132,7 +138,10 @@ export async function loadSlashCommands(options: LoadSlashCommandsOptions = {}):
|
|
|
132
138
|
const name = cmd.name.replace(/\.md$/, "");
|
|
133
139
|
if (seenNames.has(name)) continue;
|
|
134
140
|
|
|
135
|
-
const { description, body } = parseCommandTemplate(cmd.content
|
|
141
|
+
const { description, body } = parseCommandTemplate(cmd.content, {
|
|
142
|
+
source: `embedded:${cmd.name}`,
|
|
143
|
+
level: "fatal",
|
|
144
|
+
});
|
|
136
145
|
fileCommands.push({
|
|
137
146
|
name,
|
|
138
147
|
description,
|
package/src/core/tools/bash.ts
CHANGED
|
@@ -15,6 +15,8 @@ import { resolveToCwd } from "./path-utils";
|
|
|
15
15
|
import { createToolUIKit } from "./render-utils";
|
|
16
16
|
import { DEFAULT_MAX_BYTES, formatSize, type TruncationResult, truncateTail } from "./truncate";
|
|
17
17
|
|
|
18
|
+
export const BASH_DEFAULT_PREVIEW_LINES = 10;
|
|
19
|
+
|
|
18
20
|
const bashSchema = Type.Object({
|
|
19
21
|
command: Type.String({ description: "Bash command to execute" }),
|
|
20
22
|
timeout: Type.Optional(Type.Number({ description: "Timeout in seconds (optional, no default timeout)" })),
|
|
@@ -26,6 +28,7 @@ const bashSchema = Type.Object({
|
|
|
26
28
|
export interface BashToolDetails {
|
|
27
29
|
truncation?: TruncationResult;
|
|
28
30
|
fullOutputPath?: string;
|
|
31
|
+
fullOutput?: string;
|
|
29
32
|
}
|
|
30
33
|
|
|
31
34
|
/**
|
|
@@ -101,9 +104,12 @@ export function createBashTool(session: ToolSession, options?: BashToolOptions):
|
|
|
101
104
|
const truncation = truncateTail(currentOutput);
|
|
102
105
|
onUpdate({
|
|
103
106
|
content: [{ type: "text", text: truncation.content || "" }],
|
|
104
|
-
details:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
+
details: truncation.truncated
|
|
108
|
+
? {
|
|
109
|
+
truncation,
|
|
110
|
+
fullOutput: currentOutput,
|
|
111
|
+
}
|
|
112
|
+
: undefined,
|
|
107
113
|
});
|
|
108
114
|
}
|
|
109
115
|
},
|
|
@@ -129,6 +135,7 @@ export function createBashTool(session: ToolSession, options?: BashToolOptions):
|
|
|
129
135
|
details = {
|
|
130
136
|
truncation,
|
|
131
137
|
fullOutputPath: result.fullOutputPath,
|
|
138
|
+
fullOutput: currentOutput,
|
|
132
139
|
};
|
|
133
140
|
|
|
134
141
|
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
|
@@ -173,6 +180,9 @@ interface BashRenderContext {
|
|
|
173
180
|
previewLines?: number;
|
|
174
181
|
}
|
|
175
182
|
|
|
183
|
+
// Preview line limit when not expanded (matches tool-execution behavior)
|
|
184
|
+
export const BASH_PREVIEW_LINES = 10;
|
|
185
|
+
|
|
176
186
|
export const bashToolRenderer = {
|
|
177
187
|
renderCall(args: BashRenderArgs, uiTheme: Theme): Component {
|
|
178
188
|
const ui = createToolUIKit(uiTheme);
|
|
@@ -214,21 +224,25 @@ export const bashToolRenderer = {
|
|
|
214
224
|
const { renderContext } = options;
|
|
215
225
|
const details = result.details;
|
|
216
226
|
|
|
227
|
+
const expanded = renderContext?.expanded ?? options.expanded;
|
|
228
|
+
const previewLines = renderContext?.previewLines ?? BASH_DEFAULT_PREVIEW_LINES;
|
|
229
|
+
|
|
217
230
|
// Get output from context (preferred) or fall back to result content
|
|
218
231
|
const output = renderContext?.output ?? (result.content?.find((c) => c.type === "text")?.text ?? "").trim();
|
|
219
|
-
const
|
|
220
|
-
const
|
|
232
|
+
const fullOutput = details?.fullOutput;
|
|
233
|
+
const displayOutput = expanded ? (fullOutput ?? output) : output;
|
|
234
|
+
const showingFullOutput = expanded && fullOutput !== undefined;
|
|
221
235
|
|
|
222
236
|
// Build truncation warning lines (static, doesn't depend on width)
|
|
223
237
|
const truncation = details?.truncation;
|
|
224
238
|
const fullOutputPath = details?.fullOutputPath;
|
|
225
239
|
let warningLine: string | undefined;
|
|
226
|
-
if (truncation?.truncated
|
|
240
|
+
if (fullOutputPath || (truncation?.truncated && !showingFullOutput)) {
|
|
227
241
|
const warnings: string[] = [];
|
|
228
242
|
if (fullOutputPath) {
|
|
229
243
|
warnings.push(`Full output: ${fullOutputPath}`);
|
|
230
244
|
}
|
|
231
|
-
if (truncation?.truncated) {
|
|
245
|
+
if (truncation?.truncated && !showingFullOutput) {
|
|
232
246
|
if (truncation.truncatedBy === "lines") {
|
|
233
247
|
warnings.push(`Truncated: showing ${truncation.outputLines} of ${truncation.totalLines} lines`);
|
|
234
248
|
} else {
|
|
@@ -237,17 +251,19 @@ export const bashToolRenderer = {
|
|
|
237
251
|
);
|
|
238
252
|
}
|
|
239
253
|
}
|
|
240
|
-
|
|
254
|
+
if (warnings.length > 0) {
|
|
255
|
+
warningLine = uiTheme.fg("warning", ui.wrapBrackets(warnings.join(". ")));
|
|
256
|
+
}
|
|
241
257
|
}
|
|
242
258
|
|
|
243
|
-
if (!
|
|
259
|
+
if (!displayOutput) {
|
|
244
260
|
// No output - just show warning if any
|
|
245
261
|
return new Text(warningLine ?? "", 0, 0);
|
|
246
262
|
}
|
|
247
263
|
|
|
248
264
|
if (expanded) {
|
|
249
265
|
// Show all lines when expanded
|
|
250
|
-
const styledOutput =
|
|
266
|
+
const styledOutput = displayOutput
|
|
251
267
|
.split("\n")
|
|
252
268
|
.map((line) => uiTheme.fg("toolOutput", line))
|
|
253
269
|
.join("\n");
|
|
@@ -256,7 +272,7 @@ export const bashToolRenderer = {
|
|
|
256
272
|
}
|
|
257
273
|
|
|
258
274
|
// Collapsed: use width-aware caching component
|
|
259
|
-
const styledOutput =
|
|
275
|
+
const styledOutput = displayOutput
|
|
260
276
|
.split("\n")
|
|
261
277
|
.map((line) => uiTheme.fg("toolOutput", line))
|
|
262
278
|
.join("\n");
|
|
@@ -478,7 +478,7 @@ export const calculatorToolRenderer = {
|
|
|
478
478
|
const hasMore = outputs.length > maxItems;
|
|
479
479
|
const icon = uiTheme.styledSymbol("status.success", "success");
|
|
480
480
|
const summary = uiTheme.fg("dim", formatCount("result", outputs.length));
|
|
481
|
-
const expandHint = formatExpandHint(expanded, hasMore
|
|
481
|
+
const expandHint = formatExpandHint(uiTheme, expanded, hasMore);
|
|
482
482
|
let text = `${icon} ${summary}${expandHint}`;
|
|
483
483
|
|
|
484
484
|
// Render each result as a tree branch
|
package/src/core/tools/edit.ts
CHANGED
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
import type { ToolSession } from "./index";
|
|
22
22
|
import { createLspWritethrough, type FileDiagnosticsResult, writethroughNoop } from "./lsp/index";
|
|
23
23
|
import { resolveToCwd } from "./path-utils";
|
|
24
|
-
import { createToolUIKit, getDiffStats, shortenPath, truncateDiffByHunk } from "./render-utils";
|
|
24
|
+
import { createToolUIKit, formatExpandHint, getDiffStats, shortenPath, truncateDiffByHunk } from "./render-utils";
|
|
25
25
|
|
|
26
26
|
const editSchema = Type.Object({
|
|
27
27
|
path: Type.String({ description: "Path to the file to edit (relative or absolute)" }),
|
|
@@ -308,7 +308,7 @@ export const editToolRenderer = {
|
|
|
308
308
|
if (hiddenLines > 0) remainder.push(`${hiddenLines} more lines`);
|
|
309
309
|
text += uiTheme.fg(
|
|
310
310
|
"toolOutput",
|
|
311
|
-
`\n${uiTheme.format.ellipsis} (${remainder.join(", ")}) ${
|
|
311
|
+
`\n${uiTheme.format.ellipsis} (${remainder.join(", ")}) ${formatExpandHint(uiTheme)}`,
|
|
312
312
|
);
|
|
313
313
|
}
|
|
314
314
|
}
|
|
@@ -13,9 +13,9 @@ import {
|
|
|
13
13
|
formatCount,
|
|
14
14
|
formatExpandHint,
|
|
15
15
|
formatMoreItems,
|
|
16
|
+
formatStatusIcon,
|
|
16
17
|
getDomain,
|
|
17
18
|
getPreviewLines,
|
|
18
|
-
getStyledStatusIcon,
|
|
19
19
|
PREVIEW_LIMITS,
|
|
20
20
|
TRUNCATE_LENGTHS,
|
|
21
21
|
truncate,
|
|
@@ -32,14 +32,14 @@ const MAX_HIGHLIGHT_LEN = TRUNCATE_LENGTHS.CONTENT;
|
|
|
32
32
|
function renderErrorMessage(message: string, theme: Theme): Text {
|
|
33
33
|
const clean = message.replace(/^Error:\s*/, "").trim();
|
|
34
34
|
return new Text(
|
|
35
|
-
`${
|
|
35
|
+
`${formatStatusIcon("error", theme)} ${theme.fg("error", `Error: ${clean || "Unknown error"}`)}`,
|
|
36
36
|
0,
|
|
37
37
|
0,
|
|
38
38
|
);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function renderEmptyMessage(message: string, theme: Theme): Text {
|
|
42
|
-
return new Text(`${
|
|
42
|
+
return new Text(`${formatStatusIcon("warning", theme)} ${theme.fg("muted", message)}`, 0, 0);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/** Render Exa result with tree-based layout */
|
|
@@ -64,9 +64,9 @@ export function renderExaResult(
|
|
|
64
64
|
const maxLines = expanded ? rawLines.length : Math.min(rawLines.length, COLLAPSED_PREVIEW_LINES);
|
|
65
65
|
const displayLines = rawLines.slice(0, maxLines);
|
|
66
66
|
const remaining = rawLines.length - maxLines;
|
|
67
|
-
const expandHint = formatExpandHint(expanded, remaining > 0
|
|
67
|
+
const expandHint = formatExpandHint(uiTheme, expanded, remaining > 0);
|
|
68
68
|
|
|
69
|
-
let text = `${
|
|
69
|
+
let text = `${formatStatusIcon("info", uiTheme)} ${uiTheme.fg("dim", "Raw response")}${expandHint}`;
|
|
70
70
|
|
|
71
71
|
for (let i = 0; i < displayLines.length; i++) {
|
|
72
72
|
const isLast = i === displayLines.length - 1 && remaining === 0;
|
|
@@ -78,7 +78,10 @@ export function renderExaResult(
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
if (remaining > 0) {
|
|
81
|
-
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
|
|
81
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
|
|
82
|
+
"muted",
|
|
83
|
+
formatMoreItems(remaining, "line", uiTheme),
|
|
84
|
+
)}`;
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
return new Text(text, 0, 0);
|
|
@@ -91,7 +94,7 @@ export function renderExaResult(
|
|
|
91
94
|
const cost = response.costDollars?.total;
|
|
92
95
|
const time = response.searchTime;
|
|
93
96
|
|
|
94
|
-
const icon =
|
|
97
|
+
const icon = formatStatusIcon(resultCount > 0 ? "success" : "warning", uiTheme);
|
|
95
98
|
|
|
96
99
|
const metaParts = [formatCount("result", resultCount)];
|
|
97
100
|
if (cost !== undefined) metaParts.push(`cost:$${cost.toFixed(4)}`);
|
|
@@ -104,7 +107,7 @@ export function renderExaResult(
|
|
|
104
107
|
const totalLines = previewText.split("\n").filter((l) => l.trim()).length;
|
|
105
108
|
hasMorePreview = totalLines > COLLAPSED_PREVIEW_LINES || resultCount > 1;
|
|
106
109
|
}
|
|
107
|
-
const expandHint = formatExpandHint(expanded, hasMorePreview
|
|
110
|
+
const expandHint = formatExpandHint(uiTheme, expanded, hasMorePreview);
|
|
108
111
|
|
|
109
112
|
let text = `${icon} ${uiTheme.fg("dim", summaryText)}${expandHint}`;
|
|
110
113
|
|
|
@@ -165,11 +168,17 @@ export function renderExaResult(
|
|
|
165
168
|
text += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("accent", title)}${domainPart}`;
|
|
166
169
|
|
|
167
170
|
if (res.url) {
|
|
168
|
-
text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
|
|
171
|
+
text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
|
|
172
|
+
"mdLinkUrl",
|
|
173
|
+
res.url,
|
|
174
|
+
)}`;
|
|
169
175
|
}
|
|
170
176
|
|
|
171
177
|
if (res.author) {
|
|
172
|
-
text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
|
|
178
|
+
text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
|
|
179
|
+
"muted",
|
|
180
|
+
`Author: ${res.author}`,
|
|
181
|
+
)}`;
|
|
173
182
|
}
|
|
174
183
|
|
|
175
184
|
if (res.publishedDate) {
|
|
@@ -197,7 +206,10 @@ export function renderExaResult(
|
|
|
197
206
|
}
|
|
198
207
|
|
|
199
208
|
if (res.highlights?.length) {
|
|
200
|
-
text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
|
|
209
|
+
text += `\n ${uiTheme.fg("dim", cont)} ${uiTheme.fg("dim", uiTheme.tree.hook)} ${uiTheme.fg(
|
|
210
|
+
"accent",
|
|
211
|
+
"Highlights",
|
|
212
|
+
)}`;
|
|
201
213
|
const maxHighlights = Math.min(res.highlights.length, 3);
|
|
202
214
|
for (let j = 0; j < maxHighlights; j++) {
|
|
203
215
|
const h = res.highlights[j];
|
|
@@ -29,6 +29,7 @@ describe("createTools", () => {
|
|
|
29
29
|
expect(names).toContain("lsp");
|
|
30
30
|
expect(names).toContain("notebook");
|
|
31
31
|
expect(names).toContain("task");
|
|
32
|
+
expect(names).toContain("todo_write");
|
|
32
33
|
expect(names).toContain("output");
|
|
33
34
|
expect(names).toContain("web_fetch");
|
|
34
35
|
expect(names).toContain("web_search");
|
|
@@ -164,6 +165,7 @@ describe("createTools", () => {
|
|
|
164
165
|
"output",
|
|
165
166
|
"read",
|
|
166
167
|
"task",
|
|
168
|
+
"todo_write",
|
|
167
169
|
"web_fetch",
|
|
168
170
|
"web_search",
|
|
169
171
|
"write",
|
package/src/core/tools/index.ts
CHANGED
|
@@ -28,6 +28,7 @@ export { createReadTool, type ReadToolDetails } from "./read";
|
|
|
28
28
|
export { reportFindingTool, type SubmitReviewDetails } from "./review";
|
|
29
29
|
export { createSshTool, type SSHToolDetails } from "./ssh";
|
|
30
30
|
export { BUNDLED_AGENTS, createTaskTool, taskTool } from "./task/index";
|
|
31
|
+
export { createTodoWriteTool, type TodoItem, type TodoWriteToolDetails } from "./todo-write";
|
|
31
32
|
export {
|
|
32
33
|
DEFAULT_MAX_BYTES,
|
|
33
34
|
DEFAULT_MAX_LINES,
|
|
@@ -79,6 +80,7 @@ import { createReadTool } from "./read";
|
|
|
79
80
|
import { reportFindingTool } from "./review";
|
|
80
81
|
import { createSshTool } from "./ssh";
|
|
81
82
|
import { createTaskTool } from "./task/index";
|
|
83
|
+
import { createTodoWriteTool } from "./todo-write";
|
|
82
84
|
import { createWebFetchTool } from "./web-fetch";
|
|
83
85
|
import { createWebSearchTool } from "./web-search/index";
|
|
84
86
|
import { createWriteTool } from "./write";
|
|
@@ -145,6 +147,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
145
147
|
output: createOutputTool,
|
|
146
148
|
read: createReadTool,
|
|
147
149
|
task: createTaskTool,
|
|
150
|
+
todo_write: createTodoWriteTool,
|
|
148
151
|
web_fetch: createWebFetchTool,
|
|
149
152
|
web_search: createWebSearchTool,
|
|
150
153
|
write: createWriteTool,
|
|
@@ -108,12 +108,7 @@ function convertSchema(schema: unknown): unknown {
|
|
|
108
108
|
if (!jsonType) {
|
|
109
109
|
return { type: schema.type };
|
|
110
110
|
}
|
|
111
|
-
|
|
112
|
-
// Add format for timestamp
|
|
113
|
-
if (schema.type === "timestamp") {
|
|
114
|
-
result.format = "date-time";
|
|
115
|
-
}
|
|
116
|
-
return result;
|
|
111
|
+
return { type: jsonType };
|
|
117
112
|
}
|
|
118
113
|
|
|
119
114
|
// Enum form: { enum: ["a", "b"] } → { enum: ["a", "b"] }
|
package/src/core/tools/ls.ts
CHANGED
|
@@ -265,7 +265,7 @@ export const lsToolRenderer = {
|
|
|
265
265
|
);
|
|
266
266
|
const maxEntries = expanded ? entries.length : Math.min(entries.length, COLLAPSED_LIST_LIMIT);
|
|
267
267
|
const hasMoreEntries = entries.length > maxEntries;
|
|
268
|
-
const expandHint = formatExpandHint(expanded, hasMoreEntries
|
|
268
|
+
const expandHint = formatExpandHint(uiTheme, expanded, hasMoreEntries);
|
|
269
269
|
|
|
270
270
|
let text = `${icon} ${uiTheme.fg("dim", summaryText)}${formatTruncationSuffix(truncated, uiTheme)}${expandHint}`;
|
|
271
271
|
|
|
@@ -302,7 +302,10 @@ export const lsToolRenderer = {
|
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
if (hasTruncation) {
|
|
305
|
-
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
|
|
305
|
+
text += `\n ${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg(
|
|
306
|
+
"warning",
|
|
307
|
+
`truncated: ${truncationReasons.join(", ")}`,
|
|
308
|
+
)}`;
|
|
306
309
|
}
|
|
307
310
|
|
|
308
311
|
return new Text(text, 0, 0);
|