@oh-my-pi/pi-coding-agent 14.6.1 → 14.6.3
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 +82 -1
- package/README.md +21 -0
- package/package.json +23 -7
- package/src/cli/grievances-cli.ts +89 -4
- package/src/commands/grievances.ts +33 -7
- package/src/config/prompt-templates.ts +14 -7
- package/src/config/settings-schema.ts +595 -100
- package/src/config/settings.ts +46 -0
- package/src/discovery/helpers.ts +13 -6
- package/src/edit/index.ts +3 -3
- package/src/edit/line-hash.ts +73 -25
- package/src/edit/modes/hashline.lark +10 -3
- package/src/edit/modes/hashline.ts +104 -38
- package/src/edit/renderer.ts +3 -3
- package/src/hindsight/backend.ts +444 -0
- package/src/hindsight/bank.ts +131 -0
- package/src/hindsight/client.ts +445 -0
- package/src/hindsight/config.ts +165 -0
- package/src/hindsight/content.ts +205 -0
- package/src/hindsight/index.ts +6 -0
- package/src/hindsight/retain-queue.ts +166 -0
- package/src/hindsight/transcript.ts +71 -0
- package/src/main.ts +7 -10
- package/src/memories/index.ts +1 -1
- package/src/memory-backend/index.ts +4 -0
- package/src/memory-backend/local-backend.ts +30 -0
- package/src/memory-backend/off-backend.ts +16 -0
- package/src/memory-backend/resolve.ts +24 -0
- package/src/memory-backend/types.ts +69 -0
- package/src/modes/components/settings-defs.ts +50 -451
- package/src/modes/components/settings-selector.ts +4 -2
- package/src/modes/components/status-line/presets.ts +1 -1
- package/src/modes/components/status-line.ts +4 -1
- package/src/modes/controllers/command-controller.ts +6 -5
- package/src/modes/controllers/event-controller.ts +12 -0
- package/src/modes/controllers/mcp-command-controller.ts +23 -0
- package/src/modes/controllers/selector-controller.ts +10 -12
- package/src/modes/interactive-mode.ts +3 -2
- package/src/modes/theme/theme.ts +4 -0
- package/src/prompts/tools/github.md +3 -0
- package/src/prompts/tools/hashline.md +20 -16
- package/src/prompts/tools/read.md +10 -6
- package/src/prompts/tools/recall.md +5 -0
- package/src/prompts/tools/reflect.md +5 -0
- package/src/prompts/tools/retain.md +5 -0
- package/src/prompts/tools/search.md +1 -1
- package/src/sdk.ts +12 -9
- package/src/session/agent-session.ts +75 -3
- package/src/slash-commands/builtin-registry.ts +2 -12
- package/src/ssh/connection-manager.ts +1 -1
- package/src/tools/ast-edit.ts +14 -5
- package/src/tools/ast-grep.ts +12 -3
- package/src/tools/find.ts +47 -7
- package/src/tools/gh-renderer.ts +10 -1
- package/src/tools/gh.ts +233 -5
- package/src/tools/hindsight-recall.ts +70 -0
- package/src/tools/hindsight-reflect.ts +57 -0
- package/src/tools/hindsight-retain.ts +63 -0
- package/src/tools/index.ts +17 -0
- package/src/tools/output-meta.ts +1 -0
- package/src/tools/path-utils.ts +55 -0
- package/src/tools/read.ts +1 -1
- package/src/tools/search.ts +45 -8
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
3
|
+
import { getHindsightSessionState } from "../hindsight/backend";
|
|
4
|
+
import { enqueueRetain } from "../hindsight/retain-queue";
|
|
5
|
+
import retainDescription from "../prompts/tools/retain.md" with { type: "text" };
|
|
6
|
+
import type { ToolSession } from ".";
|
|
7
|
+
|
|
8
|
+
const hindsightRetainSchema = Type.Object({
|
|
9
|
+
items: Type.Array(
|
|
10
|
+
Type.Object({
|
|
11
|
+
content: Type.String({
|
|
12
|
+
description: "The information to remember. Be specific and self-contained — include who, what, when, why.",
|
|
13
|
+
}),
|
|
14
|
+
context: Type.Optional(
|
|
15
|
+
Type.String({ description: "Optional context describing where this information came from." }),
|
|
16
|
+
),
|
|
17
|
+
}),
|
|
18
|
+
{
|
|
19
|
+
minItems: 1,
|
|
20
|
+
description:
|
|
21
|
+
"One or more memories to retain. Batch related facts in a single call rather than calling retain repeatedly — they are deduplicated and consolidated together.",
|
|
22
|
+
},
|
|
23
|
+
),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
export type HindsightRetainParams = Static<typeof hindsightRetainSchema>;
|
|
27
|
+
export class HindsightRetainTool implements AgentTool<typeof hindsightRetainSchema> {
|
|
28
|
+
readonly name = "retain";
|
|
29
|
+
readonly label = "Retain";
|
|
30
|
+
readonly description = retainDescription;
|
|
31
|
+
readonly parameters = hindsightRetainSchema;
|
|
32
|
+
readonly strict = true;
|
|
33
|
+
|
|
34
|
+
constructor(private readonly session: ToolSession) {}
|
|
35
|
+
|
|
36
|
+
static createIf(session: ToolSession): HindsightRetainTool | null {
|
|
37
|
+
if (session.settings.get("memory.backend") !== "hindsight") return null;
|
|
38
|
+
return new HindsightRetainTool(session);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async execute(_id: string, params: HindsightRetainParams): Promise<AgentToolResult> {
|
|
42
|
+
const sessionId = this.session.getSessionId?.();
|
|
43
|
+
const state = sessionId ? getHindsightSessionState(sessionId) : undefined;
|
|
44
|
+
if (!state || !sessionId) {
|
|
45
|
+
throw new Error("Hindsight backend is not initialised for this session.");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Push every item onto the global queue and return immediately. The
|
|
49
|
+
// queue flushes either when it reaches its batch threshold or when its
|
|
50
|
+
// debounce timer fires. If the eventual batch fails, the queue
|
|
51
|
+
// surfaces a UI-only warning notice — the LLM is not informed.
|
|
52
|
+
for (const item of params.items) {
|
|
53
|
+
enqueueRetain(sessionId, item.content, item.context);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const count = params.items.length;
|
|
57
|
+
const noun = count === 1 ? "memory" : "memories";
|
|
58
|
+
return {
|
|
59
|
+
content: [{ type: "text", text: `${count} ${noun} queued.` }],
|
|
60
|
+
details: { count },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -30,6 +30,9 @@ import { EvalTool } from "./eval";
|
|
|
30
30
|
import { ExitPlanModeTool } from "./exit-plan-mode";
|
|
31
31
|
import { FindTool } from "./find";
|
|
32
32
|
import { GithubTool } from "./gh";
|
|
33
|
+
import { HindsightRecallTool } from "./hindsight-recall";
|
|
34
|
+
import { HindsightReflectTool } from "./hindsight-reflect";
|
|
35
|
+
import { HindsightRetainTool } from "./hindsight-retain";
|
|
33
36
|
import { InspectImageTool } from "./inspect-image";
|
|
34
37
|
import { IrcTool } from "./irc";
|
|
35
38
|
import { JobTool } from "./job";
|
|
@@ -69,6 +72,9 @@ export * from "./eval";
|
|
|
69
72
|
export * from "./exit-plan-mode";
|
|
70
73
|
export * from "./find";
|
|
71
74
|
export * from "./gh";
|
|
75
|
+
export * from "./hindsight-recall";
|
|
76
|
+
export * from "./hindsight-reflect";
|
|
77
|
+
export * from "./hindsight-retain";
|
|
72
78
|
export * from "./image-gen";
|
|
73
79
|
export * from "./inspect-image";
|
|
74
80
|
export * from "./irc";
|
|
@@ -231,6 +237,9 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
231
237
|
web_search: s => new WebSearchTool(s),
|
|
232
238
|
search_tool_bm25: SearchToolBm25Tool.createIf,
|
|
233
239
|
write: s => new WriteTool(s),
|
|
240
|
+
retain: HindsightRetainTool.createIf,
|
|
241
|
+
recall: HindsightRecallTool.createIf,
|
|
242
|
+
reflect: HindsightReflectTool.createIf,
|
|
234
243
|
};
|
|
235
244
|
|
|
236
245
|
export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
|
|
@@ -342,6 +351,11 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
342
351
|
) {
|
|
343
352
|
requestedTools.push("recipe");
|
|
344
353
|
}
|
|
354
|
+
if (session.settings.get("memory.backend") === "hindsight") {
|
|
355
|
+
for (const name of ["recall", "retain", "reflect"]) {
|
|
356
|
+
if (!requestedTools.includes(name)) requestedTools.push(name);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
345
359
|
}
|
|
346
360
|
const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
|
|
347
361
|
const isToolAllowed = (name: string) => {
|
|
@@ -365,6 +379,9 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
365
379
|
if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
|
|
366
380
|
if (name === "irc") return session.settings.get("irc.enabled");
|
|
367
381
|
if (name === "recipe") return session.settings.get("recipe.enabled");
|
|
382
|
+
if (name === "retain" || name === "recall" || name === "reflect") {
|
|
383
|
+
return session.settings.get("memory.backend") === "hindsight";
|
|
384
|
+
}
|
|
368
385
|
if (name === "task") {
|
|
369
386
|
const maxDepth = session.settings.get("task.maxRecursionDepth") ?? 2;
|
|
370
387
|
const currentDepth = session.taskDepth ?? 0;
|
package/src/tools/output-meta.ts
CHANGED
|
@@ -465,6 +465,7 @@ async function spillLargeResultToArtifact(
|
|
|
465
465
|
): Promise<AgentToolResult> {
|
|
466
466
|
const sessionManager = context?.sessionManager;
|
|
467
467
|
if (!sessionManager) return result;
|
|
468
|
+
if (toolName === "read") return result;
|
|
468
469
|
const { threshold, tailBytes, tailLines } = getSpillConfig(context?.settings);
|
|
469
470
|
|
|
470
471
|
// Skip if tool already saved an artifact
|
package/src/tools/path-utils.ts
CHANGED
|
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import * as url from "node:url";
|
|
5
|
+
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
5
6
|
|
|
6
7
|
const UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
|
|
7
8
|
const NARROW_NO_BREAK_SPACE = "\u202F";
|
|
@@ -442,6 +443,60 @@ export async function resolveExplicitFindPatterns(
|
|
|
442
443
|
return resolveFindPatternItems([...new Set(patternItems)], cwd);
|
|
443
444
|
}
|
|
444
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Result of partitioning a list of user-supplied paths/globs into entries whose
|
|
448
|
+
* base directory currently exists on disk versus those that do not.
|
|
449
|
+
*
|
|
450
|
+
* Used by multi-path tools (search, find, ast_grep, ast_edit) to tolerate one
|
|
451
|
+
* or more missing entries in a multi-path call: the surviving entries should
|
|
452
|
+
* still be searched, with the missing entries surfaced as a non-fatal warning.
|
|
453
|
+
*/
|
|
454
|
+
export interface PartitionedPaths {
|
|
455
|
+
/** Raw input strings whose resolved base path exists. */
|
|
456
|
+
valid: string[];
|
|
457
|
+
/** Raw input strings whose resolved base path is missing (ENOENT). */
|
|
458
|
+
missing: string[];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Stat each input's base path concurrently; return entries split by existence.
|
|
463
|
+
*
|
|
464
|
+
* `splitter` is expected to be {@link parseFindPattern} or
|
|
465
|
+
* {@link parseSearchPath}: both return a `basePath` field that this helper
|
|
466
|
+
* resolves against `cwd` and stats. ENOENT is the only swallowed error — every
|
|
467
|
+
* other stat failure (permission, IO, etc.) propagates so callers do not silently
|
|
468
|
+
* skip paths that exist but are unreadable.
|
|
469
|
+
*
|
|
470
|
+
* Order of `valid` and `missing` follows the input order, so callers can rely
|
|
471
|
+
* on `valid[0]` matching the first surviving user-supplied entry.
|
|
472
|
+
*/
|
|
473
|
+
export async function partitionExistingPaths(
|
|
474
|
+
items: string[],
|
|
475
|
+
cwd: string,
|
|
476
|
+
splitter: (item: string) => { basePath: string },
|
|
477
|
+
): Promise<PartitionedPaths> {
|
|
478
|
+
const settled = await Promise.all(
|
|
479
|
+
items.map(async item => {
|
|
480
|
+
const { basePath } = splitter(item);
|
|
481
|
+
const absoluteBasePath = resolveToCwd(basePath, cwd);
|
|
482
|
+
try {
|
|
483
|
+
await fs.promises.stat(absoluteBasePath);
|
|
484
|
+
return { item, exists: true } as const;
|
|
485
|
+
} catch (err) {
|
|
486
|
+
if (isEnoent(err)) return { item, exists: false } as const;
|
|
487
|
+
throw err;
|
|
488
|
+
}
|
|
489
|
+
}),
|
|
490
|
+
);
|
|
491
|
+
const valid: string[] = [];
|
|
492
|
+
const missing: string[] = [];
|
|
493
|
+
for (const entry of settled) {
|
|
494
|
+
if (entry.exists) valid.push(entry.item);
|
|
495
|
+
else missing.push(entry.item);
|
|
496
|
+
}
|
|
497
|
+
return { valid, missing };
|
|
498
|
+
}
|
|
499
|
+
|
|
445
500
|
export function resolveReadPath(filePath: string, cwd: string): string {
|
|
446
501
|
const resolved = resolveToCwd(filePath, cwd);
|
|
447
502
|
const shellEscapedVariant = tryShellEscapedPath(resolved);
|
package/src/tools/read.ts
CHANGED
|
@@ -472,7 +472,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
472
472
|
this.description = prompt.render(readDescription, {
|
|
473
473
|
DEFAULT_LIMIT: String(this.#defaultLimit),
|
|
474
474
|
DEFAULT_MAX_LINES: String(DEFAULT_MAX_LINES),
|
|
475
|
-
|
|
475
|
+
IS_HL_MODE: displayMode.hashLines,
|
|
476
476
|
IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
|
|
477
477
|
});
|
|
478
478
|
}
|
package/src/tools/search.ts
CHANGED
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
hasGlobPathChars,
|
|
23
23
|
normalizePathLikeInput,
|
|
24
24
|
parseSearchPath,
|
|
25
|
+
partitionExistingPaths,
|
|
25
26
|
resolveExplicitSearchPaths,
|
|
26
27
|
resolveToCwd,
|
|
27
28
|
} from "./path-utils";
|
|
@@ -68,6 +69,10 @@ export interface SearchToolDetails {
|
|
|
68
69
|
* `result.text` lines but uses a `│` gutter and `*` to mark match lines (vs space for
|
|
69
70
|
* context). The TUI uses this directly so it never parses model-facing hashline anchors. */
|
|
70
71
|
displayContent?: string;
|
|
72
|
+
/** User-supplied paths whose base directory was missing on disk. The tool
|
|
73
|
+
* skipped these and continued with the surviving entries; surfaced as a
|
|
74
|
+
* non-fatal warning in the renderer and in the model-facing text. */
|
|
75
|
+
missingPaths?: string[];
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
type SearchParams = Static<typeof searchSchema>;
|
|
@@ -82,7 +87,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
82
87
|
constructor(private readonly session: ToolSession) {
|
|
83
88
|
const displayMode = resolveFileDisplayMode(session);
|
|
84
89
|
this.description = prompt.render(searchDescription, {
|
|
85
|
-
|
|
90
|
+
IS_HL_MODE: displayMode.hashLines,
|
|
86
91
|
IS_LINE_NUMBER_MODE: !displayMode.hashLines && displayMode.lineNumbers,
|
|
87
92
|
});
|
|
88
93
|
}
|
|
@@ -140,13 +145,26 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
140
145
|
}
|
|
141
146
|
resolvedPathInputs.push(resource.sourcePath);
|
|
142
147
|
}
|
|
143
|
-
|
|
144
|
-
|
|
148
|
+
// Tolerate missing entries in a multi-path call: skip ones whose base
|
|
149
|
+
// directory is gone, and only error if every entry is missing. Single
|
|
150
|
+
// missing path keeps the original ENOENT semantics.
|
|
151
|
+
let missingPaths: string[] = [];
|
|
152
|
+
let effectivePaths = resolvedPathInputs;
|
|
153
|
+
if (resolvedPathInputs.length > 1) {
|
|
154
|
+
const partition = await partitionExistingPaths(resolvedPathInputs, this.session.cwd, parseSearchPath);
|
|
155
|
+
if (partition.valid.length === 0) {
|
|
156
|
+
throw new ToolError(`Path not found: ${partition.missing.join(", ")}`);
|
|
157
|
+
}
|
|
158
|
+
effectivePaths = partition.valid;
|
|
159
|
+
missingPaths = partition.missing;
|
|
160
|
+
}
|
|
161
|
+
if (effectivePaths.length === 1) {
|
|
162
|
+
const parsedPath = parseSearchPath(effectivePaths[0] ?? ".");
|
|
145
163
|
searchPath = resolveToCwd(parsedPath.basePath, this.session.cwd);
|
|
146
164
|
globFilter = parsedPath.glob;
|
|
147
165
|
scopePath = formatScopePath(searchPath);
|
|
148
166
|
} else {
|
|
149
|
-
const multiSearchPath = await resolveExplicitSearchPaths(
|
|
167
|
+
const multiSearchPath = await resolveExplicitSearchPaths(effectivePaths, this.session.cwd, globFilter);
|
|
150
168
|
if (!multiSearchPath) {
|
|
151
169
|
throw new ToolError("`paths` must contain at least one path or glob");
|
|
152
170
|
}
|
|
@@ -285,6 +303,8 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
285
303
|
const limitMessage = `Result limit reached; narrow paths or use skip=${nextSkip}.`;
|
|
286
304
|
const { record: recordFile, list: fileList } = createFileRecorder();
|
|
287
305
|
const fileMatchCounts = new Map<string, number>();
|
|
306
|
+
const missingPathsNote =
|
|
307
|
+
missingPaths.length > 0 ? `Skipped missing paths: ${missingPaths.join(", ")}` : undefined;
|
|
288
308
|
if (selectedMatches.length === 0) {
|
|
289
309
|
const details: SearchToolDetails = {
|
|
290
310
|
scopePath,
|
|
@@ -292,8 +312,10 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
292
312
|
fileCount: 0,
|
|
293
313
|
files: [],
|
|
294
314
|
truncated: false,
|
|
315
|
+
missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
|
|
295
316
|
};
|
|
296
|
-
|
|
317
|
+
const text = missingPathsNote ? `No matches found\n${missingPathsNote}` : "No matches found";
|
|
318
|
+
return toolResult(details).text(text).done();
|
|
297
319
|
}
|
|
298
320
|
const outputLines: string[] = [];
|
|
299
321
|
let linesTruncated = false;
|
|
@@ -365,6 +387,9 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
365
387
|
if (matchLimitReached || result.limitReached) {
|
|
366
388
|
outputLines.push("", limitMessage);
|
|
367
389
|
}
|
|
390
|
+
if (missingPathsNote) {
|
|
391
|
+
outputLines.push("", missingPathsNote);
|
|
392
|
+
}
|
|
368
393
|
const rawOutput = outputLines.join("\n");
|
|
369
394
|
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
370
395
|
const output = truncation.content;
|
|
@@ -382,6 +407,7 @@ export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDeta
|
|
|
382
407
|
matchLimitReached: matchLimitReached ? effectiveLimit : undefined,
|
|
383
408
|
resultLimitReached: result.limitReached ? internalLimit : undefined,
|
|
384
409
|
displayContent: displayLines.join("\n"),
|
|
410
|
+
missingPaths: missingPaths.length > 0 ? missingPaths : undefined,
|
|
385
411
|
};
|
|
386
412
|
if (truncation.truncated) details.truncation = truncation;
|
|
387
413
|
if (linesTruncated) details.linesTruncated = true;
|
|
@@ -487,12 +513,20 @@ export const searchToolRenderer = {
|
|
|
487
513
|
details?.truncated || truncation || limits?.matchLimit || limits?.resultLimit || limits?.columnTruncated,
|
|
488
514
|
);
|
|
489
515
|
|
|
516
|
+
const missingPathsList = details?.missingPaths ?? [];
|
|
517
|
+
const missingNote =
|
|
518
|
+
missingPathsList.length > 0
|
|
519
|
+
? uiTheme.fg("warning", `skipped missing: ${missingPathsList.join(", ")}`)
|
|
520
|
+
: undefined;
|
|
521
|
+
|
|
490
522
|
if (matchCount === 0) {
|
|
491
523
|
const header = renderStatusLine(
|
|
492
524
|
{ icon: "warning", title: "Search", description: args?.pattern, meta: ["0 matches"] },
|
|
493
525
|
uiTheme,
|
|
494
526
|
);
|
|
495
|
-
|
|
527
|
+
const lines = [header, formatEmptyMessage("No matches found", uiTheme)];
|
|
528
|
+
if (missingNote) lines.push(missingNote);
|
|
529
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
496
530
|
}
|
|
497
531
|
|
|
498
532
|
const summaryParts = [formatCount("match", matchCount), formatCount("file", fileCount)];
|
|
@@ -538,8 +572,11 @@ export const searchToolRenderer = {
|
|
|
538
572
|
if (limits?.columnTruncated) truncationReasons.push(`line length ${limits.columnTruncated.maxColumn}`);
|
|
539
573
|
if (truncation?.artifactId) truncationReasons.push(formatFullOutputReference(truncation.artifactId));
|
|
540
574
|
|
|
541
|
-
const extraLines =
|
|
542
|
-
|
|
575
|
+
const extraLines: string[] = [];
|
|
576
|
+
if (truncationReasons.length > 0) {
|
|
577
|
+
extraLines.push(uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`));
|
|
578
|
+
}
|
|
579
|
+
if (missingNote) extraLines.push(missingNote);
|
|
543
580
|
|
|
544
581
|
let cached: RenderCache | undefined;
|
|
545
582
|
return {
|