@oh-my-pi/pi-coding-agent 4.3.2 → 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 +43 -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/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 +61 -24
- 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 +18 -2
- 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
|
@@ -128,7 +128,7 @@ function renderHover(
|
|
|
128
128
|
// Collapsed view
|
|
129
129
|
const firstCodeLine = codeLines[0] || "";
|
|
130
130
|
const hasMore = codeLines.length > 1 || Boolean(afterCode);
|
|
131
|
-
const expandHint = formatExpandHint(
|
|
131
|
+
const expandHint = formatExpandHint(theme, expanded, hasMore);
|
|
132
132
|
|
|
133
133
|
let output = `${icon}${langLabel}${expandHint}`;
|
|
134
134
|
const h = theme.boxSharp.horizontal;
|
|
@@ -237,7 +237,10 @@ function renderDiagnostics(
|
|
|
237
237
|
}
|
|
238
238
|
const severityColor = severityToColor(item.severity);
|
|
239
239
|
const location = formatDiagnosticLocation(item.file, item.line, item.col, theme);
|
|
240
|
-
output += `\n ${theme.fg("dim", branch)} ${theme.fg(severityColor, location)} ${theme.fg(
|
|
240
|
+
output += `\n ${theme.fg("dim", branch)} ${theme.fg(severityColor, location)} ${theme.fg(
|
|
241
|
+
"dim",
|
|
242
|
+
`[${item.severity}]`,
|
|
243
|
+
)}`;
|
|
241
244
|
if (item.message) {
|
|
242
245
|
output += `\n ${theme.fg("dim", detailPrefix)}${theme.fg(
|
|
243
246
|
"muted",
|
|
@@ -253,7 +256,7 @@ function renderDiagnostics(
|
|
|
253
256
|
parsedDiagnostics.length > 0 ? parsedDiagnostics.slice(0, 3) : fallbackDiagnostics.slice(0, 3);
|
|
254
257
|
const remaining =
|
|
255
258
|
(parsedDiagnostics.length > 0 ? parsedDiagnostics.length : fallbackDiagnostics.length) - previewItems.length;
|
|
256
|
-
const expandHint = formatExpandHint(
|
|
259
|
+
const expandHint = formatExpandHint(theme, expanded, remaining > 0);
|
|
257
260
|
let output = `${icon} ${theme.fg("dim", meta.join(theme.sep.dot))}${expandHint}`;
|
|
258
261
|
for (let i = 0; i < previewItems.length; i++) {
|
|
259
262
|
const item = previewItems[i];
|
|
@@ -271,7 +274,10 @@ function renderDiagnostics(
|
|
|
271
274
|
output += `\n ${theme.fg("dim", branch)} ${theme.fg(severityColor, location)}${message}`;
|
|
272
275
|
}
|
|
273
276
|
if (remaining > 0) {
|
|
274
|
-
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
277
|
+
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
278
|
+
"muted",
|
|
279
|
+
`${theme.format.ellipsis} ${remaining} more`,
|
|
280
|
+
)}`;
|
|
275
281
|
}
|
|
276
282
|
|
|
277
283
|
return new Text(output, 0, 0);
|
|
@@ -305,7 +311,7 @@ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded:
|
|
|
305
311
|
const files = Array.from(byFile.keys());
|
|
306
312
|
|
|
307
313
|
const renderGrouped = (maxFiles: number, maxLocsPerFile: number, showHint: boolean): string => {
|
|
308
|
-
const expandHint = formatExpandHint(
|
|
314
|
+
const expandHint = formatExpandHint(theme, undefined, showHint);
|
|
309
315
|
let output = `${icon} ${theme.fg("dim", `${refCount} found`)}${expandHint}`;
|
|
310
316
|
|
|
311
317
|
const filesToShow = files.slice(0, maxFiles);
|
|
@@ -326,7 +332,10 @@ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded:
|
|
|
326
332
|
const isLastLoc = li === locsToShow.length - 1 && locs.length <= maxLocsPerFile;
|
|
327
333
|
const locBranch = isLastLoc ? theme.tree.last : theme.tree.branch;
|
|
328
334
|
const locCont = isLastLoc ? " " : `${theme.tree.vertical} `;
|
|
329
|
-
output += `\n ${theme.fg("dim", fileCont)}${theme.fg("dim", locBranch)} ${theme.fg(
|
|
335
|
+
output += `\n ${theme.fg("dim", fileCont)}${theme.fg("dim", locBranch)} ${theme.fg(
|
|
336
|
+
"muted",
|
|
337
|
+
`line ${line}, col ${col}`,
|
|
338
|
+
)}`;
|
|
330
339
|
if (expanded) {
|
|
331
340
|
const context = `at ${file}:${line}:${col}`;
|
|
332
341
|
output += `\n ${theme.fg("dim", fileCont)}${theme.fg("dim", locCont)}${theme.fg(
|
|
@@ -345,7 +354,10 @@ function renderReferences(refMatch: RegExpMatchArray, lines: string[], expanded:
|
|
|
345
354
|
}
|
|
346
355
|
|
|
347
356
|
if (files.length > maxFiles) {
|
|
348
|
-
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
357
|
+
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
358
|
+
"muted",
|
|
359
|
+
formatMoreItems(files.length - maxFiles, "file", theme),
|
|
360
|
+
)}`;
|
|
349
361
|
}
|
|
350
362
|
|
|
351
363
|
return output;
|
|
@@ -439,7 +451,7 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
|
|
|
439
451
|
// Collapsed: show first 3 top-level symbols
|
|
440
452
|
const topLevel = symbols.filter((s) => s.indent === 0).slice(0, 3);
|
|
441
453
|
const hasMoreSymbols = symbols.length > topLevel.length;
|
|
442
|
-
const expandHint = formatExpandHint(
|
|
454
|
+
const expandHint = formatExpandHint(theme, expanded, hasMoreSymbols);
|
|
443
455
|
let output = `${icon} ${theme.fg("dim", `in ${fileName}`)}${expandHint}`;
|
|
444
456
|
for (let i = 0; i < topLevel.length; i++) {
|
|
445
457
|
const sym = topLevel[i];
|
|
@@ -451,7 +463,10 @@ function renderSymbols(symbolsMatch: RegExpMatchArray, lines: string[], expanded
|
|
|
451
463
|
)}`;
|
|
452
464
|
}
|
|
453
465
|
if (topLevelCount > 3) {
|
|
454
|
-
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
466
|
+
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
467
|
+
"muted",
|
|
468
|
+
`${theme.format.ellipsis} ${topLevelCount - 3} more`,
|
|
469
|
+
)}`;
|
|
455
470
|
}
|
|
456
471
|
|
|
457
472
|
return new Text(output, 0, 0);
|
|
@@ -486,8 +501,11 @@ function renderGeneric(text: string, lines: string[], expanded: boolean, theme:
|
|
|
486
501
|
}
|
|
487
502
|
|
|
488
503
|
const firstLine = lines[0] || "No output";
|
|
489
|
-
const expandHint = formatExpandHint(
|
|
490
|
-
let output = `${icon} ${theme.fg(
|
|
504
|
+
const expandHint = formatExpandHint(theme, expanded, lines.length > 1);
|
|
505
|
+
let output = `${icon} ${theme.fg(
|
|
506
|
+
"dim",
|
|
507
|
+
truncate(firstLine, TRUNCATE_LENGTHS.TITLE, theme.format.ellipsis),
|
|
508
|
+
)}${expandHint}`;
|
|
491
509
|
|
|
492
510
|
if (lines.length > 1) {
|
|
493
511
|
const previewLines = lines.slice(1, 4);
|
|
@@ -500,7 +518,10 @@ function renderGeneric(text: string, lines: string[], expanded: boolean, theme:
|
|
|
500
518
|
)}`;
|
|
501
519
|
}
|
|
502
520
|
if (lines.length > 4) {
|
|
503
|
-
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
521
|
+
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
522
|
+
"muted",
|
|
523
|
+
formatMoreItems(lines.length - 4, "line", theme),
|
|
524
|
+
)}`;
|
|
504
525
|
}
|
|
505
526
|
}
|
|
506
527
|
|
|
@@ -278,7 +278,7 @@ export const notebookToolRenderer = {
|
|
|
278
278
|
if (totalCells !== undefined) summaryParts.push(`${totalCells} total`);
|
|
279
279
|
const summaryText = summaryParts.join(uiTheme.sep.dot);
|
|
280
280
|
|
|
281
|
-
const expandHint =
|
|
281
|
+
const expandHint = expanded || !canExpand ? "" : formatExpandHint(uiTheme);
|
|
282
282
|
let text = `${icon} ${uiTheme.fg("dim", summaryText)}${expandHint}`;
|
|
283
283
|
|
|
284
284
|
if (cellSource) {
|
package/src/core/tools/output.ts
CHANGED
|
@@ -470,7 +470,7 @@ export const outputToolRenderer = {
|
|
|
470
470
|
const maxOutputs = expanded ? outputs.length : Math.min(outputs.length, 5);
|
|
471
471
|
const hasMoreOutputs = outputs.length > maxOutputs;
|
|
472
472
|
const hasMorePreview = outputs.some((o) => (o.previewLines?.length ?? 0) > previewLimit);
|
|
473
|
-
const expandHint = formatExpandHint(expanded, hasMoreOutputs || hasMorePreview
|
|
473
|
+
const expandHint = formatExpandHint(uiTheme, expanded, hasMoreOutputs || hasMorePreview);
|
|
474
474
|
let text = `${icon} ${uiTheme.fg("dim", summary)}${expandHint}`;
|
|
475
475
|
|
|
476
476
|
for (let i = 0; i < maxOutputs; i++) {
|
package/src/core/tools/read.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { Component } from "@oh-my-pi/pi-tui";
|
|
|
6
6
|
import { Text } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { Type } from "@sinclair/typebox";
|
|
8
8
|
import { CONFIG_DIR_NAME } from "../../config";
|
|
9
|
-
import {
|
|
9
|
+
import type { Theme } from "../../modes/interactive/theme/theme";
|
|
10
10
|
import readDescription from "../../prompts/tools/read.md" with { type: "text" };
|
|
11
11
|
import { formatDimensionNote, resizeImage } from "../../utils/image-resize";
|
|
12
12
|
import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime";
|
|
@@ -17,7 +17,7 @@ import type { ToolSession } from "../sdk";
|
|
|
17
17
|
import { ScopeSignal, untilAborted } from "../utils";
|
|
18
18
|
import { createLsTool } from "./ls";
|
|
19
19
|
import { resolveReadPath, resolveToCwd } from "./path-utils";
|
|
20
|
-
import {
|
|
20
|
+
import { shortenPath, wrapBrackets } from "./render-utils";
|
|
21
21
|
import {
|
|
22
22
|
DEFAULT_MAX_BYTES,
|
|
23
23
|
DEFAULT_MAX_LINES,
|
|
@@ -657,17 +657,6 @@ interface ReadRenderArgs {
|
|
|
657
657
|
limit?: number;
|
|
658
658
|
}
|
|
659
659
|
|
|
660
|
-
const IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "ico", "bmp", "tiff"]);
|
|
661
|
-
const BINARY_EXTENSIONS = new Set(["pdf", "zip", "tar", "gz", "exe", "dll", "so", "dylib", "wasm"]);
|
|
662
|
-
|
|
663
|
-
function getFileType(filePath: string): "image" | "binary" | "text" {
|
|
664
|
-
const ext = filePath.split(".").pop()?.toLowerCase();
|
|
665
|
-
if (!ext) return "text";
|
|
666
|
-
if (IMAGE_EXTENSIONS.has(ext)) return "image";
|
|
667
|
-
if (BINARY_EXTENSIONS.has(ext)) return "binary";
|
|
668
|
-
return "text";
|
|
669
|
-
}
|
|
670
|
-
|
|
671
660
|
export const readToolRenderer = {
|
|
672
661
|
renderCall(args: ReadRenderArgs, uiTheme: Theme): Component {
|
|
673
662
|
const rawPath = args.file_path || args.path || "";
|
|
@@ -688,49 +677,26 @@ export const readToolRenderer = {
|
|
|
688
677
|
|
|
689
678
|
renderResult(
|
|
690
679
|
result: { content: Array<{ type: string; text?: string }>; details?: ReadToolDetails },
|
|
691
|
-
|
|
680
|
+
_options: RenderResultOptions,
|
|
692
681
|
uiTheme: Theme,
|
|
693
|
-
|
|
682
|
+
_args?: ReadRenderArgs,
|
|
694
683
|
): Component {
|
|
695
|
-
const rawPath = args?.file_path || args?.path || "";
|
|
696
|
-
const fileType = getFileType(rawPath);
|
|
697
684
|
const details = result.details;
|
|
698
685
|
const lines: string[] = [];
|
|
699
686
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
if (fileType === "image") {
|
|
703
|
-
lines.push(uiTheme.fg("muted", "Image rendered below"));
|
|
704
|
-
} else if (fileType === "binary") {
|
|
705
|
-
// Binary files just show the header from renderCall
|
|
706
|
-
} else {
|
|
707
|
-
// Text file
|
|
708
|
-
const lang = getLanguageFromPath(rawPath);
|
|
709
|
-
const contentLines = lang ? highlightCode(replaceTabs(output), lang) : output.split("\n");
|
|
710
|
-
|
|
711
|
-
if (expanded) {
|
|
712
|
-
lines.push(
|
|
713
|
-
...contentLines.map((line: string) =>
|
|
714
|
-
lang ? replaceTabs(line) : uiTheme.fg("toolOutput", replaceTabs(line)),
|
|
715
|
-
),
|
|
716
|
-
);
|
|
717
|
-
} else {
|
|
718
|
-
lines.push(uiTheme.fg("dim", `${uiTheme.nav.expand} Ctrl+O to show content`));
|
|
719
|
-
}
|
|
687
|
+
lines.push(uiTheme.fg("dim", "Content hidden"));
|
|
720
688
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
}
|
|
730
|
-
warning = `Truncated: ${truncation.outputLines} lines (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`;
|
|
731
|
-
}
|
|
732
|
-
lines.push(uiTheme.fg("warning", wrapBrackets(warning, uiTheme)));
|
|
689
|
+
const truncation = details?.truncation;
|
|
690
|
+
if (truncation?.truncated) {
|
|
691
|
+
let warning: string;
|
|
692
|
+
if (truncation.firstLineExceedsLimit) {
|
|
693
|
+
warning = `First line exceeds ${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit`;
|
|
694
|
+
} else if (truncation.truncatedBy === "lines") {
|
|
695
|
+
warning = `Truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${truncation.maxLines ?? DEFAULT_MAX_LINES} line limit)`;
|
|
696
|
+
} else {
|
|
697
|
+
warning = `Truncated: ${truncation.outputLines} lines (${formatSize(truncation.maxBytes ?? DEFAULT_MAX_BYTES)} limit)`;
|
|
733
698
|
}
|
|
699
|
+
lines.push(uiTheme.fg("warning", wrapBrackets(warning, uiTheme)));
|
|
734
700
|
}
|
|
735
701
|
|
|
736
702
|
return new Text(lines.join("\n"), 0, 0);
|
|
@@ -41,7 +41,7 @@ export const TRUNCATE_LENGTHS = {
|
|
|
41
41
|
} as const;
|
|
42
42
|
|
|
43
43
|
/** Standard expand hint text */
|
|
44
|
-
export const EXPAND_HINT = "(Ctrl+O
|
|
44
|
+
export const EXPAND_HINT = "(Ctrl+O for more)";
|
|
45
45
|
|
|
46
46
|
// =============================================================================
|
|
47
47
|
// Text Truncation Utilities
|
|
@@ -148,7 +148,7 @@ export function formatAge(ageSeconds: number | null | undefined): string {
|
|
|
148
148
|
* Get the appropriate status icon with color for a given state.
|
|
149
149
|
* Standardizes status icon usage across all renderers.
|
|
150
150
|
*/
|
|
151
|
-
export function
|
|
151
|
+
export function formatStatusIcon(status: ToolUIStatus, theme: Theme, spinnerFrame?: number): string {
|
|
152
152
|
switch (status) {
|
|
153
153
|
case "success":
|
|
154
154
|
return theme.styledSymbol("status.success", "success");
|
|
@@ -175,8 +175,10 @@ export function getStyledStatusIcon(status: ToolUIStatus, theme: Theme, spinnerF
|
|
|
175
175
|
* Format the expand hint with proper theming.
|
|
176
176
|
* Returns empty string if already expanded or there is nothing more to show.
|
|
177
177
|
*/
|
|
178
|
-
export function formatExpandHint(
|
|
179
|
-
|
|
178
|
+
export function formatExpandHint(theme: Theme, expanded?: boolean, hasMore?: boolean): string {
|
|
179
|
+
if (expanded) return "";
|
|
180
|
+
if (hasMore === false) return "";
|
|
181
|
+
return theme.fg("dim", wrapBrackets(EXPAND_HINT, theme));
|
|
180
182
|
}
|
|
181
183
|
|
|
182
184
|
/**
|
|
@@ -267,13 +269,13 @@ export function createToolUIKit(theme: Theme): ToolUIKit {
|
|
|
267
269
|
meta: (meta) => formatMeta(meta, theme),
|
|
268
270
|
count: (label, count) => formatCount(label, count),
|
|
269
271
|
moreItems: (remaining, itemType) => formatMoreItems(remaining, itemType, theme),
|
|
270
|
-
expandHint: (expanded, hasMore) => formatExpandHint(expanded, hasMore
|
|
272
|
+
expandHint: (expanded, hasMore) => formatExpandHint(theme, expanded, hasMore),
|
|
271
273
|
scope: (scopePath) => formatScope(scopePath, theme),
|
|
272
274
|
truncationSuffix: (truncated) => formatTruncationSuffix(truncated, theme),
|
|
273
275
|
errorMessage: (message) => formatErrorMessage(message, theme),
|
|
274
276
|
emptyMessage: (message) => formatEmptyMessage(message, theme),
|
|
275
277
|
badge: (label, color) => formatBadge(label, color, theme),
|
|
276
|
-
statusIcon: (status, spinnerFrame) =>
|
|
278
|
+
statusIcon: (status, spinnerFrame) => formatStatusIcon(status, theme, spinnerFrame),
|
|
277
279
|
wrapBrackets: (text) => wrapBrackets(text, theme),
|
|
278
280
|
truncate: (text, maxLen) => truncate(text, maxLen, theme.format.ellipsis),
|
|
279
281
|
previewLines: (text, maxLines, maxLineLen) => getPreviewLines(text, maxLines, maxLineLen, theme.format.ellipsis),
|
|
@@ -342,26 +344,56 @@ export function formatDiagnostics(
|
|
|
342
344
|
let output = `\n\n${headerIcon} ${theme.fg("toolTitle", "Diagnostics")} ${theme.fg("dim", `(${diag.summary})`)}`;
|
|
343
345
|
|
|
344
346
|
const maxDiags = expanded ? diag.messages.length : 5;
|
|
345
|
-
let
|
|
347
|
+
let diagsShown = 0;
|
|
346
348
|
|
|
347
349
|
const files = Array.from(byFile.entries());
|
|
348
|
-
|
|
350
|
+
|
|
351
|
+
// Count total diagnostics for "... X more" calculation
|
|
352
|
+
const totalParsedDiags = files.reduce((sum, [, diags]) => sum + diags.length, 0);
|
|
353
|
+
const totalDiags = totalParsedDiags + unparsed.length;
|
|
354
|
+
|
|
355
|
+
// Helper to check if this is the very last item in the tree
|
|
356
|
+
const isTreeEnd = (fileIdx: number, diagIdx: number | null, unparsedIdx: number | null): boolean => {
|
|
357
|
+
const willShowMore = totalDiags > diagsShown + 1;
|
|
358
|
+
if (willShowMore) return false;
|
|
359
|
+
|
|
360
|
+
if (unparsedIdx !== null) {
|
|
361
|
+
return unparsedIdx === unparsed.length - 1;
|
|
362
|
+
}
|
|
363
|
+
if (diagIdx !== null) {
|
|
364
|
+
const isLastDiagInFile = diagIdx === files[fileIdx][1].length - 1;
|
|
365
|
+
const isLastFile = fileIdx === files.length - 1;
|
|
366
|
+
return isLastDiagInFile && isLastFile && unparsed.length === 0;
|
|
367
|
+
}
|
|
368
|
+
// File node - never the tree end if it has diagnostics
|
|
369
|
+
return false;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
for (let fi = 0; fi < files.length && diagsShown < maxDiags; fi++) {
|
|
349
373
|
const [filePath, diagnostics] = files[fi];
|
|
350
|
-
|
|
351
|
-
const
|
|
374
|
+
// File is "last" only if no more files AND no unparsed AND we'll show all diags AND no "... X more"
|
|
375
|
+
const remainingDiagsInFile = diagnostics.length;
|
|
376
|
+
const remainingDiagsAfter = files.slice(fi + 1).reduce((sum, [, d]) => sum + d.length, 0) + unparsed.length;
|
|
377
|
+
const willShowAllRemaining = diagsShown + remainingDiagsInFile + remainingDiagsAfter <= maxDiags;
|
|
378
|
+
const isLastFileNode = fi === files.length - 1 && unparsed.length === 0 && willShowAllRemaining;
|
|
379
|
+
const fileBranch = isLastFileNode ? theme.tree.last : theme.tree.branch;
|
|
352
380
|
|
|
353
381
|
const fileIcon = theme.fg("muted", getLangIcon(filePath));
|
|
354
382
|
output += `\n ${theme.fg("dim", fileBranch)} ${fileIcon} ${theme.fg("accent", filePath)}`;
|
|
355
|
-
shown++;
|
|
356
383
|
|
|
357
|
-
for (let di = 0; di < diagnostics.length &&
|
|
384
|
+
for (let di = 0; di < diagnostics.length && diagsShown < maxDiags; di++) {
|
|
358
385
|
const d = diagnostics[di];
|
|
359
|
-
const
|
|
360
|
-
|
|
361
|
-
|
|
386
|
+
const isLastDiagInFile = di === diagnostics.length - 1;
|
|
387
|
+
// This is the last visible diag in file if it's actually last OR we're about to hit the limit
|
|
388
|
+
const atDisplayLimit = diagsShown + 1 >= maxDiags;
|
|
389
|
+
const isLastVisibleInFile = isLastDiagInFile || atDisplayLimit;
|
|
390
|
+
// Check if this is the last visible item in the entire tree
|
|
391
|
+
const isVeryLast = isTreeEnd(fi, di, null);
|
|
392
|
+
const diagBranch = isLastFileNode
|
|
393
|
+
? isLastVisibleInFile || isVeryLast
|
|
362
394
|
? ` ${theme.tree.last}`
|
|
363
395
|
: ` ${theme.tree.branch}`
|
|
364
|
-
:
|
|
396
|
+
: isLastVisibleInFile || isVeryLast
|
|
365
397
|
? `${theme.tree.vertical} ${theme.tree.last}`
|
|
366
398
|
: `${theme.tree.vertical} ${theme.tree.branch}`;
|
|
367
399
|
|
|
@@ -376,20 +408,25 @@ export function formatDiagnostics(
|
|
|
376
408
|
const msgColor = d.severity === "error" ? "error" : d.severity === "warning" ? "warning" : "toolOutput";
|
|
377
409
|
|
|
378
410
|
output += `\n ${theme.fg("dim", diagBranch)} ${sevIcon}${location} ${theme.fg(msgColor, d.message)}${codeTag}`;
|
|
379
|
-
|
|
411
|
+
diagsShown++;
|
|
380
412
|
}
|
|
381
413
|
}
|
|
382
414
|
|
|
383
|
-
for (
|
|
384
|
-
|
|
415
|
+
for (let ui = 0; ui < unparsed.length && diagsShown < maxDiags; ui++) {
|
|
416
|
+
const msg = unparsed[ui];
|
|
417
|
+
const isVeryLast = isTreeEnd(-1, null, ui);
|
|
418
|
+
const branch = isVeryLast ? theme.tree.last : theme.tree.branch;
|
|
385
419
|
const color = msg.includes("[error]") ? "error" : msg.includes("[warning]") ? "warning" : "dim";
|
|
386
|
-
output += `\n ${theme.fg("dim",
|
|
387
|
-
|
|
420
|
+
output += `\n ${theme.fg("dim", branch)} ${theme.fg(color, msg)}`;
|
|
421
|
+
diagsShown++;
|
|
388
422
|
}
|
|
389
423
|
|
|
390
|
-
if (
|
|
391
|
-
const remaining =
|
|
392
|
-
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
424
|
+
if (totalDiags > diagsShown) {
|
|
425
|
+
const remaining = totalDiags - diagsShown;
|
|
426
|
+
output += `\n ${theme.fg("dim", theme.tree.last)} ${theme.fg(
|
|
427
|
+
"muted",
|
|
428
|
+
`${theme.format.ellipsis} ${remaining} more`,
|
|
429
|
+
)} ${formatExpandHint(theme)}`;
|
|
393
430
|
}
|
|
394
431
|
|
|
395
432
|
return output;
|
|
@@ -20,6 +20,7 @@ import { outputToolRenderer } from "./output";
|
|
|
20
20
|
import { readToolRenderer } from "./read";
|
|
21
21
|
import { sshToolRenderer } from "./ssh";
|
|
22
22
|
import { taskToolRenderer } from "./task/render";
|
|
23
|
+
import { todoWriteToolRenderer } from "./todo-write";
|
|
23
24
|
import { webFetchToolRenderer } from "./web-fetch";
|
|
24
25
|
import { webSearchToolRenderer } from "./web-search/render";
|
|
25
26
|
import { writeToolRenderer } from "./write";
|
|
@@ -49,6 +50,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
49
50
|
read: readToolRenderer as ToolRenderer,
|
|
50
51
|
ssh: sshToolRenderer as ToolRenderer,
|
|
51
52
|
task: taskToolRenderer as ToolRenderer,
|
|
53
|
+
todo_write: todoWriteToolRenderer as ToolRenderer,
|
|
52
54
|
web_fetch: webFetchToolRenderer as ToolRenderer,
|
|
53
55
|
web_search: webSearchToolRenderer as ToolRenderer,
|
|
54
56
|
write: writeToolRenderer as ToolRenderer,
|