@oh-my-pi/pi-coding-agent 17.3.2 → 17.3.4
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 +21 -0
- package/dist/{CHANGELOG-fr2awajz.md → CHANGELOG-trcc215s.md} +21 -0
- package/dist/cli.js +3558 -3530
- package/dist/docs-index.generated.txt +1 -1
- package/dist/types/markit/converters/pdf/index.d.ts +2 -1
- package/dist/types/session/turn-recovery.d.ts +1 -1
- package/dist/types/tools/read-pdf.d.ts +15 -0
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/dist/types/utils/markit.d.ts +3 -4
- package/package.json +13 -16
- package/scripts/build-binary.ts +0 -2
- package/scripts/bundle-dist.ts +0 -1
- package/src/cli/read-cli.ts +2 -0
- package/src/markit/NOTICE +8 -8
- package/src/markit/converters/pdf/index.ts +14 -126
- package/src/mcp/client.ts +8 -9
- package/src/modes/controllers/event-controller.ts +57 -11
- package/src/modes/rpc/rpc-client.ts +7 -0
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/prompts/system/empty-stop-retry.md +1 -1
- package/src/session/agent-session.ts +8 -1
- package/src/session/turn-recovery.ts +59 -55
- package/src/tools/read-pdf.ts +135 -0
- package/src/tools/read.ts +63 -37
- package/src/utils/external-editor.ts +24 -5
- package/src/utils/markit.ts +6 -44
- package/dist/types/markit/converters/pdf/columns.d.ts +0 -35
- package/dist/types/markit/converters/pdf/extract.d.ts +0 -10
- package/dist/types/markit/converters/pdf/grid.d.ts +0 -25
- package/dist/types/markit/converters/pdf/headers.d.ts +0 -24
- package/dist/types/markit/converters/pdf/render.d.ts +0 -24
- package/dist/types/markit/converters/pdf/types.d.ts +0 -75
- package/dist/types/tools/read-pdf-images.d.ts +0 -12
- package/dist/types/utils/mupdf-wasm-embed.d.ts +0 -1
- package/scripts/embed-mupdf-wasm.ts +0 -67
- package/src/markit/converters/pdf/columns.ts +0 -103
- package/src/markit/converters/pdf/extract.ts +0 -598
- package/src/markit/converters/pdf/grid.ts +0 -780
- package/src/markit/converters/pdf/headers.ts +0 -106
- package/src/markit/converters/pdf/render.ts +0 -501
- package/src/markit/converters/pdf/types.ts +0 -84
- package/src/tools/read-pdf-images.ts +0 -250
- package/src/utils/mupdf-wasm-embed.ts +0 -12
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ConversionResult, Converter, StreamInfo } from "../../types.js";
|
|
2
|
+
/** Converts PDF buffers to Markdown through the native `pdf-inspector` bridge. */
|
|
2
3
|
export declare class PdfConverter implements Converter {
|
|
3
4
|
name: string;
|
|
4
5
|
accepts(streamInfo: StreamInfo): boolean;
|
|
5
|
-
convert(input: Buffer,
|
|
6
|
+
convert(input: Buffer, _streamInfo: StreamInfo): Promise<ConversionResult>;
|
|
6
7
|
}
|
|
@@ -126,7 +126,7 @@ export declare class TurnRecovery {
|
|
|
126
126
|
/** Persists an otherwise skipped terminal empty error turn. */
|
|
127
127
|
persistTerminalEmptyErrorTurn(message: AssistantMessage): Promise<void>;
|
|
128
128
|
/** Handles empty terminal assistant turns and schedules bounded recovery. */
|
|
129
|
-
handleEmptyAssistantStop(message: AssistantMessage): Promise<
|
|
129
|
+
handleEmptyAssistantStop(message: AssistantMessage): Promise<"continue" | "terminal" | undefined>;
|
|
130
130
|
/** Classifies suspicious terminal stops and schedules bounded recovery. */
|
|
131
131
|
handleUnexpectedAssistantStop(message: AssistantMessage): Promise<boolean>;
|
|
132
132
|
/** Removes a persisted failed assistant turn after its persistence slot settles. */
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ToolSession } from "../sdk.js";
|
|
2
|
+
import type { ScreenshotResult } from "./browser/tab-protocol.js";
|
|
3
|
+
/** A legacy PDF image-member path interpreted as a page screenshot request. */
|
|
4
|
+
export interface PdfImageReadTarget {
|
|
5
|
+
/** PDF path before the member delimiter. */
|
|
6
|
+
pdfPath: string;
|
|
7
|
+
/** Original member text after the delimiter. */
|
|
8
|
+
member: string;
|
|
9
|
+
/** One-indexed page inferred from names such as `p2-img0.png`; defaults to page 1. */
|
|
10
|
+
page: number;
|
|
11
|
+
}
|
|
12
|
+
/** Parse a former PDF image-member path as a Chromium page screenshot request. */
|
|
13
|
+
export declare function splitPdfImageReadPath(readPath: string): PdfImageReadTarget | null;
|
|
14
|
+
/** Render one PDF page through the browser tool's shared headless Chromium. */
|
|
15
|
+
export declare function renderPdfPageScreenshot(session: ToolSession, absolutePdfPath: string, page: number, signal?: AbortSignal): Promise<ScreenshotResult>;
|
|
@@ -18,6 +18,13 @@ export interface OpenInEditorOptions {
|
|
|
18
18
|
/** Keep the file's trailing newline instead of trimming it from the returned text. */
|
|
19
19
|
trimTrailingNewline?: boolean;
|
|
20
20
|
}
|
|
21
|
+
/** Subprocess argv and Windows quoting mode used to launch an external editor. */
|
|
22
|
+
export interface EditorSpawnCommand {
|
|
23
|
+
cmd: string[];
|
|
24
|
+
windowsVerbatimArguments: boolean;
|
|
25
|
+
}
|
|
26
|
+
/** Resolves shell argv without letting the host runtime re-quote the editor command. */
|
|
27
|
+
export declare function resolveEditorSpawnCommand(editorCmd: string, tmpFile: string, platform?: NodeJS.Platform): EditorSpawnCommand;
|
|
21
28
|
/**
|
|
22
29
|
* Opens `content` in the user's external editor and returns the edited text.
|
|
23
30
|
* Returns `null` if the editor exits with a non-zero code.
|
|
@@ -18,10 +18,9 @@ export interface MarkitConversionResult {
|
|
|
18
18
|
}
|
|
19
19
|
export interface MarkitFileConversionOptions {
|
|
20
20
|
/**
|
|
21
|
-
* Directory
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* placeholder comment instead.
|
|
21
|
+
* Directory converters may use for extracted image or diagram files. Since
|
|
22
|
+
* those files are conversion side effects, conversions using this option
|
|
23
|
+
* bypass the markdown cache.
|
|
25
24
|
*/
|
|
26
25
|
imageDir?: string;
|
|
27
26
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "17.3.
|
|
4
|
+
"version": "17.3.4",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,8 +41,6 @@
|
|
|
41
41
|
"format-prompts": "bun scripts/format-prompts.ts",
|
|
42
42
|
"gen:tool-views": "bun --cwd=../collab-web run gen:tool-views",
|
|
43
43
|
"gen:bundle": "bun scripts/bundle-dist.ts",
|
|
44
|
-
"gen:mupdf": "bun scripts/embed-mupdf-wasm.ts --generate",
|
|
45
|
-
"gen:mupdf:reset": "bun scripts/embed-mupdf-wasm.ts --reset",
|
|
46
44
|
"gen:native": "bun --cwd=../natives run gen:native",
|
|
47
45
|
"gen:native:reset": "bun --cwd=../natives run gen:native:reset",
|
|
48
46
|
"prepack": "bun run gen:tool-views && bun run gen:bundle",
|
|
@@ -50,18 +48,18 @@
|
|
|
50
48
|
},
|
|
51
49
|
"dependencies": {
|
|
52
50
|
"@babel/parser": "^7.29.7",
|
|
53
|
-
"@oh-my-pi/hashline": "17.3.
|
|
54
|
-
"@oh-my-pi/omp-stats": "17.3.
|
|
55
|
-
"@oh-my-pi/omptype": "17.3.
|
|
56
|
-
"@oh-my-pi/pi-agent-core": "17.3.
|
|
57
|
-
"@oh-my-pi/pi-ai": "17.3.
|
|
58
|
-
"@oh-my-pi/pi-catalog": "17.3.
|
|
59
|
-
"@oh-my-pi/pi-mnemopi": "17.3.
|
|
60
|
-
"@oh-my-pi/pi-natives": "17.3.
|
|
61
|
-
"@oh-my-pi/pi-tui": "17.3.
|
|
62
|
-
"@oh-my-pi/pi-utils": "17.3.
|
|
63
|
-
"@oh-my-pi/pi-wire": "17.3.
|
|
64
|
-
"@oh-my-pi/snapcompact": "17.3.
|
|
51
|
+
"@oh-my-pi/hashline": "17.3.4",
|
|
52
|
+
"@oh-my-pi/omp-stats": "17.3.4",
|
|
53
|
+
"@oh-my-pi/omptype": "17.3.4",
|
|
54
|
+
"@oh-my-pi/pi-agent-core": "17.3.4",
|
|
55
|
+
"@oh-my-pi/pi-ai": "17.3.4",
|
|
56
|
+
"@oh-my-pi/pi-catalog": "17.3.4",
|
|
57
|
+
"@oh-my-pi/pi-mnemopi": "17.3.4",
|
|
58
|
+
"@oh-my-pi/pi-natives": "17.3.4",
|
|
59
|
+
"@oh-my-pi/pi-tui": "17.3.4",
|
|
60
|
+
"@oh-my-pi/pi-utils": "17.3.4",
|
|
61
|
+
"@oh-my-pi/pi-wire": "17.3.4",
|
|
62
|
+
"@oh-my-pi/snapcompact": "17.3.4",
|
|
65
63
|
"@opentelemetry/api": "^1.9.1",
|
|
66
64
|
"@opentelemetry/api-logs": "^0.220.0",
|
|
67
65
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
@@ -73,7 +71,6 @@
|
|
|
73
71
|
"@opentelemetry/sdk-metrics": "^2.9.0",
|
|
74
72
|
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
75
73
|
"@opentelemetry/sdk-trace-node": "^2.9.0",
|
|
76
|
-
"mupdf": "^1.28.0",
|
|
77
74
|
"puppeteer-core": "25.3.0"
|
|
78
75
|
},
|
|
79
76
|
"optionalDependencies": {
|
package/scripts/build-binary.ts
CHANGED
|
@@ -88,7 +88,6 @@ async function main(): Promise<void> {
|
|
|
88
88
|
["bun", "--cwd=../natives", "run", "gen:native"],
|
|
89
89
|
crossBuild ? { ...Bun.env, TARGET_PLATFORM: crossBuild.platform, TARGET_ARCH: crossBuild.arch } : Bun.env,
|
|
90
90
|
);
|
|
91
|
-
await runCommand(["bun", "run", "gen:mupdf"]);
|
|
92
91
|
try {
|
|
93
92
|
await compileCodingAgent({
|
|
94
93
|
repoRoot,
|
|
@@ -104,7 +103,6 @@ async function main(): Promise<void> {
|
|
|
104
103
|
await runCommand(["codesign", "--force", "--sign", "-", outputPath]);
|
|
105
104
|
}
|
|
106
105
|
} finally {
|
|
107
|
-
await runCommand(["bun", "run", "gen:mupdf:reset"]);
|
|
108
106
|
await runCommand(["bun", "--cwd=../natives", "run", "gen:native:reset"]);
|
|
109
107
|
}
|
|
110
108
|
} finally {
|
package/scripts/bundle-dist.ts
CHANGED
|
@@ -15,7 +15,6 @@ const legacyHtmlExportAssetPattern = /^(?:template-[^.]+\.(?:css|html|js)|tool-v
|
|
|
15
15
|
// `omp-legacy-pi-modules` exists only in compiled binaries via the build plugin;
|
|
16
16
|
// the npm bundle never executes that `isCompiledBinary()` branch.
|
|
17
17
|
const ALWAYS_EXTERNAL = [
|
|
18
|
-
"mupdf",
|
|
19
18
|
"@oh-my-pi/pi-natives",
|
|
20
19
|
"@huggingface/transformers",
|
|
21
20
|
"fastembed",
|
package/src/cli/read-cli.ts
CHANGED
|
@@ -10,6 +10,7 @@ import chalk from "@oh-my-pi/pi-utils/chalk";
|
|
|
10
10
|
import { Settings } from "../config/settings";
|
|
11
11
|
import { extractUriScheme } from "../internal-urls/parse";
|
|
12
12
|
import { InternalUrlRouter } from "../internal-urls/router";
|
|
13
|
+
import { closeDaemonClients } from "../launch/client";
|
|
13
14
|
import { discoverAndLoadMCPTools } from "../mcp/loader";
|
|
14
15
|
import { MCPManager } from "../mcp/manager";
|
|
15
16
|
import { discoverAuthStorage } from "../session/auth-broker-config";
|
|
@@ -93,6 +94,7 @@ export async function runReadCommand(cmd: ReadCommandArgs): Promise<void> {
|
|
|
93
94
|
if (MCPManager.instance() === mcpManager) MCPManager.setInstance(undefined);
|
|
94
95
|
}
|
|
95
96
|
authStorage?.close();
|
|
97
|
+
await closeDaemonClients();
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
if (failed) process.exit(1);
|
package/src/markit/NOTICE
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
Portions of this in-house document-to-markdown engine are adapted from
|
|
2
2
|
markit-ai (https://github.com/Michaelliv/markit), used under the MIT License.
|
|
3
|
+
This attribution covers the shared registry/types and the DOCX, PPTX, XLSX,
|
|
4
|
+
and EPUB converters. The PDF converter is implemented separately and is not
|
|
5
|
+
derived from markit-ai.
|
|
3
6
|
|
|
4
7
|
Copyright (c) 2026 Michael Liv
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
`.rtf` are routed by the read/fetch tools but have no converter — they surface
|
|
11
|
-
a conversion error, exactly as upstream markit did. Logic is ported faithfully
|
|
12
|
-
so conversion output matches the upstream package.
|
|
9
|
+
The CLI, plugin/provider, and unused converters (html, image, audio,
|
|
10
|
+
plain-text, rss, github, wikipedia, csv, json, yaml, ipynb, iwork, zip, xml)
|
|
11
|
+
were dropped. Legacy binary `.doc`/`.ppt`/`.xls` and `.rtf` have no converter
|
|
12
|
+
and surface a conversion error.
|
|
13
13
|
|
|
14
14
|
MIT License
|
|
15
15
|
|
|
@@ -1,48 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* PDF to Markdown converter.
|
|
5
|
-
*
|
|
6
|
-
* Uses mupdf (native WASM) for fast PDF parsing and a custom pipeline for
|
|
7
|
-
* table detection via vector line extraction + raycasting.
|
|
8
|
-
*
|
|
9
|
-
* Pipeline:
|
|
10
|
-
* 1. Extract text boxes + vector segments + image regions per page (mupdf)
|
|
11
|
-
* 2. Detect column layout (single vs multi-column)
|
|
12
|
-
* 3. Per column: detect table grids from segments (grid detection + raycasting)
|
|
13
|
-
* 4. Render diagrams as PNG files (if output directory provided)
|
|
14
|
-
* 5. Render tables as markdown tables, free text as paragraphs/headings
|
|
15
|
-
*/
|
|
16
|
-
import * as path from "node:path";
|
|
1
|
+
import { pdfToMarkdown } from "@oh-my-pi/pi-natives";
|
|
17
2
|
import type { ConversionResult, Converter, StreamInfo } from "../../types";
|
|
18
|
-
import { detectColumns } from "./columns";
|
|
19
|
-
import { extractPages, renderImageRegion } from "./extract";
|
|
20
|
-
import { resolveTableGrids } from "./grid";
|
|
21
|
-
import { stripHeadersFooters } from "./headers";
|
|
22
|
-
import { renderPageContent } from "./render";
|
|
23
|
-
import type { Segment, TextBox } from "./types";
|
|
24
3
|
|
|
25
4
|
const EXTENSIONS = [".pdf"];
|
|
26
5
|
const MIMETYPES = ["application/pdf", "application/x-pdf"];
|
|
27
6
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Process a set of text boxes (one column or full page): run table detection,
|
|
32
|
-
* separate free text, and render to markdown.
|
|
33
|
-
*/
|
|
34
|
-
function processColumn(
|
|
35
|
-
pageNumber: number,
|
|
36
|
-
textBoxes: TextBox[],
|
|
37
|
-
segments: Segment[],
|
|
38
|
-
imageBlocks: ImageBlock[],
|
|
39
|
-
): string {
|
|
40
|
-
const { grids, consumedIds } = resolveTableGrids(pageNumber, textBoxes, segments);
|
|
41
|
-
const consumedSet = new Set(consumedIds);
|
|
42
|
-
const freeTextBoxes = textBoxes.filter(tb => !consumedSet.has(tb.id));
|
|
43
|
-
return renderPageContent(freeTextBoxes, grids, imageBlocks, textBoxes);
|
|
44
|
-
}
|
|
45
|
-
|
|
7
|
+
/** Converts PDF buffers to Markdown through the native `pdf-inspector` bridge. */
|
|
46
8
|
export class PdfConverter implements Converter {
|
|
47
9
|
name = "pdf";
|
|
48
10
|
|
|
@@ -56,91 +18,17 @@ export class PdfConverter implements Converter {
|
|
|
56
18
|
return false;
|
|
57
19
|
}
|
|
58
20
|
|
|
59
|
-
async convert(input: Buffer,
|
|
60
|
-
const
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
for (const img of page.images) {
|
|
72
|
-
const filename = `${img.id}.png`;
|
|
73
|
-
const filepath = path.join(imageDir, filename);
|
|
74
|
-
try {
|
|
75
|
-
const png = await renderImageRegion(pdfBytes, img);
|
|
76
|
-
await Bun.write(filepath, png);
|
|
77
|
-
imageBlocks.push({ topY: img.topY, markdown: `` });
|
|
78
|
-
} catch {
|
|
79
|
-
// Image rendering failed — skip.
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} else if (page.images.length > 0) {
|
|
83
|
-
for (const img of page.images) {
|
|
84
|
-
imageBlocks.push({
|
|
85
|
-
topY: img.topY,
|
|
86
|
-
markdown: `<!-- image: ${img.id} (page ${img.pageNumber}, ${img.bbox.w}x${img.bbox.h}pt) -->`,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Detect column layout.
|
|
92
|
-
// If the page has vertical segments (tables), suppress column detection
|
|
93
|
-
// when one detected column is very narrow — that's a table's first column,
|
|
94
|
-
// not a page layout column.
|
|
95
|
-
const layout = detectColumns(page.textBoxes);
|
|
96
|
-
if (layout.columnCount > 1 && page.segments.some(s => Math.abs(s.x1 - s.x2) <= 0.8)) {
|
|
97
|
-
const pageXMin = Math.min(...page.textBoxes.map(tb => tb.bounds.left));
|
|
98
|
-
const pageXMax = Math.max(...page.textBoxes.map(tb => tb.bounds.right));
|
|
99
|
-
const pageWidth = pageXMax - pageXMin;
|
|
100
|
-
const minColFraction = 0.3;
|
|
101
|
-
const tooNarrow = layout.columns.some(col => {
|
|
102
|
-
const colXMin = Math.min(...col.map(tb => tb.bounds.left));
|
|
103
|
-
const colXMax = Math.max(...col.map(tb => tb.bounds.right));
|
|
104
|
-
return (colXMax - colXMin) / pageWidth < minColFraction;
|
|
105
|
-
});
|
|
106
|
-
if (tooNarrow) {
|
|
107
|
-
layout.columnCount = 1;
|
|
108
|
-
layout.columns = [page.textBoxes];
|
|
109
|
-
layout.boundaries = [];
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (layout.columnCount === 1) {
|
|
114
|
-
// Single column — process normally.
|
|
115
|
-
const md = processColumn(page.pageNumber, page.textBoxes, page.segments, imageBlocks);
|
|
116
|
-
if (md.length > 0) pageMarkdowns.push(md);
|
|
117
|
-
} else {
|
|
118
|
-
// Multi-column — process each column independently, then join.
|
|
119
|
-
const columnMarkdowns: string[] = [];
|
|
120
|
-
for (const colBoxes of layout.columns) {
|
|
121
|
-
// Filter segments to those within this column's X range.
|
|
122
|
-
const colXMin = Math.min(...colBoxes.map(tb => tb.bounds.left));
|
|
123
|
-
const colXMax = Math.max(...colBoxes.map(tb => tb.bounds.right));
|
|
124
|
-
const margin = 10;
|
|
125
|
-
const colSegments = page.segments.filter(seg => {
|
|
126
|
-
const segXMin = Math.min(seg.x1, seg.x2);
|
|
127
|
-
const segXMax = Math.max(seg.x1, seg.x2);
|
|
128
|
-
return segXMax >= colXMin - margin && segXMin <= colXMax + margin;
|
|
129
|
-
});
|
|
130
|
-
// Images go with the first column only (no X info to split by).
|
|
131
|
-
const md = processColumn(
|
|
132
|
-
page.pageNumber,
|
|
133
|
-
colBoxes,
|
|
134
|
-
colSegments,
|
|
135
|
-
columnMarkdowns.length === 0 ? imageBlocks : [],
|
|
136
|
-
);
|
|
137
|
-
if (md.length > 0) columnMarkdowns.push(md);
|
|
138
|
-
}
|
|
139
|
-
const joined = columnMarkdowns.join("\n\n");
|
|
140
|
-
if (joined.length > 0) pageMarkdowns.push(joined);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return { markdown: pageMarkdowns.join("\n\n") };
|
|
21
|
+
async convert(input: Buffer, _streamInfo: StreamInfo): Promise<ConversionResult> {
|
|
22
|
+
const result = await pdfToMarkdown(input);
|
|
23
|
+
const notice =
|
|
24
|
+
result.pagesNeedingOcr.length > 0
|
|
25
|
+
? `Text extraction is incomplete for PDF pages ${result.pagesNeedingOcr.join(", ")}. Use the browser tool to render those pages or OCR them.`
|
|
26
|
+
: undefined;
|
|
27
|
+
|
|
28
|
+
const conversion: ConversionResult = {
|
|
29
|
+
markdown: notice ? [result.markdown, notice].filter(Boolean).join("\n\n") : result.markdown,
|
|
30
|
+
};
|
|
31
|
+
if (result.title !== undefined) conversion.title = result.title;
|
|
32
|
+
return conversion;
|
|
145
33
|
}
|
|
146
34
|
}
|
package/src/mcp/client.ts
CHANGED
|
@@ -92,7 +92,7 @@ async function initializeConnection(
|
|
|
92
92
|
transport: MCPTransport,
|
|
93
93
|
options?: {
|
|
94
94
|
signal?: AbortSignal;
|
|
95
|
-
/** Called after
|
|
95
|
+
/** Called after notifications/initialized succeeds. */
|
|
96
96
|
onInitialized?: () => void | Promise<void>;
|
|
97
97
|
},
|
|
98
98
|
): Promise<MCPInitializeResult> {
|
|
@@ -119,14 +119,13 @@ async function initializeConnection(
|
|
|
119
119
|
// initialize; transports that don't need it ignore this.
|
|
120
120
|
transport.setProtocolVersion?.(result.protocolVersion);
|
|
121
121
|
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
await options?.onInitialized?.();
|
|
126
|
-
|
|
127
|
-
// Send initialized notification
|
|
122
|
+
// Send initialized before opening the optional GET SSE stream. Servers may
|
|
123
|
+
// reject or terminate sessions that receive session traffic before this
|
|
124
|
+
// notification; POST response streams already carry messages during setup.
|
|
128
125
|
await transport.notify("notifications/initialized");
|
|
129
126
|
|
|
127
|
+
await options?.onInitialized?.();
|
|
128
|
+
|
|
130
129
|
return result;
|
|
131
130
|
}
|
|
132
131
|
|
|
@@ -162,8 +161,8 @@ export async function connectToServer(
|
|
|
162
161
|
const initResult = await initializeConnection(transport, {
|
|
163
162
|
signal: options?.signal,
|
|
164
163
|
async onInitialized() {
|
|
165
|
-
// Open the SSE stream
|
|
166
|
-
//
|
|
164
|
+
// Open the optional GET SSE stream only after the initialized
|
|
165
|
+
// notification makes the session ready for further traffic.
|
|
167
166
|
if ("startSSEListener" in transport! && typeof transport!.startSSEListener === "function") {
|
|
168
167
|
await (transport as { startSSEListener(): Promise<void> }).startSSEListener();
|
|
169
168
|
}
|
|
@@ -141,6 +141,8 @@ export class EventController {
|
|
|
141
141
|
// restored when the banner clears at the next `agent_start` (see
|
|
142
142
|
// #handleMessageEnd / #handleAgentStart).
|
|
143
143
|
#pinnedErrorComponent: AssistantMessageComponent | undefined = undefined;
|
|
144
|
+
#pinnedErrorMessage: AssistantMessage | undefined = undefined;
|
|
145
|
+
#restorePinnedErrorInline = true;
|
|
144
146
|
#retrySupersededAssistantComponents = new Map<string, AssistantMessageComponent>();
|
|
145
147
|
#retrySupersededAssistantQueue: AssistantMessageComponent[] = [];
|
|
146
148
|
// Set when `auto_retry_start` fires and cleared by `auto_retry_end` (both
|
|
@@ -650,6 +652,8 @@ export class EventController {
|
|
|
650
652
|
this.#readToolCallAssistantComponents.clear();
|
|
651
653
|
this.#lastAssistantComponent = undefined;
|
|
652
654
|
this.#pinnedErrorComponent = undefined;
|
|
655
|
+
this.#pinnedErrorMessage = undefined;
|
|
656
|
+
this.#restorePinnedErrorInline = true;
|
|
653
657
|
this.#retryPending = this.ctx.viewSession.isRetrying;
|
|
654
658
|
this.#cancelIdleCompaction();
|
|
655
659
|
this.#cancelIdleRecap();
|
|
@@ -743,10 +747,13 @@ export class EventController {
|
|
|
743
747
|
this.#resetReadGroup();
|
|
744
748
|
this.#resolveDisplaceableTodo();
|
|
745
749
|
this.#lastAssistantComponent = undefined;
|
|
746
|
-
// Restore
|
|
747
|
-
//
|
|
748
|
-
|
|
750
|
+
// Restore terminal errors in transcript history when their banner clears.
|
|
751
|
+
// Recoverable empty-output attempts are discarded by session recovery and
|
|
752
|
+
// must stay hidden rather than resurfacing as a stale inline error.
|
|
753
|
+
if (this.#restorePinnedErrorInline) this.#pinnedErrorComponent?.setErrorPinned(false);
|
|
749
754
|
this.#pinnedErrorComponent = undefined;
|
|
755
|
+
this.#pinnedErrorMessage = undefined;
|
|
756
|
+
this.#restorePinnedErrorInline = true;
|
|
750
757
|
this.ctx.clearPinnedError();
|
|
751
758
|
if (this.ctx.retryLoader) {
|
|
752
759
|
this.ctx.retryLoader.stop();
|
|
@@ -1320,14 +1327,20 @@ export class EventController {
|
|
|
1320
1327
|
}
|
|
1321
1328
|
this.ctx.streamingComponent = undefined;
|
|
1322
1329
|
this.ctx.streamingMessage = undefined;
|
|
1323
|
-
// Pin a turn-ending provider error
|
|
1324
|
-
//
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1330
|
+
// Pin a turn-ending provider error above the editor so it survives
|
|
1331
|
+
// transcript scroll and suppress its duplicate inline row. Empty-output
|
|
1332
|
+
// errors are known intermediate attempts: hide them entirely while
|
|
1333
|
+
// session recovery continues, but retain the component so a terminal
|
|
1334
|
+
// retry-cap event can promote its final error into the one banner.
|
|
1327
1335
|
if (event.message.stopReason === "error" && event.message.errorMessage && !isSilentAbort(event.message)) {
|
|
1336
|
+
const recoverableEmptyOutput =
|
|
1337
|
+
!event.message.errorMessage.startsWith("Retry budget exhausted") &&
|
|
1338
|
+
AIError.is(AIError.classifyMessage(event.message), AIError.Flag.EmptyResponse);
|
|
1328
1339
|
this.#lastAssistantComponent?.setErrorPinned(true);
|
|
1329
1340
|
this.#pinnedErrorComponent = this.#lastAssistantComponent;
|
|
1330
|
-
this
|
|
1341
|
+
this.#pinnedErrorMessage = event.message;
|
|
1342
|
+
this.#restorePinnedErrorInline = !recoverableEmptyOutput;
|
|
1343
|
+
if (!recoverableEmptyOutput) this.ctx.showPinnedError(event.message.errorMessage);
|
|
1331
1344
|
}
|
|
1332
1345
|
this.ctx.statusLine.invalidate();
|
|
1333
1346
|
this.ctx.ui.requestRender();
|
|
@@ -1947,6 +1960,8 @@ export class EventController {
|
|
|
1947
1960
|
// restore its inline Error row; just unpin the fixed-region banner so the
|
|
1948
1961
|
// retry UI is the visible state.
|
|
1949
1962
|
this.#pinnedErrorComponent = undefined;
|
|
1963
|
+
this.#pinnedErrorMessage = undefined;
|
|
1964
|
+
this.#restorePinnedErrorInline = true;
|
|
1950
1965
|
this.ctx.clearPinnedError();
|
|
1951
1966
|
}
|
|
1952
1967
|
const delaySeconds = Math.round(event.delayMs / 1000);
|
|
@@ -1968,20 +1983,51 @@ export class EventController {
|
|
|
1968
1983
|
this.ctx.retryLoader = undefined;
|
|
1969
1984
|
this.ctx.statusContainer.disposeChildren();
|
|
1970
1985
|
}
|
|
1986
|
+
const pinnedError = this.#pinnedErrorMessage?.errorMessage;
|
|
1987
|
+
const terminalFailurePinned =
|
|
1988
|
+
!event.success &&
|
|
1989
|
+
this.#pinnedErrorComponent !== undefined &&
|
|
1990
|
+
pinnedError !== undefined &&
|
|
1991
|
+
pinnedError === event.finalError;
|
|
1992
|
+
let stalePinnedErrorCleared = false;
|
|
1993
|
+
if (!event.success && this.#pinnedErrorComponent && !terminalFailurePinned) {
|
|
1994
|
+
this.#pinnedErrorComponent.setErrorPinned(false);
|
|
1995
|
+
this.#pinnedErrorComponent = undefined;
|
|
1996
|
+
this.#pinnedErrorMessage = undefined;
|
|
1997
|
+
this.#restorePinnedErrorInline = true;
|
|
1998
|
+
this.ctx.clearPinnedError();
|
|
1999
|
+
stalePinnedErrorCleared = true;
|
|
2000
|
+
}
|
|
1971
2001
|
let appliedRetryUpdate = false;
|
|
1972
2002
|
for (const retryError of event.retryErrors ?? []) {
|
|
1973
2003
|
const component = this.#takeRetrySupersededAssistantComponent(retryError.persistenceKey);
|
|
1974
2004
|
if (!component) continue;
|
|
1975
2005
|
component.applyRetryRecovery(retryError.retryRecovery);
|
|
1976
|
-
if (this.#pinnedErrorComponent === component)
|
|
2006
|
+
if (!terminalFailurePinned && this.#pinnedErrorComponent === component) {
|
|
2007
|
+
this.#pinnedErrorComponent = undefined;
|
|
2008
|
+
this.#pinnedErrorMessage = undefined;
|
|
2009
|
+
this.#restorePinnedErrorInline = true;
|
|
2010
|
+
}
|
|
1977
2011
|
appliedRetryUpdate = true;
|
|
1978
2012
|
}
|
|
1979
|
-
if (
|
|
2013
|
+
if (
|
|
2014
|
+
!terminalFailurePinned &&
|
|
2015
|
+
!stalePinnedErrorCleared &&
|
|
2016
|
+
(appliedRetryUpdate || (event.retryErrors?.length ?? 0) > 0)
|
|
2017
|
+
) {
|
|
1980
2018
|
this.ctx.clearPinnedError();
|
|
1981
2019
|
}
|
|
1982
2020
|
this.#clearRetrySupersededAssistantComponents();
|
|
1983
2021
|
if (!event.success) {
|
|
1984
|
-
|
|
2022
|
+
if (terminalFailurePinned) {
|
|
2023
|
+
const terminalError = this.#restorePinnedErrorInline
|
|
2024
|
+
? `Retry failed after ${event.attempt} attempts: ${event.finalError || pinnedError || "Unknown error"}`
|
|
2025
|
+
: (pinnedError ?? event.finalError);
|
|
2026
|
+
if (terminalError) this.ctx.showPinnedError(terminalError);
|
|
2027
|
+
this.#restorePinnedErrorInline = true;
|
|
2028
|
+
} else {
|
|
2029
|
+
this.ctx.showError(`Retry failed after ${event.attempt} attempts: ${event.finalError || "Unknown error"}`);
|
|
2030
|
+
}
|
|
1985
2031
|
}
|
|
1986
2032
|
this.#ensureWorkingLoaderWhileStreaming();
|
|
1987
2033
|
this.ctx.ui.requestRender();
|
|
@@ -353,6 +353,13 @@ export class RpcClient {
|
|
|
353
353
|
// failures are reaped by the readyPromise catch below; established
|
|
354
354
|
// workers are reaped here so pending requests cannot hang indefinitely.
|
|
355
355
|
if (!readySettled) {
|
|
356
|
+
// Stdout can close before the exit reaper finishes draining stderr.
|
|
357
|
+
// child.exited settles only after the stderr tail is complete (for
|
|
358
|
+
// nonzero exits), so give it a bounded head start: the exit watcher
|
|
359
|
+
// below was registered first and rejects with the real stderr text
|
|
360
|
+
// instead of an empty "Stderr:" (flaked under full-suite load).
|
|
361
|
+
await Promise.race([child.exited.catch(() => {}), Bun.sleep(250)]);
|
|
362
|
+
if (readySettled) return;
|
|
356
363
|
readySettled = true;
|
|
357
364
|
readyReject(new Error(`Agent output stream ended before ready. Stderr: ${child.peekStderr()}`));
|
|
358
365
|
return;
|
|
@@ -39,7 +39,7 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
|
|
|
39
39
|
"| `Tab` | Path completion / accept autocomplete |",
|
|
40
40
|
`| \`${appKey(bindings, "app.interrupt")}\` | Cancel autocomplete / interrupt active work |`,
|
|
41
41
|
`| \`${appKey(bindings, "app.clear")}\` | Clear editor (first) / exit (second) |`,
|
|
42
|
-
`| \`${appKey(bindings, "app.exit")}\` | Exit (
|
|
42
|
+
`| \`${appKey(bindings, "app.exit")}\` | Exit (saves current prompt as draft) |`,
|
|
43
43
|
`| \`${appKey(bindings, "app.suspend")}\` | Suspend to background |`,
|
|
44
44
|
`| \`${appKey(bindings, "app.display.reset")}\` | Reset terminal display |`,
|
|
45
45
|
`| \`${appKey(bindings, "app.thinking.cycle")}\` | Cycle thinking level |`,
|
|
@@ -2854,11 +2854,18 @@ export class AgentSession {
|
|
|
2854
2854
|
// tool_result and corrupts message history. The handler also
|
|
2855
2855
|
// schedules its own retry, so a real empty stop never needs the
|
|
2856
2856
|
// active-goal threshold pre-empt below.
|
|
2857
|
-
|
|
2857
|
+
const emptyOutputRecovery = await this.#recovery.handleEmptyAssistantStop(msg);
|
|
2858
|
+
if (emptyOutputRecovery === "continue") {
|
|
2858
2859
|
maintenanceRoute("empty-stop-handled");
|
|
2859
2860
|
await emitAgentEndNotification({ willContinue: true });
|
|
2860
2861
|
return;
|
|
2861
2862
|
}
|
|
2863
|
+
if (emptyOutputRecovery === "terminal") {
|
|
2864
|
+
// The cap already closed retry state and made provider-empty errors
|
|
2865
|
+
// non-retryable. Continue through terminal maintenance so session_stop
|
|
2866
|
+
// hooks and queued follow-up handling retain their normal contract.
|
|
2867
|
+
maintenanceRoute("empty-stop-retry-cap");
|
|
2868
|
+
}
|
|
2862
2869
|
|
|
2863
2870
|
// Record quota exhaustion before deciding whether this failed turn may be
|
|
2864
2871
|
// replayed. Visible/side-effecting output then remains terminal while its
|