@oh-my-pi/pi-coding-agent 15.9.67 → 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 +63 -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 +6 -1
- 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 +32 -6
- 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/custom-editor.d.ts +2 -1
- package/dist/types/modes/components/tool-execution.d.ts +18 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/index.d.ts +5 -4
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- 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 +1 -1
- 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 -8
- package/dist/types/tools/fetch.d.ts +15 -7
- package/dist/types/tools/render-utils.d.ts +8 -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/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/cli/args.ts +2 -2
- 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 +15 -6
- 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 +18 -5
- package/src/config/settings.ts +11 -0
- package/src/dap/client.ts +14 -16
- package/src/edit/renderer.ts +36 -48
- package/src/eval/__tests__/agent-bridge.test.ts +75 -32
- package/src/eval/agent-bridge.ts +34 -7
- package/src/extensibility/extensions/runner.ts +1 -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 +5 -5
- package/src/lsp/client.ts +104 -55
- package/src/lsp/types.ts +10 -0
- package/src/main.ts +44 -49
- 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/custom-editor.ts +10 -1
- package/src/modes/components/status-line.ts +3 -5
- package/src/modes/components/tool-execution.ts +61 -16
- package/src/modes/controllers/command-controller.ts +13 -2
- package/src/modes/controllers/input-controller.ts +11 -3
- package/src/modes/controllers/selector-controller.ts +2 -2
- package/src/modes/index.ts +5 -4
- package/src/modes/interactive-mode.ts +17 -3
- 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/types.ts +1 -1
- package/src/modes/utils/context-usage.ts +10 -6
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/sdk.ts +21 -23
- package/src/session/agent-session.ts +7 -7
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/slash-commands/helpers/usage-report.ts +2 -0
- package/src/task/executor.ts +20 -2
- package/src/task/render.ts +1 -2
- package/src/telemetry-export.ts +25 -7
- package/src/tools/eval-backends.ts +6 -17
- package/src/tools/eval-render.ts +21 -18
- package/src/tools/eval.ts +5 -4
- package/src/tools/fetch.ts +94 -84
- package/src/tools/render-utils.ts +17 -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/todo.ts +20 -7
- package/src/tools/write.ts +22 -1
- 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 +39 -54
- package/src/web/search/types.ts +5 -1
- package/dist/types/eval/__tests__/shared-executors.test.d.ts +0 -1
- package/src/eval/__tests__/shared-executors.test.ts +0 -609
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/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,
|
|
@@ -1020,8 +1021,19 @@ export const writeToolRenderer = {
|
|
|
1020
1021
|
return new Text(text, 0, 0);
|
|
1021
1022
|
},
|
|
1022
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
|
+
|
|
1023
1035
|
renderResult(
|
|
1024
|
-
result: { content: Array<{ type: string; text?: string }>; details?: WriteToolDetails },
|
|
1036
|
+
result: { content: Array<{ type: string; text?: string }>; details?: WriteToolDetails; isError?: boolean },
|
|
1025
1037
|
options: RenderResultOptions,
|
|
1026
1038
|
uiTheme: Theme,
|
|
1027
1039
|
args?: WriteRenderArgs,
|
|
@@ -1032,6 +1044,15 @@ export const writeToolRenderer = {
|
|
|
1032
1044
|
const lang = getLanguageFromPath(rawPath);
|
|
1033
1045
|
const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang));
|
|
1034
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
|
+
}
|
|
1035
1056
|
const lineCount = countLines(fileContent);
|
|
1036
1057
|
const lineSuffix = formatLineCountSuffix(lineCount, uiTheme);
|
|
1037
1058
|
const execSuffix = result.details?.madeExecutable
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import { $env, ptree } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { RenderResult, SpecialHandler } from "./types";
|
|
3
|
-
import { buildResult, loadPage } from "./types";
|
|
3
|
+
import { buildResult, formatMediaDuration, loadPage } from "./types";
|
|
4
4
|
|
|
5
5
|
interface GitHubUrl {
|
|
6
|
-
type:
|
|
6
|
+
type:
|
|
7
|
+
| "blob"
|
|
8
|
+
| "tree"
|
|
9
|
+
| "repo"
|
|
10
|
+
| "issue"
|
|
11
|
+
| "issues"
|
|
12
|
+
| "pull"
|
|
13
|
+
| "pulls"
|
|
14
|
+
| "discussion"
|
|
15
|
+
| "discussions"
|
|
16
|
+
| "actions-run"
|
|
17
|
+
| "actions-job"
|
|
18
|
+
| "other";
|
|
7
19
|
owner: string;
|
|
8
20
|
repo: string;
|
|
9
21
|
ref?: string;
|
|
10
22
|
path?: string;
|
|
11
23
|
number?: number;
|
|
24
|
+
runId?: number;
|
|
25
|
+
jobId?: number;
|
|
12
26
|
}
|
|
13
27
|
|
|
14
28
|
interface GitHubIssueComment {
|
|
@@ -20,7 +34,7 @@ interface GitHubIssueComment {
|
|
|
20
34
|
/**
|
|
21
35
|
* Parse GitHub URL into components
|
|
22
36
|
*/
|
|
23
|
-
function parseGitHubUrl(url: string): GitHubUrl | null {
|
|
37
|
+
export function parseGitHubUrl(url: string): GitHubUrl | null {
|
|
24
38
|
try {
|
|
25
39
|
const parsed = new URL(url);
|
|
26
40
|
if (parsed.hostname !== "github.com") return null;
|
|
@@ -54,6 +68,20 @@ function parseGitHubUrl(url: string): GitHubUrl | null {
|
|
|
54
68
|
return { type: "pulls", owner, repo };
|
|
55
69
|
case "pulls":
|
|
56
70
|
return { type: "pulls", owner, repo };
|
|
71
|
+
case "actions": {
|
|
72
|
+
// /actions/runs/{runId} → run summary + jobs
|
|
73
|
+
// /actions/runs/{runId}/job/{jobId} → single job (web URL uses singular "job")
|
|
74
|
+
// /actions/runs/{runId}/jobs/{jobId} → single job (API-style plural)
|
|
75
|
+
if (subParts[0] === "runs" && /^\d+$/.test(subParts[1] ?? "")) {
|
|
76
|
+
const runId = parseInt(subParts[1], 10);
|
|
77
|
+
const seg = subParts[2];
|
|
78
|
+
if ((seg === "job" || seg === "jobs") && /^\d+$/.test(subParts[3] ?? "")) {
|
|
79
|
+
return { type: "actions-job", owner, repo, runId, jobId: parseInt(subParts[3], 10) };
|
|
80
|
+
}
|
|
81
|
+
return { type: "actions-run", owner, repo, runId };
|
|
82
|
+
}
|
|
83
|
+
return { type: "other", owner, repo };
|
|
84
|
+
}
|
|
57
85
|
case "discussions":
|
|
58
86
|
if (subParts.length > 0 && /^\d+$/.test(subParts[0])) {
|
|
59
87
|
return { type: "discussion", owner, repo, number: parseInt(subParts[0], 10) };
|
|
@@ -371,6 +399,212 @@ async function renderGitHubRepo(
|
|
|
371
399
|
return { content: md, ok: true };
|
|
372
400
|
}
|
|
373
401
|
|
|
402
|
+
interface GitHubActionsStep {
|
|
403
|
+
name: string;
|
|
404
|
+
status: string;
|
|
405
|
+
conclusion: string | null;
|
|
406
|
+
number: number;
|
|
407
|
+
started_at: string | null;
|
|
408
|
+
completed_at: string | null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
interface GitHubActionsJob {
|
|
412
|
+
id: number;
|
|
413
|
+
run_id: number;
|
|
414
|
+
name: string;
|
|
415
|
+
status: string;
|
|
416
|
+
conclusion: string | null;
|
|
417
|
+
started_at: string | null;
|
|
418
|
+
completed_at: string | null;
|
|
419
|
+
html_url: string | null;
|
|
420
|
+
steps?: GitHubActionsStep[];
|
|
421
|
+
runner_name?: string | null;
|
|
422
|
+
labels?: string[];
|
|
423
|
+
workflow_name?: string | null;
|
|
424
|
+
head_branch?: string | null;
|
|
425
|
+
head_sha?: string;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
interface GitHubActionsRun {
|
|
429
|
+
id: number;
|
|
430
|
+
name?: string | null;
|
|
431
|
+
display_title?: string;
|
|
432
|
+
run_number: number;
|
|
433
|
+
run_attempt?: number;
|
|
434
|
+
event: string;
|
|
435
|
+
status: string;
|
|
436
|
+
conclusion: string | null;
|
|
437
|
+
head_branch?: string | null;
|
|
438
|
+
head_sha?: string;
|
|
439
|
+
html_url: string;
|
|
440
|
+
created_at: string;
|
|
441
|
+
updated_at: string;
|
|
442
|
+
run_started_at?: string;
|
|
443
|
+
actor?: { login: string };
|
|
444
|
+
triggering_actor?: { login: string };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Combine status + conclusion into a single label, e.g. `completed (failure)`. */
|
|
448
|
+
function statusLabel(status: string, conclusion: string | null | undefined): string {
|
|
449
|
+
return conclusion ? `${status} (${conclusion})` : status;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Wall-clock duration between two ISO timestamps, formatted HH:MM:SS / MM:SS. Empty when unknown. */
|
|
453
|
+
function actionDuration(start?: string | null, end?: string | null): string {
|
|
454
|
+
if (!start || !end) return "";
|
|
455
|
+
const ms = Date.parse(end) - Date.parse(start);
|
|
456
|
+
if (!Number.isFinite(ms) || ms < 0) return "";
|
|
457
|
+
return formatMediaDuration(Math.round(ms / 1000));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** Escape `|` so step/job names can't break a markdown table row. */
|
|
461
|
+
function escapeCell(text: string): string {
|
|
462
|
+
return text.replaceAll("|", "\\|");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Strip the per-line ISO-8601 timestamp prefix GitHub prepends to every job log line.
|
|
467
|
+
* Cuts ~28 bytes/line of noise while preserving the message text. Also drops the leading
|
|
468
|
+
* UTF-8 BOM GitHub puts at the start of the log file (otherwise the first line's timestamp
|
|
469
|
+
* survives because `^` no longer sits before a digit).
|
|
470
|
+
*/
|
|
471
|
+
export function stripActionsLogTimestamps(logs: string): string {
|
|
472
|
+
return logs.replace(/^\uFEFF/, "").replace(/^\d{4}-\d{2}-\d{2}T[\d:.]+Z /gm, "");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Render a job's steps as a markdown table. Empty string when there are no steps. */
|
|
476
|
+
function renderActionsSteps(steps?: GitHubActionsStep[]): string {
|
|
477
|
+
if (!steps || steps.length === 0) return "";
|
|
478
|
+
let md = "| # | Step | Status | Conclusion | Duration |\n";
|
|
479
|
+
md += "|---|------|--------|------------|----------|\n";
|
|
480
|
+
for (const step of steps) {
|
|
481
|
+
const dur = actionDuration(step.started_at, step.completed_at) || "-";
|
|
482
|
+
md += `| ${step.number} | ${escapeCell(step.name)} | ${step.status} | ${step.conclusion ?? "-"} | ${dur} |\n`;
|
|
483
|
+
}
|
|
484
|
+
return `${md}\n`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Run-level metadata lines shared by the run and job renderers. */
|
|
488
|
+
function renderActionsRunMeta(run: GitHubActionsRun): string {
|
|
489
|
+
let md = `**Workflow:** ${run.name ?? "(unknown)"}\n`;
|
|
490
|
+
md += `**Run:** #${run.run_number}`;
|
|
491
|
+
if (run.run_attempt && run.run_attempt > 1) md += ` (attempt ${run.run_attempt})`;
|
|
492
|
+
md += ` · ${statusLabel(run.status, run.conclusion)}\n`;
|
|
493
|
+
if (run.head_branch) {
|
|
494
|
+
md += `**Branch:** ${run.head_branch}${run.head_sha ? ` @ ${run.head_sha.slice(0, 7)}` : ""}\n`;
|
|
495
|
+
}
|
|
496
|
+
const actor = run.triggering_actor?.login ?? run.actor?.login;
|
|
497
|
+
md += `**Event:** ${run.event}${actor ? ` · by @${actor}` : ""}\n`;
|
|
498
|
+
const started = run.run_started_at ?? run.created_at;
|
|
499
|
+
const dur = actionDuration(started, run.updated_at);
|
|
500
|
+
md += `Started: ${started}${dur ? ` · Duration: ${dur}` : ""}\n`;
|
|
501
|
+
md += `URL: ${run.html_url}\n`;
|
|
502
|
+
return md;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/** Fetch a job's plain-text logs. Returns null when unavailable (no token / expired / private). */
|
|
506
|
+
async function fetchGitHubJobLogs(
|
|
507
|
+
owner: string,
|
|
508
|
+
repo: string,
|
|
509
|
+
jobId: number,
|
|
510
|
+
timeout: number,
|
|
511
|
+
signal?: AbortSignal,
|
|
512
|
+
): Promise<string | null> {
|
|
513
|
+
const headers: Record<string, string> = {
|
|
514
|
+
Accept: "application/vnd.github+json",
|
|
515
|
+
"X-GitHub-Api-Version": "2022-11-28",
|
|
516
|
+
};
|
|
517
|
+
const token = $env.GITHUB_TOKEN || $env.GH_TOKEN;
|
|
518
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
519
|
+
|
|
520
|
+
// 302 → signed log URL on a different origin; fetch strips Authorization on the cross-origin hop.
|
|
521
|
+
const result = await loadPage(`https://api.github.com/repos/${owner}/${repo}/actions/jobs/${jobId}/logs`, {
|
|
522
|
+
timeout,
|
|
523
|
+
headers,
|
|
524
|
+
signal,
|
|
525
|
+
});
|
|
526
|
+
return result.ok && result.content ? result.content : null;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Render a workflow run: run metadata plus a per-job breakdown. Steps are listed for any job that
|
|
531
|
+
* did not succeed (the debugging-relevant ones); successful jobs collapse to a single line.
|
|
532
|
+
*/
|
|
533
|
+
async function renderGitHubActionsRun(
|
|
534
|
+
gh: GitHubUrl,
|
|
535
|
+
timeout: number,
|
|
536
|
+
signal?: AbortSignal,
|
|
537
|
+
): Promise<{ content: string; ok: boolean }> {
|
|
538
|
+
const runResult = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}/actions/runs/${gh.runId}`, timeout, signal);
|
|
539
|
+
if (!runResult.ok || !runResult.data) return { content: "", ok: false };
|
|
540
|
+
|
|
541
|
+
const run = runResult.data as GitHubActionsRun;
|
|
542
|
+
let md = `# ${run.display_title || run.name || `Run #${run.run_number}`}\n\n`;
|
|
543
|
+
md += renderActionsRunMeta(run);
|
|
544
|
+
md += `\n---\n\n`;
|
|
545
|
+
|
|
546
|
+
const jobsResult = await fetchGitHubApi(
|
|
547
|
+
`/repos/${gh.owner}/${gh.repo}/actions/runs/${gh.runId}/jobs?per_page=100`,
|
|
548
|
+
timeout,
|
|
549
|
+
signal,
|
|
550
|
+
);
|
|
551
|
+
if (jobsResult.ok && jobsResult.data) {
|
|
552
|
+
const jobs = (jobsResult.data as { jobs?: GitHubActionsJob[] }).jobs ?? [];
|
|
553
|
+
md += `## Jobs (${jobs.length})\n\n`;
|
|
554
|
+
for (const job of jobs) {
|
|
555
|
+
const dur = actionDuration(job.started_at, job.completed_at);
|
|
556
|
+
md += `### ${escapeCell(job.name)} — ${statusLabel(job.status, job.conclusion)}${dur ? ` (${dur})` : ""}\n\n`;
|
|
557
|
+
if (job.conclusion !== "success") {
|
|
558
|
+
md += renderActionsSteps(job.steps);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return { content: md, ok: true };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Render a single workflow job: run context, step table, and the full job logs.
|
|
568
|
+
*/
|
|
569
|
+
async function renderGitHubActionsJob(
|
|
570
|
+
gh: GitHubUrl,
|
|
571
|
+
timeout: number,
|
|
572
|
+
signal?: AbortSignal,
|
|
573
|
+
): Promise<{ content: string; ok: boolean }> {
|
|
574
|
+
const jobResult = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}/actions/jobs/${gh.jobId}`, timeout, signal);
|
|
575
|
+
if (!jobResult.ok || !jobResult.data) return { content: "", ok: false };
|
|
576
|
+
|
|
577
|
+
const job = jobResult.data as GitHubActionsJob;
|
|
578
|
+
|
|
579
|
+
// Best-effort run context for nicer headers; the job render stands on its own without it.
|
|
580
|
+
const runResult = await fetchGitHubApi(`/repos/${gh.owner}/${gh.repo}/actions/runs/${job.run_id}`, timeout, signal);
|
|
581
|
+
const run = runResult.ok && runResult.data ? (runResult.data as GitHubActionsRun) : null;
|
|
582
|
+
|
|
583
|
+
let md = `# ${escapeCell(job.name)}\n\n`;
|
|
584
|
+
if (run) {
|
|
585
|
+
md += renderActionsRunMeta(run);
|
|
586
|
+
} else if (job.workflow_name) {
|
|
587
|
+
md += `**Workflow:** ${job.workflow_name}\n`;
|
|
588
|
+
if (job.head_branch) md += `**Branch:** ${job.head_branch}\n`;
|
|
589
|
+
}
|
|
590
|
+
const dur = actionDuration(job.started_at, job.completed_at);
|
|
591
|
+
md += `**Job:** ${escapeCell(job.name)} · ${statusLabel(job.status, job.conclusion)}${dur ? ` · ${dur}` : ""}\n`;
|
|
592
|
+
if (job.runner_name) md += `**Runner:** ${job.runner_name}\n`;
|
|
593
|
+
if (job.html_url) md += `URL: ${job.html_url}\n`;
|
|
594
|
+
md += `\n---\n\n`;
|
|
595
|
+
|
|
596
|
+
const steps = renderActionsSteps(job.steps);
|
|
597
|
+
if (steps) md += `## Steps\n\n${steps}`;
|
|
598
|
+
|
|
599
|
+
const logs = await fetchGitHubJobLogs(gh.owner, gh.repo, job.id, timeout, signal);
|
|
600
|
+
md += `## Logs\n\n`;
|
|
601
|
+
md += logs
|
|
602
|
+
? stripActionsLogTimestamps(logs)
|
|
603
|
+
: "*Logs unavailable — requires a GITHUB_TOKEN/GH_TOKEN with read access, or the run's logs have expired.*\n";
|
|
604
|
+
|
|
605
|
+
return { content: md, ok: true };
|
|
606
|
+
}
|
|
607
|
+
|
|
374
608
|
/**
|
|
375
609
|
* Handle GitHub URLs specially
|
|
376
610
|
*/
|
|
@@ -445,6 +679,24 @@ export const handleGitHub: SpecialHandler = async (
|
|
|
445
679
|
}
|
|
446
680
|
break;
|
|
447
681
|
}
|
|
682
|
+
|
|
683
|
+
case "actions-run": {
|
|
684
|
+
notes.push(`Fetched via GitHub API`);
|
|
685
|
+
const result = await renderGitHubActionsRun(gh, timeout, signal);
|
|
686
|
+
if (result.ok) {
|
|
687
|
+
return buildResult(result.content, { url, method: "github-actions-run", fetchedAt, notes });
|
|
688
|
+
}
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
case "actions-job": {
|
|
693
|
+
notes.push(`Fetched via GitHub API`);
|
|
694
|
+
const result = await renderGitHubActionsJob(gh, timeout, signal);
|
|
695
|
+
if (result.ok) {
|
|
696
|
+
return buildResult(result.content, { url, method: "github-actions-job", fetchedAt, notes });
|
|
697
|
+
}
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
448
700
|
}
|
|
449
701
|
|
|
450
702
|
// Fall back to null (let normal rendering handle it)
|