@oh-my-pi/pi-coding-agent 15.9.5 → 15.10.0
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 +98 -1
- package/dist/types/cli/args.d.ts +1 -1
- package/dist/types/cli/gallery-cli.d.ts +43 -0
- package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
- package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
- package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
- package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
- package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
- package/dist/types/cli/gallery-screenshot.d.ts +35 -0
- package/dist/types/commands/gallery.d.ts +47 -0
- package/dist/types/config/keybindings.d.ts +10 -2
- package/dist/types/config/model-id-affixes.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +8 -1
- package/dist/types/config/settings-schema.d.ts +43 -7
- package/dist/types/edit/file-snapshot-store.d.ts +1 -1
- package/dist/types/eval/backend.d.ts +6 -6
- package/dist/types/eval/bridge-timeout.d.ts +27 -0
- package/dist/types/eval/idle-timeout.d.ts +16 -14
- package/dist/types/eval/js/executor.d.ts +3 -3
- package/dist/types/eval/py/executor.d.ts +2 -2
- package/dist/types/eval/py/spawn-options.d.ts +58 -0
- package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
- package/dist/types/lsp/types.d.ts +10 -0
- package/dist/types/main.d.ts +3 -2
- package/dist/types/memory-backend/index.d.ts +2 -1
- package/dist/types/memory-backend/resolve.d.ts +1 -1
- package/dist/types/memory-backend/types.d.ts +1 -1
- package/dist/types/modes/components/assistant-message.d.ts +5 -0
- package/dist/types/modes/components/copy-selector.d.ts +22 -0
- package/dist/types/modes/components/custom-editor.d.ts +2 -1
- package/dist/types/modes/components/model-selector.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +18 -0
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/index.d.ts +5 -4
- package/dist/types/modes/interactive-mode.d.ts +2 -2
- package/dist/types/modes/setup-version.d.ts +11 -0
- package/dist/types/modes/setup-wizard/index.d.ts +2 -1
- package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -2
- package/dist/types/modes/utils/copy-targets.d.ts +53 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/task/executor.d.ts +7 -0
- package/dist/types/telemetry-export.d.ts +1 -1
- package/dist/types/tools/eval-render.d.ts +1 -0
- package/dist/types/tools/fetch.d.ts +15 -7
- package/dist/types/tools/render-utils.d.ts +33 -0
- package/dist/types/tools/renderers.d.ts +16 -2
- package/dist/types/tools/search.d.ts +1 -1
- package/dist/types/tools/write.d.ts +2 -0
- package/dist/types/tui/code-cell.d.ts +6 -0
- package/dist/types/tui/output-block.d.ts +11 -0
- package/dist/types/web/scrapers/github.d.ts +22 -0
- package/dist/types/web/search/providers/perplexity.d.ts +8 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +9 -9
- package/scripts/dev-launch +42 -0
- package/scripts/dev-launch-preload.ts +19 -0
- package/src/autoresearch/dashboard.ts +11 -21
- package/src/cli/args.ts +2 -2
- package/src/cli/claude-trace-cli.ts +13 -1
- package/src/cli/gallery-cli.ts +223 -0
- package/src/cli/gallery-fixtures/agentic.ts +292 -0
- package/src/cli/gallery-fixtures/codeintel.ts +188 -0
- package/src/cli/gallery-fixtures/edit.ts +194 -0
- package/src/cli/gallery-fixtures/fs.ts +153 -0
- package/src/cli/gallery-fixtures/index.ts +40 -0
- package/src/cli/gallery-fixtures/interaction.ts +49 -0
- package/src/cli/gallery-fixtures/memory.ts +81 -0
- package/src/cli/gallery-fixtures/misc.ts +221 -0
- package/src/cli/gallery-fixtures/search.ts +213 -0
- package/src/cli/gallery-fixtures/shell.ts +167 -0
- package/src/cli/gallery-fixtures/types.ts +41 -0
- package/src/cli/gallery-fixtures/web.ts +158 -0
- package/src/cli/gallery-screenshot.ts +279 -0
- package/src/cli-commands.ts +1 -0
- package/src/commands/gallery.ts +52 -0
- package/src/commands/launch.ts +1 -1
- package/src/config/keybindings.ts +68 -2
- package/src/config/model-equivalence.ts +35 -12
- package/src/config/model-id-affixes.ts +39 -22
- package/src/config/model-registry.ts +16 -16
- package/src/config/settings-schema.ts +29 -6
- package/src/config/settings.ts +11 -0
- package/src/dap/client.ts +14 -16
- package/src/debug/raw-sse.ts +18 -4
- package/src/edit/file-snapshot-store.ts +1 -1
- package/src/edit/index.ts +1 -1
- package/src/edit/renderer.ts +43 -55
- package/src/edit/streaming.ts +1 -1
- package/src/eval/__tests__/agent-bridge.test.ts +102 -58
- package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
- package/src/eval/__tests__/idle-timeout.test.ts +26 -12
- package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
- package/src/eval/__tests__/llm-bridge.test.ts +10 -10
- package/src/eval/agent-bridge.ts +38 -12
- package/src/eval/backend.ts +6 -6
- package/src/eval/bridge-timeout.ts +44 -0
- package/src/eval/idle-timeout.ts +33 -15
- package/src/eval/js/executor.ts +10 -10
- package/src/eval/llm-bridge.ts +4 -5
- package/src/eval/py/executor.ts +6 -6
- package/src/eval/py/kernel.ts +11 -1
- package/src/eval/py/spawn-options.ts +126 -0
- package/src/export/ttsr.ts +9 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/plugins/doctor.ts +0 -1
- package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
- package/src/goals/tools/goal-tool.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/lsp/client.ts +179 -52
- package/src/lsp/index.ts +38 -4
- package/src/lsp/render.ts +3 -3
- package/src/lsp/types.ts +10 -0
- package/src/main.ts +47 -52
- package/src/memory-backend/index.ts +13 -1
- package/src/memory-backend/resolve.ts +3 -5
- package/src/memory-backend/types.ts +1 -1
- package/src/modes/components/agent-dashboard.ts +13 -4
- package/src/modes/components/assistant-message.ts +22 -1
- package/src/modes/components/copy-selector.ts +249 -0
- package/src/modes/components/custom-editor.ts +10 -1
- package/src/modes/components/extensions/extension-list.ts +17 -8
- package/src/modes/components/history-search.ts +19 -11
- package/src/modes/components/model-selector.ts +125 -29
- package/src/modes/components/oauth-selector.ts +28 -12
- package/src/modes/components/session-observer-overlay.ts +13 -15
- package/src/modes/components/session-selector.ts +24 -13
- package/src/modes/components/status-line.ts +3 -5
- package/src/modes/components/tool-execution.ts +83 -24
- package/src/modes/components/tree-selector.ts +19 -7
- package/src/modes/components/user-message-selector.ts +25 -14
- package/src/modes/controllers/command-controller.ts +13 -118
- package/src/modes/controllers/event-controller.ts +26 -10
- package/src/modes/controllers/input-controller.ts +11 -3
- package/src/modes/controllers/selector-controller.ts +40 -3
- package/src/modes/index.ts +5 -4
- package/src/modes/interactive-mode.ts +21 -7
- package/src/modes/setup-version.ts +11 -0
- package/src/modes/setup-wizard/index.ts +3 -2
- package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
- package/src/modes/theme/theme.ts +46 -10
- package/src/modes/types.ts +2 -2
- package/src/modes/utils/context-usage.ts +10 -6
- package/src/modes/utils/copy-targets.ts +254 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/sdk.ts +21 -23
- package/src/session/agent-session.ts +13 -9
- package/src/slash-commands/builtin-registry.ts +4 -12
- package/src/slash-commands/helpers/usage-report.ts +2 -0
- package/src/task/executor.ts +20 -2
- package/src/task/render.ts +37 -11
- package/src/telemetry-export.ts +25 -7
- package/src/tools/bash.ts +18 -8
- package/src/tools/browser/render.ts +5 -4
- package/src/tools/debug.ts +3 -3
- package/src/tools/eval-backends.ts +6 -17
- package/src/tools/eval-render.ts +28 -10
- package/src/tools/eval.ts +19 -23
- package/src/tools/fetch.ts +99 -89
- package/src/tools/read.ts +7 -7
- package/src/tools/render-utils.ts +63 -3
- package/src/tools/renderers.ts +16 -1
- package/src/tools/report-tool-issue.ts +1 -1
- package/src/tools/search.ts +173 -81
- package/src/tools/ssh.ts +21 -8
- package/src/tools/todo.ts +20 -7
- package/src/tools/write.ts +39 -9
- package/src/tui/code-cell.ts +19 -4
- package/src/tui/output-block.ts +14 -0
- package/src/web/scrapers/github.ts +255 -3
- package/src/web/scrapers/youtube.ts +3 -2
- package/src/web/search/providers/perplexity.ts +199 -51
- package/src/web/search/render.ts +42 -57
- package/src/web/search/types.ts +5 -1
- package/dist/types/eval/heartbeat.d.ts +0 -45
- package/src/eval/__tests__/heartbeat.test.ts +0 -84
- package/src/eval/__tests__/shared-executors.test.ts +0 -609
- package/src/eval/heartbeat.ts +0 -74
- /package/dist/types/eval/__tests__/{heartbeat.test.d.ts → bridge-timeout.test.d.ts} +0 -0
- /package/dist/types/eval/__tests__/{shared-executors.test.d.ts → kernel-spawn.test.d.ts} +0 -0
package/src/tools/search.ts
CHANGED
|
@@ -19,6 +19,8 @@ import { DEFAULT_MAX_COLUMN, type TruncationResult, truncateHead, truncateLine }
|
|
|
19
19
|
import {
|
|
20
20
|
Ellipsis,
|
|
21
21
|
fileHyperlink,
|
|
22
|
+
getTreeBranch,
|
|
23
|
+
getTreeContinuePrefix,
|
|
22
24
|
renderStatusLine,
|
|
23
25
|
renderTreeList,
|
|
24
26
|
truncateToWidth,
|
|
@@ -36,7 +38,7 @@ import {
|
|
|
36
38
|
import { createFileRecorder, formatResultPath } from "./file-recorder";
|
|
37
39
|
import { formatGroupedFiles } from "./grouped-file-output";
|
|
38
40
|
import { formatMatchLine } from "./match-line-format";
|
|
39
|
-
import {
|
|
41
|
+
import type { OutputMeta } from "./output-meta";
|
|
40
42
|
import {
|
|
41
43
|
expandDelimitedPathEntries,
|
|
42
44
|
hasGlobPathChars,
|
|
@@ -56,7 +58,9 @@ import {
|
|
|
56
58
|
formatCount,
|
|
57
59
|
formatEmptyMessage,
|
|
58
60
|
formatErrorMessage,
|
|
61
|
+
formatMoreItems,
|
|
59
62
|
PREVIEW_LIMITS,
|
|
63
|
+
replaceTabs,
|
|
60
64
|
splitGroupsByBlankLine,
|
|
61
65
|
} from "./render-utils";
|
|
62
66
|
import { ToolError } from "./tool-errors";
|
|
@@ -1169,6 +1173,10 @@ interface SearchRenderArgs {
|
|
|
1169
1173
|
}
|
|
1170
1174
|
|
|
1171
1175
|
const COLLAPSED_TEXT_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
|
|
1176
|
+
/** Line budget for the expanded view. Larger than collapsed so expanding
|
|
1177
|
+
* reveals more matches with context, but still bounded so a single hot file
|
|
1178
|
+
* whose matches span the whole file can't dump its entire length. */
|
|
1179
|
+
const EXPANDED_TEXT_LIMIT = PREVIEW_LIMITS.EXPANDED_LINES * 2;
|
|
1172
1180
|
|
|
1173
1181
|
const SEARCH_CODE_FRAME_LINE_RE = /^\s*\*?(\d+)│/;
|
|
1174
1182
|
|
|
@@ -1190,6 +1198,160 @@ function parseSearchDisplayLineNumber(line: string): number | undefined {
|
|
|
1190
1198
|
return Number.parseInt(match[1]!, 10);
|
|
1191
1199
|
}
|
|
1192
1200
|
|
|
1201
|
+
const SEARCH_MATCH_LINE_RE = /^\s*\*\d+(?:│|[:|])/;
|
|
1202
|
+
|
|
1203
|
+
interface RenderedSearchLine {
|
|
1204
|
+
raw: string;
|
|
1205
|
+
styled: string;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function isSearchMatchLine(line: string): boolean {
|
|
1209
|
+
return SEARCH_MATCH_LINE_RE.test(line);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function isSearchHeaderLine(line: string): boolean {
|
|
1213
|
+
return line.startsWith("# ") || line.startsWith("## ");
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function renderSearchDisplayGroup(
|
|
1217
|
+
group: string[],
|
|
1218
|
+
searchBase: string | undefined,
|
|
1219
|
+
uiTheme: Theme,
|
|
1220
|
+
): RenderedSearchLine[] {
|
|
1221
|
+
// Track directory/file context within a group so headers and code-frame
|
|
1222
|
+
// lines link to the backing file, with line-specific links for matches.
|
|
1223
|
+
let contextDir = searchBase ?? "";
|
|
1224
|
+
const hasFileHeader = group.some(line => line.startsWith("# "));
|
|
1225
|
+
let currentFilePath: string | undefined = hasFileHeader ? undefined : searchBase;
|
|
1226
|
+
return group.map(line => {
|
|
1227
|
+
if (line.startsWith("## ")) {
|
|
1228
|
+
// Strip optional ` (suffix)` and `#hash` before resolving.
|
|
1229
|
+
const fileName = line
|
|
1230
|
+
.slice(3)
|
|
1231
|
+
.trimEnd()
|
|
1232
|
+
.replace(/\s+\([^)]*\)\s*$/, "")
|
|
1233
|
+
.replace(/#[0-9a-f]+$/, "");
|
|
1234
|
+
const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
|
|
1235
|
+
currentFilePath = absPath;
|
|
1236
|
+
const styled = uiTheme.fg("dim", line);
|
|
1237
|
+
return { raw: line, styled: absPath ? fileHyperlink(absPath, styled) : styled };
|
|
1238
|
+
}
|
|
1239
|
+
if (line.startsWith("# ")) {
|
|
1240
|
+
const raw = line
|
|
1241
|
+
.slice(2)
|
|
1242
|
+
.trimEnd()
|
|
1243
|
+
.replace(/\s+\([^)]*\)\s*$/, "");
|
|
1244
|
+
if (INTERNAL_URL_DISPLAY_RE.test(raw)) {
|
|
1245
|
+
contextDir = "";
|
|
1246
|
+
const styled = uiTheme.fg("accent", line);
|
|
1247
|
+
const linked = linkUrlLikeSearchHeader(raw, styled);
|
|
1248
|
+
currentFilePath = linked.absPath;
|
|
1249
|
+
return { raw: line, styled: linked.line };
|
|
1250
|
+
}
|
|
1251
|
+
const isDirectory = raw.endsWith("/");
|
|
1252
|
+
const name = isDirectory ? raw.replace(/\/$/, "") : raw.replace(/#[0-9a-f]+$/, "");
|
|
1253
|
+
if (isDirectory) {
|
|
1254
|
+
const absPath = searchBase ? (name === "." ? searchBase : path.join(searchBase, name)) : undefined;
|
|
1255
|
+
if (absPath) {
|
|
1256
|
+
contextDir = absPath;
|
|
1257
|
+
}
|
|
1258
|
+
currentFilePath = undefined;
|
|
1259
|
+
const styled = uiTheme.fg("accent", line);
|
|
1260
|
+
return { raw: line, styled: absPath ? fileHyperlink(absPath, styled) : styled };
|
|
1261
|
+
}
|
|
1262
|
+
// Root-level file emitted by formatGroupedFiles when the directory is `.`.
|
|
1263
|
+
const absPath = searchBase && name ? path.join(searchBase, name) : undefined;
|
|
1264
|
+
currentFilePath = absPath;
|
|
1265
|
+
const styled = uiTheme.fg("accent", line);
|
|
1266
|
+
return { raw: line, styled: absPath ? fileHyperlink(absPath, styled) : styled };
|
|
1267
|
+
}
|
|
1268
|
+
const styled = uiTheme.fg("toolOutput", line);
|
|
1269
|
+
const lineNumber = parseSearchDisplayLineNumber(line);
|
|
1270
|
+
return {
|
|
1271
|
+
raw: line,
|
|
1272
|
+
styled:
|
|
1273
|
+
currentFilePath && lineNumber !== undefined
|
|
1274
|
+
? fileHyperlink(currentFilePath, styled, { line: lineNumber })
|
|
1275
|
+
: styled,
|
|
1276
|
+
};
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
function compactSearchPreviewGroup(group: RenderedSearchLine[]): RenderedSearchLine[] {
|
|
1281
|
+
const compact = group.filter(line => isSearchHeaderLine(line.raw) || isSearchMatchLine(line.raw));
|
|
1282
|
+
return compact.length > 0 ? compact : group;
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function countPreviewMatches(lines: readonly RenderedSearchLine[], hasMarkedMatches: boolean): number {
|
|
1286
|
+
if (hasMarkedMatches) return lines.reduce((count, line) => count + (isSearchMatchLine(line.raw) ? 1 : 0), 0);
|
|
1287
|
+
return lines.reduce((count, line) => count + (!isSearchHeaderLine(line.raw) && line.raw.length > 0 ? 1 : 0), 0);
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function renderBudgetedSearchGroups(
|
|
1291
|
+
groups: string[][],
|
|
1292
|
+
maxLines: number,
|
|
1293
|
+
matchCount: number,
|
|
1294
|
+
searchBase: string | undefined,
|
|
1295
|
+
uiTheme: Theme,
|
|
1296
|
+
compact: boolean,
|
|
1297
|
+
): string[] {
|
|
1298
|
+
if (maxLines <= 0) return [];
|
|
1299
|
+
const renderedGroups = groups
|
|
1300
|
+
.map(group => {
|
|
1301
|
+
const rendered = renderSearchDisplayGroup(group, searchBase, uiTheme);
|
|
1302
|
+
return compact ? compactSearchPreviewGroup(rendered) : rendered;
|
|
1303
|
+
})
|
|
1304
|
+
.filter(group => group.length > 0);
|
|
1305
|
+
if (renderedGroups.length === 0) return [];
|
|
1306
|
+
|
|
1307
|
+
let totalLines = 0;
|
|
1308
|
+
let totalMarkedMatches = 0;
|
|
1309
|
+
let totalFallbackMatches = 0;
|
|
1310
|
+
for (const group of renderedGroups) {
|
|
1311
|
+
totalLines += group.length;
|
|
1312
|
+
totalMarkedMatches += countPreviewMatches(group, true);
|
|
1313
|
+
totalFallbackMatches += countPreviewMatches(group, false);
|
|
1314
|
+
}
|
|
1315
|
+
const hasMarkedMatches = totalMarkedMatches > 0;
|
|
1316
|
+
const needsSummary = totalLines > maxLines;
|
|
1317
|
+
const contentBudget = needsSummary ? Math.max(maxLines - 1, 0) : maxLines;
|
|
1318
|
+
const visibleGroups: RenderedSearchLine[][] = [];
|
|
1319
|
+
let visibleLineCount = 0;
|
|
1320
|
+
let visibleMatches = 0;
|
|
1321
|
+
for (const group of renderedGroups) {
|
|
1322
|
+
if (visibleLineCount >= contentBudget) break;
|
|
1323
|
+
const available = contentBudget - visibleLineCount;
|
|
1324
|
+
const take = Math.min(group.length, available);
|
|
1325
|
+
if (take <= 0) break;
|
|
1326
|
+
const visibleGroup = group.slice(0, take);
|
|
1327
|
+
visibleGroups.push(visibleGroup);
|
|
1328
|
+
visibleLineCount += visibleGroup.length;
|
|
1329
|
+
visibleMatches += countPreviewMatches(visibleGroup, hasMarkedMatches);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const totalMatches = hasMarkedMatches ? totalMarkedMatches : Math.max(matchCount, totalFallbackMatches);
|
|
1333
|
+
const hiddenMatches = Math.max(totalMatches - visibleMatches, 0);
|
|
1334
|
+
const hiddenLines = Math.max(totalLines - visibleLineCount, 0);
|
|
1335
|
+
const hasSummary = needsSummary && (hiddenMatches > 0 || hiddenLines > 0);
|
|
1336
|
+
const lines: string[] = [];
|
|
1337
|
+
for (let i = 0; i < visibleGroups.length; i++) {
|
|
1338
|
+
const group = visibleGroups[i]!;
|
|
1339
|
+
const isLast = !hasSummary && i === visibleGroups.length - 1;
|
|
1340
|
+
const prefix = `${uiTheme.fg("dim", getTreeBranch(isLast, uiTheme))} `;
|
|
1341
|
+
const continuePrefix = uiTheme.fg("dim", getTreeContinuePrefix(isLast, uiTheme));
|
|
1342
|
+
lines.push(`${prefix}${replaceTabs(group[0]!.styled)}`);
|
|
1343
|
+
for (let j = 1; j < group.length; j++) {
|
|
1344
|
+
lines.push(`${continuePrefix}${replaceTabs(group[j]!.styled)}`);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
if (hasSummary) {
|
|
1348
|
+
const hiddenLabel =
|
|
1349
|
+
hiddenMatches > 0 ? formatMoreItems(hiddenMatches, "match") : formatMoreItems(hiddenLines, "line");
|
|
1350
|
+
lines.push(`${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.fg("muted", hiddenLabel)}`);
|
|
1351
|
+
}
|
|
1352
|
+
return lines;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1193
1355
|
export const searchToolRenderer = {
|
|
1194
1356
|
inline: true,
|
|
1195
1357
|
renderCall(args: SearchRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
@@ -1291,94 +1453,24 @@ export const searchToolRenderer = {
|
|
|
1291
1453
|
const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
|
|
1292
1454
|
const matchGroups = splitGroupsByBlankLine(textContent.split("\n"));
|
|
1293
1455
|
|
|
1294
|
-
const renderedFileLimit = details?.fileLimitReached;
|
|
1295
|
-
const renderedPerFileLimit = details?.perFileLimitReached;
|
|
1296
|
-
const truncationReasons: string[] = [];
|
|
1297
|
-
if (renderedFileLimit) truncationReasons.push(`first ${renderedFileLimit} files (skip to paginate)`);
|
|
1298
|
-
if (renderedPerFileLimit) truncationReasons.push(`first ${renderedPerFileLimit} matches per file`);
|
|
1299
|
-
if (truncation) truncationReasons.push(truncation.truncatedBy === "lines" ? "line limit" : "size limit");
|
|
1300
|
-
if (limits?.columnTruncated) truncationReasons.push(`line length ${limits.columnTruncated.maxColumn}`);
|
|
1301
|
-
if (truncation?.artifactId) truncationReasons.push(formatFullOutputReference(truncation.artifactId));
|
|
1302
|
-
|
|
1303
1456
|
const extraLines: string[] = [];
|
|
1304
|
-
if (truncationReasons.length > 0) {
|
|
1305
|
-
extraLines.push(uiTheme.fg("warning", `truncated: ${truncationReasons.join(", ")}`));
|
|
1306
|
-
}
|
|
1307
1457
|
if (missingNote) extraLines.push(missingNote);
|
|
1308
1458
|
|
|
1309
1459
|
return createCachedComponent(
|
|
1310
1460
|
() => options.expanded,
|
|
1311
1461
|
width => {
|
|
1312
|
-
const
|
|
1462
|
+
const budget = Math.max(
|
|
1463
|
+
(options.expanded ? EXPANDED_TEXT_LIMIT : COLLAPSED_TEXT_LIMIT) - extraLines.length,
|
|
1464
|
+
0,
|
|
1465
|
+
);
|
|
1313
1466
|
const searchBase = details?.searchPath;
|
|
1314
|
-
const matchLines =
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
maxCollapsedLines: collapsedMatchLineBudget,
|
|
1320
|
-
itemType: "match",
|
|
1321
|
-
renderItem: group => {
|
|
1322
|
-
// Track directory/file context within a group so headers and code-frame
|
|
1323
|
-
// lines link to the backing file, with line-specific links for matches.
|
|
1324
|
-
let contextDir = searchBase ?? "";
|
|
1325
|
-
const hasFileHeader = group.some(line => line.startsWith("# "));
|
|
1326
|
-
let currentFilePath: string | undefined = hasFileHeader ? undefined : searchBase;
|
|
1327
|
-
return group.map(line => {
|
|
1328
|
-
if (line.startsWith("## ")) {
|
|
1329
|
-
// Strip optional ` (suffix)` and `#hash` before resolving.
|
|
1330
|
-
const fileName = line
|
|
1331
|
-
.slice(3)
|
|
1332
|
-
.trimEnd()
|
|
1333
|
-
.replace(/\s+\([^)]*\)\s*$/, "")
|
|
1334
|
-
.replace(/#[0-9a-f]+$/, "");
|
|
1335
|
-
const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
|
|
1336
|
-
currentFilePath = absPath;
|
|
1337
|
-
const styled = uiTheme.fg("dim", line);
|
|
1338
|
-
return absPath ? fileHyperlink(absPath, styled) : styled;
|
|
1339
|
-
}
|
|
1340
|
-
if (line.startsWith("# ")) {
|
|
1341
|
-
const raw = line
|
|
1342
|
-
.slice(2)
|
|
1343
|
-
.trimEnd()
|
|
1344
|
-
.replace(/\s+\([^)]*\)\s*$/, "");
|
|
1345
|
-
if (INTERNAL_URL_DISPLAY_RE.test(raw)) {
|
|
1346
|
-
contextDir = "";
|
|
1347
|
-
const styled = uiTheme.fg("accent", line);
|
|
1348
|
-
const linked = linkUrlLikeSearchHeader(raw, styled);
|
|
1349
|
-
currentFilePath = linked.absPath;
|
|
1350
|
-
return linked.line;
|
|
1351
|
-
}
|
|
1352
|
-
const isDirectory = raw.endsWith("/");
|
|
1353
|
-
const name = isDirectory ? raw.replace(/\/$/, "") : raw.replace(/#[0-9a-f]+$/, "");
|
|
1354
|
-
if (isDirectory) {
|
|
1355
|
-
const absPath = searchBase
|
|
1356
|
-
? name === "."
|
|
1357
|
-
? searchBase
|
|
1358
|
-
: path.join(searchBase, name)
|
|
1359
|
-
: undefined;
|
|
1360
|
-
if (absPath) {
|
|
1361
|
-
contextDir = absPath;
|
|
1362
|
-
}
|
|
1363
|
-
currentFilePath = undefined;
|
|
1364
|
-
const styled = uiTheme.fg("accent", line);
|
|
1365
|
-
return absPath ? fileHyperlink(absPath, styled) : styled;
|
|
1366
|
-
}
|
|
1367
|
-
// Root-level file emitted by formatGroupedFiles when the directory is `.`.
|
|
1368
|
-
const absPath = searchBase && name ? path.join(searchBase, name) : undefined;
|
|
1369
|
-
currentFilePath = absPath;
|
|
1370
|
-
const styled = uiTheme.fg("accent", line);
|
|
1371
|
-
return absPath ? fileHyperlink(absPath, styled) : styled;
|
|
1372
|
-
}
|
|
1373
|
-
const styled = uiTheme.fg("toolOutput", line);
|
|
1374
|
-
const lineNumber = parseSearchDisplayLineNumber(line);
|
|
1375
|
-
return currentFilePath && lineNumber !== undefined
|
|
1376
|
-
? fileHyperlink(currentFilePath, styled, { line: lineNumber })
|
|
1377
|
-
: styled;
|
|
1378
|
-
});
|
|
1379
|
-
},
|
|
1380
|
-
},
|
|
1467
|
+
const matchLines = renderBudgetedSearchGroups(
|
|
1468
|
+
matchGroups,
|
|
1469
|
+
budget,
|
|
1470
|
+
matchCount,
|
|
1471
|
+
searchBase,
|
|
1381
1472
|
uiTheme,
|
|
1473
|
+
!options.expanded,
|
|
1382
1474
|
);
|
|
1383
1475
|
return [header, ...matchLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
|
|
1384
1476
|
},
|
package/src/tools/ssh.ts
CHANGED
|
@@ -13,11 +13,11 @@ import type { SSHHostInfo } from "../ssh/connection-manager";
|
|
|
13
13
|
import { ensureHostInfo, getHostInfoForHost } from "../ssh/connection-manager";
|
|
14
14
|
import { executeSSH } from "../ssh/ssh-executor";
|
|
15
15
|
import { renderStatusLine } from "../tui";
|
|
16
|
-
import { CachedOutputBlock } from "../tui/output-block";
|
|
16
|
+
import { CachedOutputBlock, markFramedBlockComponent } from "../tui/output-block";
|
|
17
17
|
import type { ToolSession } from ".";
|
|
18
18
|
import { truncateForPrompt } from "./approval";
|
|
19
19
|
import { formatStyledTruncationWarning, type OutputMeta, stripOutputNotice } from "./output-meta";
|
|
20
|
-
import { replaceTabs } from "./render-utils";
|
|
20
|
+
import { capPreviewLines, replaceTabs } from "./render-utils";
|
|
21
21
|
import { ToolError } from "./tool-errors";
|
|
22
22
|
import { toolResult } from "./tool-result";
|
|
23
23
|
import { clampTimeout } from "./tool-timeouts";
|
|
@@ -244,16 +244,22 @@ export const sshToolRenderer = {
|
|
|
244
244
|
const header = renderStatusLine({ icon: "pending", title: "SSH", description: `[${host}]` }, uiTheme);
|
|
245
245
|
const cmdLines = formatSshCommandLines(command, uiTheme);
|
|
246
246
|
const outputBlock = new CachedOutputBlock();
|
|
247
|
-
return {
|
|
247
|
+
return markFramedBlockComponent({
|
|
248
248
|
render: (width: number): string[] =>
|
|
249
249
|
outputBlock.render(
|
|
250
|
-
{
|
|
250
|
+
{
|
|
251
|
+
header,
|
|
252
|
+
state: "pending",
|
|
253
|
+
sections: [{ lines: capPreviewLines(cmdLines, uiTheme, { expanded: _options.expanded }) }],
|
|
254
|
+
width,
|
|
255
|
+
animate: true,
|
|
256
|
+
},
|
|
251
257
|
uiTheme,
|
|
252
258
|
),
|
|
253
259
|
invalidate: () => {
|
|
254
260
|
outputBlock.invalidate();
|
|
255
261
|
},
|
|
256
|
-
};
|
|
262
|
+
});
|
|
257
263
|
},
|
|
258
264
|
|
|
259
265
|
renderResult(
|
|
@@ -273,7 +279,7 @@ export const sshToolRenderer = {
|
|
|
273
279
|
const textContent = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
274
280
|
const outputBlock = new CachedOutputBlock();
|
|
275
281
|
|
|
276
|
-
return {
|
|
282
|
+
return markFramedBlockComponent({
|
|
277
283
|
render: (width: number): string[] => {
|
|
278
284
|
// REACTIVE: read mutable options at render time
|
|
279
285
|
const { expanded, renderContext } = options;
|
|
@@ -319,7 +325,14 @@ export const sshToolRenderer = {
|
|
|
319
325
|
{
|
|
320
326
|
header,
|
|
321
327
|
state: "success",
|
|
322
|
-
sections: [
|
|
328
|
+
sections: [
|
|
329
|
+
{
|
|
330
|
+
lines: options.isPartial
|
|
331
|
+
? capPreviewLines(cmdLines, uiTheme, { expanded: options.expanded })
|
|
332
|
+
: cmdLines,
|
|
333
|
+
},
|
|
334
|
+
{ label: uiTheme.fg("toolTitle", "Output"), lines: outputLines },
|
|
335
|
+
],
|
|
323
336
|
width,
|
|
324
337
|
},
|
|
325
338
|
uiTheme,
|
|
@@ -328,7 +341,7 @@ export const sshToolRenderer = {
|
|
|
328
341
|
invalidate: () => {
|
|
329
342
|
outputBlock.invalidate();
|
|
330
343
|
},
|
|
331
|
-
};
|
|
344
|
+
});
|
|
332
345
|
},
|
|
333
346
|
mergeCallAndResult: true,
|
|
334
347
|
};
|
package/src/tools/todo.ts
CHANGED
|
@@ -777,13 +777,26 @@ function renderNoteAttachments(phases: TodoPhase[], uiTheme: Theme): string[] {
|
|
|
777
777
|
|
|
778
778
|
export const todoToolRenderer = {
|
|
779
779
|
renderCall(args: TodoRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
780
|
+
// `args` here is the raw partially-parsed JSON from the streaming
|
|
781
|
+
// tool-call delta and may not satisfy `TodoRenderArgs` at runtime:
|
|
782
|
+
// `parseStreamingJson` can hand back `{ ops: "[" }` mid-delta, or
|
|
783
|
+
// entries that are `null` / strings before fields stream. Guard
|
|
784
|
+
// against non-array `ops` and non-object entries so a malformed
|
|
785
|
+
// delta never breaks the TUI render loop (#2005).
|
|
786
|
+
const opsList = Array.isArray(args?.ops) ? args.ops : [];
|
|
787
|
+
const ops =
|
|
788
|
+
opsList.length === 0
|
|
789
|
+
? ["update"]
|
|
790
|
+
: opsList.map(entry => {
|
|
791
|
+
const e = entry && typeof entry === "object" ? entry : ({} as NonNullable<typeof entry>);
|
|
792
|
+
const parts = [e.op ?? "update"];
|
|
793
|
+
if (e.task) parts.push(e.task);
|
|
794
|
+
if (e.phase) parts.push(e.phase);
|
|
795
|
+
if (Array.isArray(e.items) && e.items.length) {
|
|
796
|
+
parts.push(`${e.items.length} item${e.items.length === 1 ? "" : "s"}`);
|
|
797
|
+
}
|
|
798
|
+
return parts.join(" ");
|
|
799
|
+
});
|
|
787
800
|
const text = renderStatusLine({ icon: "pending", title: "Todo", meta: ops }, uiTheme);
|
|
788
801
|
return new Text(text, 0, 0);
|
|
789
802
|
},
|
package/src/tools/write.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { formatPathRelativeToCwd, isInternalUrlPath } from "./path-utils";
|
|
|
37
37
|
import { enforcePlanModeWrite, resolvePlanPath } from "./plan-mode-guard";
|
|
38
38
|
import {
|
|
39
39
|
formatDiagnostics,
|
|
40
|
+
formatErrorDetail,
|
|
40
41
|
formatExpandHint,
|
|
41
42
|
formatMoreItems,
|
|
42
43
|
formatStatusIcon,
|
|
@@ -58,7 +59,7 @@ import {
|
|
|
58
59
|
import { ToolError } from "./tool-errors";
|
|
59
60
|
import { toolResult } from "./tool-result";
|
|
60
61
|
|
|
61
|
-
const LOOSE_HASHLINE_HEADER_RE = /^\s
|
|
62
|
+
const LOOSE_HASHLINE_HEADER_RE = /^\s*\[[^#\r\n]+#[^ \t\r\n]*\]\s*$/;
|
|
62
63
|
|
|
63
64
|
let fflateModulePromise: Promise<typeof import("fflate")> | undefined;
|
|
64
65
|
async function loadFflate(): Promise<typeof import("fflate")> {
|
|
@@ -109,7 +110,7 @@ function stripWriteContentWithPotentialLooseHeader(lines: string[]): { text: str
|
|
|
109
110
|
/**
|
|
110
111
|
* Strip hashline display prefixes from write content.
|
|
111
112
|
*
|
|
112
|
-
* Only active when hashline edit mode is enabled — the model sees
|
|
113
|
+
* Only active when hashline edit mode is enabled — the model sees `[PATH#HASH]`
|
|
113
114
|
* headers plus `LINE:` prefixes in read output and sometimes copies them into write content.
|
|
114
115
|
*/
|
|
115
116
|
function stripWriteContent(session: ToolSession, content: string): { text: string; stripped: boolean } {
|
|
@@ -122,7 +123,7 @@ function stripWriteContent(session: ToolSession, content: string): { text: strin
|
|
|
122
123
|
/**
|
|
123
124
|
* Record a snapshot of the freshly-written `content` for `absolutePath`
|
|
124
125
|
* so subsequent hashline edits address the new file with a current tag,
|
|
125
|
-
* and return the matching
|
|
126
|
+
* and return the matching `[displayPath#TAG]` header. Returns `undefined`
|
|
126
127
|
* when the session is not in hashline mode so callers can no-op cheaply.
|
|
127
128
|
*
|
|
128
129
|
* Mirrors the post-commit snapshot recording the hashline patcher performs
|
|
@@ -770,7 +771,7 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
770
771
|
context?: AgentToolContext,
|
|
771
772
|
): Promise<AgentToolResult<WriteToolDetails>> {
|
|
772
773
|
return untilAborted(signal, async () => {
|
|
773
|
-
// Strip hashline display prefixes (
|
|
774
|
+
// Strip hashline display prefixes ([PATH#HASH] + LINE:) if the model copied them from read output
|
|
774
775
|
const { text: cleanContent, stripped } = stripWriteContent(this.session, content);
|
|
775
776
|
const internalRouter = InternalUrlRouter.instance();
|
|
776
777
|
if (internalRouter.canHandle(path)) {
|
|
@@ -935,11 +936,20 @@ function normalizeDisplayText(text: string): string {
|
|
|
935
936
|
return text.replace(/\r/g, "");
|
|
936
937
|
}
|
|
937
938
|
|
|
938
|
-
function formatStreamingContent(
|
|
939
|
+
function formatStreamingContent(
|
|
940
|
+
content: string,
|
|
941
|
+
expanded: boolean,
|
|
942
|
+
language: string | undefined,
|
|
943
|
+
uiTheme: Theme,
|
|
944
|
+
): string {
|
|
939
945
|
if (!content) return "";
|
|
940
946
|
const lines = normalizeDisplayText(content).split("\n");
|
|
941
947
|
const totalLines = lines.length;
|
|
942
|
-
|
|
948
|
+
// Collapsed: follow the streaming edge with a bounded tail window so the box
|
|
949
|
+
// stays short enough not to strand its scrolled-off head above the viewport
|
|
950
|
+
// while the block is volatile. `Ctrl+O` (expanded) lifts the cap for a
|
|
951
|
+
// deliberate full view — matching the eval streaming preview.
|
|
952
|
+
const startIndex = expanded ? 0 : Math.max(0, totalLines - WRITE_STREAMING_PREVIEW_LINES);
|
|
943
953
|
const visibleLines = lines.slice(startIndex);
|
|
944
954
|
const hidden = startIndex;
|
|
945
955
|
const highlighted = highlightCode(visibleLines.join("\n"), language);
|
|
@@ -1005,14 +1015,25 @@ export const writeToolRenderer = {
|
|
|
1005
1015
|
return new Text(text, 0, 0);
|
|
1006
1016
|
}
|
|
1007
1017
|
|
|
1008
|
-
// Show streaming preview of content
|
|
1009
|
-
text += formatStreamingContent(args.content, lang, uiTheme);
|
|
1018
|
+
// Show streaming preview of content — bounded tail while collapsed, full on Ctrl+O.
|
|
1019
|
+
text += formatStreamingContent(args.content, Boolean(options?.expanded), lang, uiTheme);
|
|
1010
1020
|
|
|
1011
1021
|
return new Text(text, 0, 0);
|
|
1012
1022
|
},
|
|
1013
1023
|
|
|
1024
|
+
// Only the expanded (Ctrl+O) preview is append-only: it renders the whole
|
|
1025
|
+
// content top-anchored, so streamed chunks only append rows at the bottom.
|
|
1026
|
+
// The collapsed preview slides a bounded tail window (`formatStreamingContent`
|
|
1027
|
+
// with `WRITE_STREAMING_PREVIEW_LINES`) whose visible rows re-layout as the
|
|
1028
|
+
// window moves — not append-only, but it never overflows the viewport, so its
|
|
1029
|
+
// head is never at risk of being dropped regardless. `write` has no partial
|
|
1030
|
+
// result (content streams as args), so `result` is ignored here.
|
|
1031
|
+
isStreamingPreviewAppendOnly(args: WriteRenderArgs, options: RenderResultOptions, _result?: unknown): boolean {
|
|
1032
|
+
return Boolean(options?.expanded && args.content);
|
|
1033
|
+
},
|
|
1034
|
+
|
|
1014
1035
|
renderResult(
|
|
1015
|
-
result: { content: Array<{ type: string; text?: string }>; details?: WriteToolDetails },
|
|
1036
|
+
result: { content: Array<{ type: string; text?: string }>; details?: WriteToolDetails; isError?: boolean },
|
|
1016
1037
|
options: RenderResultOptions,
|
|
1017
1038
|
uiTheme: Theme,
|
|
1018
1039
|
args?: WriteRenderArgs,
|
|
@@ -1023,6 +1044,15 @@ export const writeToolRenderer = {
|
|
|
1023
1044
|
const lang = getLanguageFromPath(rawPath);
|
|
1024
1045
|
const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang));
|
|
1025
1046
|
const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…");
|
|
1047
|
+
|
|
1048
|
+
if (result.isError) {
|
|
1049
|
+
const errorText = result.content?.find(c => c.type === "text")?.text ?? "";
|
|
1050
|
+
const errorHeader = renderStatusLine(
|
|
1051
|
+
{ icon: "error", title: "Write", description: `${langIcon} ${pathDisplay}` },
|
|
1052
|
+
uiTheme,
|
|
1053
|
+
);
|
|
1054
|
+
return new Text(`${errorHeader}\n${formatErrorDetail(errorText, uiTheme)}`, 0, 0);
|
|
1055
|
+
}
|
|
1026
1056
|
const lineCount = countLines(fileContent);
|
|
1027
1057
|
const lineSuffix = formatLineCountSuffix(lineCount, uiTheme);
|
|
1028
1058
|
const execSuffix = result.details?.madeExecutable
|
package/src/tui/code-cell.ts
CHANGED
|
@@ -25,6 +25,12 @@ export interface CodeCellOptions {
|
|
|
25
25
|
output?: string;
|
|
26
26
|
outputMaxLines?: number;
|
|
27
27
|
codeMaxLines?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Show the LAST `codeMaxLines` rows (the live streaming edge) instead of the
|
|
30
|
+
* first, with a "… N earlier lines" marker on top. Lets a pending preview
|
|
31
|
+
* follow code as it is written while staying bounded. Ignored when `expanded`.
|
|
32
|
+
*/
|
|
33
|
+
codeTail?: boolean;
|
|
28
34
|
expanded?: boolean;
|
|
29
35
|
/** Animate the cell border with a sweeping segment while pending/running. */
|
|
30
36
|
animate?: boolean;
|
|
@@ -102,13 +108,22 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
|
|
|
102
108
|
const normalizedCode = replaceTabs(code ?? "");
|
|
103
109
|
const rawCodeLines = sanitizeTerminalLines(normalizedCode);
|
|
104
110
|
const maxCodeLines = expanded ? rawCodeLines.length : Math.min(rawCodeLines.length, codeMaxLines);
|
|
105
|
-
const visibleCode = rawCodeLines.slice(0, maxCodeLines).join("\n");
|
|
106
|
-
const codeLines = highlightCode(visibleCode, language);
|
|
107
111
|
const hiddenCodeLines = rawCodeLines.length - maxCodeLines;
|
|
112
|
+
const tail = options.codeTail === true && !expanded && hiddenCodeLines > 0;
|
|
113
|
+
const startIndex = tail ? rawCodeLines.length - maxCodeLines : 0;
|
|
114
|
+
const visibleCode = rawCodeLines.slice(startIndex, startIndex + maxCodeLines).join("\n");
|
|
115
|
+
const codeLines = highlightCode(visibleCode, language);
|
|
108
116
|
if (hiddenCodeLines > 0) {
|
|
109
117
|
const hint = formatExpandHint(theme, expanded, hiddenCodeLines > 0);
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
if (tail) {
|
|
119
|
+
// Earlier rows scrolled above the live tail window — mark them on top so
|
|
120
|
+
// the newest streamed line stays pinned to the bottom of the box.
|
|
121
|
+
const earlier = `… ${hiddenCodeLines} earlier line${hiddenCodeLines === 1 ? "" : "s"}${hint ? ` ${hint}` : ""}`;
|
|
122
|
+
codeLines.unshift(theme.fg("dim", earlier));
|
|
123
|
+
} else {
|
|
124
|
+
const moreLine = `${formatMoreItems(hiddenCodeLines, "line")}${hint ? ` ${hint}` : ""}`;
|
|
125
|
+
codeLines.push(theme.fg("dim", moreLine));
|
|
126
|
+
}
|
|
112
127
|
}
|
|
113
128
|
|
|
114
129
|
const outputLines: string[] = [];
|
package/src/tui/output-block.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bordered output container with optional header and sections.
|
|
3
3
|
*/
|
|
4
|
+
import type { Component } from "@oh-my-pi/pi-tui";
|
|
4
5
|
import { ImageProtocol, padding, TERMINAL, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
|
|
5
6
|
import type { Theme } from "../modes/theme/theme";
|
|
6
7
|
import { getSixelLineMask } from "../utils/sixel";
|
|
@@ -19,6 +20,19 @@ export interface OutputBlockOptions {
|
|
|
19
20
|
animate?: boolean;
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
const FRAMED_BLOCK_COMPONENT = Symbol("framedBlockComponent");
|
|
24
|
+
|
|
25
|
+
export type FramedBlockComponent = Component & { [FRAMED_BLOCK_COMPONENT]?: true };
|
|
26
|
+
|
|
27
|
+
export function markFramedBlockComponent<T extends Component>(component: T): T & FramedBlockComponent {
|
|
28
|
+
(component as T & FramedBlockComponent)[FRAMED_BLOCK_COMPONENT] = true;
|
|
29
|
+
return component as T & FramedBlockComponent;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isFramedBlockComponent(component: Component): boolean {
|
|
33
|
+
return (component as FramedBlockComponent)[FRAMED_BLOCK_COMPONENT] === true;
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
const BORDER_SHIMMER_TICK_MS = 16;
|
|
23
37
|
/** Duration of one full left↔right↔left bounce of the bottom-edge segment, in
|
|
24
38
|
* ms. Position is derived from the wall clock against this fixed cycle so a
|