@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/task/executor.ts
CHANGED
|
@@ -166,6 +166,13 @@ export interface ExecutorOptions {
|
|
|
166
166
|
outputSchema?: unknown;
|
|
167
167
|
/** Parent task recursion depth (0 = top-level, 1 = first child, etc.) */
|
|
168
168
|
taskDepth?: number;
|
|
169
|
+
/**
|
|
170
|
+
* Override the `task.maxRuntimeMs` wall-clock cap for this run. When provided
|
|
171
|
+
* it wins over the settings value; `0` disables the per-subagent wall-clock
|
|
172
|
+
* limit entirely. Used by the eval `agent()` bridge, whose parent cell
|
|
173
|
+
* watchdog is already suspended for the call's duration.
|
|
174
|
+
*/
|
|
175
|
+
maxRuntimeMs?: number;
|
|
169
176
|
enableLsp?: boolean;
|
|
170
177
|
signal?: AbortSignal;
|
|
171
178
|
onProgress?: (progress: AgentProgress) => void;
|
|
@@ -625,7 +632,10 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
625
632
|
agent.readSummarize === false ? { "read.summarize.enabled": false } : undefined,
|
|
626
633
|
);
|
|
627
634
|
const maxRecursionDepth = settings.get("task.maxRecursionDepth") ?? 2;
|
|
628
|
-
const maxRuntimeMs = Math.max(
|
|
635
|
+
const maxRuntimeMs = Math.max(
|
|
636
|
+
0,
|
|
637
|
+
Math.trunc(Number(options.maxRuntimeMs ?? settings.get("task.maxRuntimeMs") ?? 0) || 0),
|
|
638
|
+
);
|
|
629
639
|
const parentDepth = options.taskDepth ?? 0;
|
|
630
640
|
const childDepth = parentDepth + 1;
|
|
631
641
|
const atMaxDepth = maxRecursionDepth >= 0 && childDepth >= maxRecursionDepth;
|
|
@@ -1484,7 +1494,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1484
1494
|
if (lastAssistant.stopReason === "aborted") {
|
|
1485
1495
|
aborted = abortReason === "signal" || runtimeLimitExceeded || abortReason === undefined;
|
|
1486
1496
|
if (aborted) {
|
|
1487
|
-
|
|
1497
|
+
// A real caller signal or the wall-clock timer carries a precise
|
|
1498
|
+
// reason (signal.reason / "runtime limit exceeded"). An internal
|
|
1499
|
+
// turn abort (abortReason === undefined) does NOT — prefer the
|
|
1500
|
+
// assistant message's own errorMessage ("Request was aborted" or a
|
|
1501
|
+
// specific stream error) over the misleading "Cancelled by caller".
|
|
1502
|
+
abortReasonText ??=
|
|
1503
|
+
abortReason === "signal" || runtimeLimitExceeded
|
|
1504
|
+
? resolveAbortReasonText()
|
|
1505
|
+
: lastAssistant.errorMessage?.trim() || resolveAbortReasonText();
|
|
1488
1506
|
}
|
|
1489
1507
|
exitCode = 1;
|
|
1490
1508
|
} else if (lastAssistant.stopReason === "error") {
|
package/src/task/render.ts
CHANGED
|
@@ -13,7 +13,6 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
|
13
13
|
import { formatContextUsage } from "../modes/components/status-line/context-thresholds";
|
|
14
14
|
import type { Theme } from "../modes/theme/theme";
|
|
15
15
|
import {
|
|
16
|
-
capPreviewLines,
|
|
17
16
|
formatBadge,
|
|
18
17
|
formatDuration,
|
|
19
18
|
formatMoreItems,
|
|
@@ -566,7 +565,7 @@ export function renderCall(
|
|
|
566
565
|
const content = line ? theme.fg("muted", replaceTabs(line)) : "";
|
|
567
566
|
return ` ${vertical} ${content}`;
|
|
568
567
|
});
|
|
569
|
-
lines.push(...
|
|
568
|
+
lines.push(...contextLines);
|
|
570
569
|
}
|
|
571
570
|
|
|
572
571
|
// `Tasks` is the last child unless the isolation flag follows it.
|
package/src/telemetry-export.ts
CHANGED
|
@@ -23,11 +23,7 @@
|
|
|
23
23
|
* `sdk-trace-base@2.7` exports cleanly on Bun.
|
|
24
24
|
*/
|
|
25
25
|
import { logger, postmortem } from "@oh-my-pi/pi-utils";
|
|
26
|
-
import
|
|
27
|
-
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto";
|
|
28
|
-
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
29
|
-
import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
30
|
-
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
26
|
+
import type * as TraceNode from "@opentelemetry/sdk-trace-node";
|
|
31
27
|
|
|
32
28
|
/**
|
|
33
29
|
* Periodic flush interval. A long-lived `omp` process (the ACP server is
|
|
@@ -36,7 +32,8 @@ import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
|
36
32
|
*/
|
|
37
33
|
const FLUSH_INTERVAL_MS = 30_000;
|
|
38
34
|
|
|
39
|
-
let provider: NodeTracerProvider | undefined;
|
|
35
|
+
let provider: TraceNode.NodeTracerProvider | undefined;
|
|
36
|
+
let initPromise: Promise<void> | undefined;
|
|
40
37
|
|
|
41
38
|
/**
|
|
42
39
|
* Whether {@link initTelemetryExport} registered a real provider. The CLI uses
|
|
@@ -53,8 +50,10 @@ export function isTelemetryExportEnabled(): boolean {
|
|
|
53
50
|
* the OTEL kill-switches are engaged), so it is safe to call unconditionally at
|
|
54
51
|
* startup.
|
|
55
52
|
*/
|
|
56
|
-
export function initTelemetryExport(): void {
|
|
53
|
+
export async function initTelemetryExport(): Promise<void> {
|
|
57
54
|
if (provider) return;
|
|
55
|
+
if (initPromise) return initPromise;
|
|
56
|
+
|
|
58
57
|
// The OTEL env contract parses booleans and enum lists case-insensitively, so
|
|
59
58
|
// OTEL_SDK_DISABLED=TRUE and OTEL_TRACES_EXPORTER=None must also disable export.
|
|
60
59
|
if (process.env.OTEL_SDK_DISABLED?.trim().toLowerCase() === "true") return;
|
|
@@ -77,6 +76,25 @@ export function initTelemetryExport(): void {
|
|
|
77
76
|
return;
|
|
78
77
|
}
|
|
79
78
|
|
|
79
|
+
initPromise = registerProvider();
|
|
80
|
+
return initPromise;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function registerProvider(): Promise<void> {
|
|
84
|
+
const [
|
|
85
|
+
{ AsyncLocalStorageContextManager },
|
|
86
|
+
{ OTLPTraceExporter },
|
|
87
|
+
{ resourceFromAttributes },
|
|
88
|
+
{ BatchSpanProcessor },
|
|
89
|
+
{ NodeTracerProvider },
|
|
90
|
+
] = await Promise.all([
|
|
91
|
+
import("@opentelemetry/context-async-hooks"),
|
|
92
|
+
import("@opentelemetry/exporter-trace-otlp-proto"),
|
|
93
|
+
import("@opentelemetry/resources"),
|
|
94
|
+
import("@opentelemetry/sdk-trace-base"),
|
|
95
|
+
import("@opentelemetry/sdk-trace-node"),
|
|
96
|
+
]);
|
|
97
|
+
|
|
80
98
|
// The exporter reads endpoint/headers/timeout from OTEL_EXPORTER_OTLP_* itself,
|
|
81
99
|
// so there is nothing to thread through here.
|
|
82
100
|
const exporter = new OTLPTraceExporter();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $
|
|
1
|
+
import { $flag } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { ToolSession } from ".";
|
|
3
3
|
|
|
4
4
|
export interface EvalBackendsAllowance {
|
|
@@ -6,21 +6,6 @@ export interface EvalBackendsAllowance {
|
|
|
6
6
|
js: boolean;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* Parse PI_PY / PI_JS environment variables. Each is a boolean flag; unset
|
|
11
|
-
* means "not specified, defer to settings". Returns null when neither is set
|
|
12
|
-
* so the caller can fall through to `readEvalBackendsAllowance` per key.
|
|
13
|
-
*/
|
|
14
|
-
function getEvalBackendsFromEnv(): EvalBackendsAllowance | null {
|
|
15
|
-
const pyEnv = $env.PI_PY;
|
|
16
|
-
const jsEnv = $env.PI_JS;
|
|
17
|
-
if (pyEnv === undefined && jsEnv === undefined) return null;
|
|
18
|
-
return {
|
|
19
|
-
python: pyEnv === undefined ? true : $flag("PI_PY"),
|
|
20
|
-
js: jsEnv === undefined ? true : $flag("PI_JS"),
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
|
|
24
9
|
/** Read per-backend allowance from settings (defaults true). */
|
|
25
10
|
export function readEvalBackendsAllowance(session: ToolSession): EvalBackendsAllowance {
|
|
26
11
|
return {
|
|
@@ -34,5 +19,9 @@ export function readEvalBackendsAllowance(session: ToolSession): EvalBackendsAll
|
|
|
34
19
|
* override the per-key settings; otherwise settings (defaults true) win.
|
|
35
20
|
*/
|
|
36
21
|
export function resolveEvalBackends(session: ToolSession): EvalBackendsAllowance {
|
|
37
|
-
|
|
22
|
+
const settings = readEvalBackendsAllowance(session);
|
|
23
|
+
return {
|
|
24
|
+
python: $flag("PI_PY", settings.python),
|
|
25
|
+
js: $flag("PI_JS", settings.js),
|
|
26
|
+
};
|
|
38
27
|
}
|
package/src/tools/eval-render.ts
CHANGED
|
@@ -40,14 +40,6 @@ import {
|
|
|
40
40
|
wrapBrackets,
|
|
41
41
|
} from "./render-utils";
|
|
42
42
|
export const EVAL_DEFAULT_PREVIEW_LINES = 10;
|
|
43
|
-
/**
|
|
44
|
-
* Rows of source kept in the *pending* eval preview. The window follows the
|
|
45
|
-
* streaming edge (newest lines pinned to the bottom) so you can watch the code
|
|
46
|
-
* being written, while staying bounded — a volatile tool block taller than the
|
|
47
|
-
* viewport would otherwise strand its scrolled-off head out of native scrollback
|
|
48
|
-
* on ED3-risk terminals. Matches the streaming windows used by edit/write.
|
|
49
|
-
*/
|
|
50
|
-
export const EVAL_STREAMING_PREVIEW_LINES = 12;
|
|
51
43
|
|
|
52
44
|
function languageForHighlighter(language: EvalLanguage | undefined): "python" | "javascript" {
|
|
53
45
|
return language === "js" ? "javascript" : "python";
|
|
@@ -517,15 +509,12 @@ export const evalToolRenderer = {
|
|
|
517
509
|
title: cell.title,
|
|
518
510
|
status: "pending",
|
|
519
511
|
width,
|
|
520
|
-
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
//
|
|
525
|
-
|
|
526
|
-
// out of native scrollback, cutting the box top. `Ctrl+O` lifts
|
|
527
|
-
// the window via `expanded` for a deliberate full view.
|
|
528
|
-
codeTail: true,
|
|
512
|
+
// Always render the full source: the code is fixed input, not the
|
|
513
|
+
// streaming part, so it is never compacted. While still pending
|
|
514
|
+
// (args streaming) the block is not yet committed to native
|
|
515
|
+
// scrollback — its head is only committed once a result exists and
|
|
516
|
+
// the code has finalized (see `isStreamingPreviewAppendOnly`).
|
|
517
|
+
codeMaxLines: Number.POSITIVE_INFINITY,
|
|
529
518
|
expanded: options.expanded,
|
|
530
519
|
animate,
|
|
531
520
|
},
|
|
@@ -628,7 +617,9 @@ export const evalToolRenderer = {
|
|
|
628
617
|
duration: cell.durationMs,
|
|
629
618
|
output: outputLines.length > 0 ? outputLines.join("\n") : undefined,
|
|
630
619
|
outputMaxLines: outputLines.length,
|
|
631
|
-
|
|
620
|
+
// Code is fixed input — always shown in full, never compacted.
|
|
621
|
+
// Only `output` honors the collapsed preview cap above.
|
|
622
|
+
codeMaxLines: Number.POSITIVE_INFINITY,
|
|
632
623
|
expanded,
|
|
633
624
|
width,
|
|
634
625
|
animate,
|
|
@@ -760,6 +751,18 @@ export const evalToolRenderer = {
|
|
|
760
751
|
},
|
|
761
752
|
};
|
|
762
753
|
},
|
|
754
|
+
|
|
755
|
+
// Append-only once a result exists (args complete → code finalized). The code
|
|
756
|
+
// is rendered in full as a fixed top-anchored prefix, and the streamed stdout
|
|
757
|
+
// below it only appends rows at the bottom, so the scrolled-off head commits
|
|
758
|
+
// to native scrollback instead of being yanked — collapsed or expanded, since
|
|
759
|
+
// the collapsed output cap keeps its sliding tail in the bottom live region.
|
|
760
|
+
// Returns false while still pending: the code is mid-stream (args incomplete)
|
|
761
|
+
// and its header still reads "pending", so committing it would strand a stale
|
|
762
|
+
// pending preview in history.
|
|
763
|
+
isStreamingPreviewAppendOnly(_args: EvalRenderArgs, _options: RenderResultOptions, result?: unknown): boolean {
|
|
764
|
+
return result != null;
|
|
765
|
+
},
|
|
763
766
|
mergeCallAndResult: true,
|
|
764
767
|
inline: true,
|
|
765
768
|
};
|
package/src/tools/eval.ts
CHANGED
|
@@ -130,11 +130,12 @@ function timeoutSecondsFromMs(timeoutMs: number): number {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
async function resolveBackend(session: ToolSession, language: EvalLanguage): Promise<ResolvedBackend> {
|
|
133
|
-
const
|
|
134
|
-
const
|
|
133
|
+
const backends = resolveEvalBackends(session);
|
|
134
|
+
const allowPy = backends.python;
|
|
135
|
+
const allowJs = backends.js;
|
|
135
136
|
|
|
136
137
|
if (language === "python") {
|
|
137
|
-
if (!allowPy) throw new ToolError("Python backend is disabled (eval.py = false).");
|
|
138
|
+
if (!allowPy) throw new ToolError("Python backend is disabled (PI_PY=0 or eval.py = false).");
|
|
138
139
|
if (!(await pythonBackend.isAvailable(session))) {
|
|
139
140
|
throw new ToolError(
|
|
140
141
|
'Python backend is unavailable in this session. Pass language: "js" or install the python kernel.',
|
|
@@ -142,7 +143,7 @@ async function resolveBackend(session: ToolSession, language: EvalLanguage): Pro
|
|
|
142
143
|
}
|
|
143
144
|
return { backend: pythonBackend };
|
|
144
145
|
}
|
|
145
|
-
if (!allowJs) throw new ToolError("JavaScript backend is disabled (eval.js = false).");
|
|
146
|
+
if (!allowJs) throw new ToolError("JavaScript backend is disabled (PI_JS=0 or eval.js = false).");
|
|
146
147
|
return { backend: jsBackend };
|
|
147
148
|
}
|
|
148
149
|
|
package/src/tools/fetch.ts
CHANGED
|
@@ -581,14 +581,25 @@ function parseFeedToMarkdown(content: string, maxItems = 10): string {
|
|
|
581
581
|
*/
|
|
582
582
|
const REMOTE_READER_MAX_MS = 10_000;
|
|
583
583
|
|
|
584
|
+
/** Reader backends for {@link renderHtmlToText}, in default priority order. */
|
|
585
|
+
export type FetchProvider = "native" | "trafilatura" | "lynx" | "parallel" | "jina";
|
|
586
|
+
|
|
587
|
+
const FETCH_PROVIDER_ORDER: readonly FetchProvider[] = ["native", "trafilatura", "lynx", "parallel", "jina"];
|
|
588
|
+
|
|
584
589
|
/**
|
|
585
|
-
* Render HTML to markdown
|
|
586
|
-
* in-process
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
*
|
|
591
|
-
*
|
|
590
|
+
* Render HTML to markdown by trying reader backends in priority order: native
|
|
591
|
+
* (in-process), trafilatura, lynx, Parallel, then Jina. The `providers.fetch`
|
|
592
|
+
* setting picks the order — `auto` uses the default above; any specific backend
|
|
593
|
+
* is tried first, then the remaining backends as fallbacks. Every backend's
|
|
594
|
+
* output must clear the same quality gate (>100 non-whitespace chars and not
|
|
595
|
+
* {@link isLowQualityOutput}) before it is accepted, otherwise the next backend
|
|
596
|
+
* is tried.
|
|
597
|
+
*
|
|
598
|
+
* The overall `timeout` budget bounds the whole call; remote backends (Parallel,
|
|
599
|
+
* Jina) are additionally capped at `REMOTE_READER_MAX_MS` so a hung endpoint
|
|
600
|
+
* cannot starve later renderers — especially the purely-local native converter,
|
|
601
|
+
* which always works on already-loaded HTML. Only a real `userSignal`
|
|
602
|
+
* cancellation aborts the chain (#1449).
|
|
592
603
|
*/
|
|
593
604
|
export async function renderHtmlToText(
|
|
594
605
|
url: string,
|
|
@@ -607,92 +618,74 @@ export async function renderHtmlToText(
|
|
|
607
618
|
signal: overallSignal,
|
|
608
619
|
};
|
|
609
620
|
const remoteBudgetMs = Math.min(timeout * 1000, REMOTE_READER_MAX_MS);
|
|
610
|
-
|
|
611
|
-
//
|
|
612
|
-
|
|
613
|
-
|
|
621
|
+
// Per-attempt budget for remote endpoints so one stall cannot consume the
|
|
622
|
+
// whole reader-mode budget and starve the local fallbacks.
|
|
623
|
+
const remoteSignal = () => ptree.combineSignals(userSignal, remoteBudgetMs);
|
|
624
|
+
|
|
625
|
+
const runners: Record<FetchProvider, () => Promise<string | null>> = {
|
|
626
|
+
// Purely local, no network/subprocess: still works on already-loaded HTML
|
|
627
|
+
// even after remote/subprocess attempts are aborted by the budget.
|
|
628
|
+
native: () => htmlToMarkdown(html, { cleanContent: true }),
|
|
629
|
+
trafilatura: async () => {
|
|
630
|
+
const trafilatura = await ensureTool("trafilatura", { signal: overallSignal, silent: true });
|
|
631
|
+
if (!trafilatura) return null;
|
|
632
|
+
const result = await ptree.exec([trafilatura, "-u", url, "--output-format", "markdown"], execOptions);
|
|
633
|
+
return result.ok ? result.stdout : null;
|
|
634
|
+
},
|
|
635
|
+
lynx: async () => {
|
|
636
|
+
if (!hasCommand("lynx")) return null;
|
|
637
|
+
const result = await ptree.exec(["lynx", "-dump", "-nolist", "-width", "250", url], execOptions);
|
|
638
|
+
return result.ok ? result.stdout : null;
|
|
639
|
+
},
|
|
640
|
+
parallel: async () => {
|
|
641
|
+
if (!findParallelApiKey(storage)) return null;
|
|
614
642
|
const parallelResult = await extractWithParallel(
|
|
615
643
|
[url],
|
|
616
|
-
{
|
|
617
|
-
objective: "Extract the main content",
|
|
618
|
-
excerpts: true,
|
|
619
|
-
fullContent: false,
|
|
620
|
-
signal: ptree.combineSignals(userSignal, remoteBudgetMs),
|
|
621
|
-
},
|
|
644
|
+
{ objective: "Extract the main content", excerpts: true, fullContent: false, signal: remoteSignal() },
|
|
622
645
|
storage,
|
|
623
646
|
);
|
|
624
647
|
const firstDocument = parallelResult.results[0];
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
// Try jina reader API with its own sub-budget so a stall cannot starve
|
|
638
|
-
// later fallbacks (#1449).
|
|
639
|
-
try {
|
|
640
|
-
const jinaUrl = `https://r.jina.ai/${url}`;
|
|
641
|
-
const response = await fetch(jinaUrl, {
|
|
642
|
-
headers: { Accept: "text/markdown" },
|
|
643
|
-
signal: ptree.combineSignals(userSignal, remoteBudgetMs),
|
|
644
|
-
});
|
|
645
|
-
if (response.ok) {
|
|
646
|
-
const content = await response.text();
|
|
647
|
-
if (content.trim().length > 100 && !isLowQualityOutput(content)) {
|
|
648
|
-
return { content, ok: true, method: "jina" };
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
} catch {
|
|
652
|
-
// Jina failed or stalled; honour real cancellation only.
|
|
653
|
-
userSignal?.throwIfAborted();
|
|
654
|
-
}
|
|
648
|
+
return firstDocument ? getParallelExtractContent(firstDocument) : null;
|
|
649
|
+
},
|
|
650
|
+
jina: async () => {
|
|
651
|
+
const response = await fetch(`https://r.jina.ai/${url}`, {
|
|
652
|
+
headers: { Accept: "text/markdown" },
|
|
653
|
+
signal: remoteSignal(),
|
|
654
|
+
});
|
|
655
|
+
return response.ok ? await response.text() : null;
|
|
656
|
+
},
|
|
657
|
+
};
|
|
655
658
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
659
|
+
const preference = settings.get("providers.fetch");
|
|
660
|
+
const order: readonly FetchProvider[] =
|
|
661
|
+
preference === "auto"
|
|
662
|
+
? FETCH_PROVIDER_ORDER
|
|
663
|
+
: [preference, ...FETCH_PROVIDER_ORDER.filter(method => method !== preference)];
|
|
664
|
+
|
|
665
|
+
// Highest-priority output that is substantial but fails the low-quality gate.
|
|
666
|
+
// Surfaced (ok: true) only when no backend clears the gate, so the caller's
|
|
667
|
+
// targeted fallbacks (llms.txt / document extraction) still run and we beat
|
|
668
|
+
// returning the unrendered raw HTML.
|
|
669
|
+
let lowQuality: { content: string; method: FetchProvider } | null = null;
|
|
670
|
+
|
|
671
|
+
for (const method of order) {
|
|
672
|
+
// Honour real user cancellation between attempts; remote per-attempt and
|
|
673
|
+
// overall-budget timeouts still fall through to later (local) renderers.
|
|
667
674
|
userSignal?.throwIfAborted();
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
if (lynx) {
|
|
674
|
-
const result = await ptree.exec(["lynx", "-dump", "-nolist", "-width", "250", url], execOptions);
|
|
675
|
-
if (result.ok) {
|
|
676
|
-
return { content: result.stdout, ok: true, method: "lynx" };
|
|
675
|
+
try {
|
|
676
|
+
const content = await runners[method]();
|
|
677
|
+
if (!content || content.trim().length <= 100) continue;
|
|
678
|
+
if (!isLowQualityOutput(content)) {
|
|
679
|
+
return { content, ok: true, method };
|
|
677
680
|
}
|
|
681
|
+
lowQuality ??= { content, method };
|
|
682
|
+
} catch {
|
|
683
|
+
userSignal?.throwIfAborted();
|
|
678
684
|
}
|
|
679
|
-
} catch {
|
|
680
|
-
// lynx failed or stalled; continue to native converter.
|
|
681
|
-
userSignal?.throwIfAborted();
|
|
682
685
|
}
|
|
683
686
|
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
// by the overall reader-mode timeout, this still works on already-loaded
|
|
687
|
-
// HTML (#1449).
|
|
688
|
-
try {
|
|
689
|
-
const content = await htmlToMarkdown(html, { cleanContent: true });
|
|
690
|
-
if (content.trim().length > 100 && !isLowQualityOutput(content)) {
|
|
691
|
-
return { content, ok: true, method: "native" };
|
|
692
|
-
}
|
|
693
|
-
} catch {
|
|
694
|
-
// Native converter failed; nothing else to try.
|
|
695
|
-
userSignal?.throwIfAborted();
|
|
687
|
+
if (lowQuality) {
|
|
688
|
+
return { content: lowQuality.content, ok: true, method: lowQuality.method };
|
|
696
689
|
}
|
|
697
690
|
return { content: "", ok: false, method: "none" };
|
|
698
691
|
}
|
|
@@ -1141,10 +1134,27 @@ async function renderUrl(
|
|
|
1141
1134
|
throw new ToolAbortError();
|
|
1142
1135
|
}
|
|
1143
1136
|
|
|
1144
|
-
// 5E: Render HTML
|
|
1137
|
+
// 5E: Render HTML via the reader-backend chain (native/trafilatura/lynx/parallel/jina)
|
|
1145
1138
|
const htmlResult = await renderHtmlToText(finalUrl, rawContent, timeout, settings, signal, storage);
|
|
1146
1139
|
if (!htmlResult.ok) {
|
|
1147
|
-
notes.push("html rendering failed (
|
|
1140
|
+
notes.push("html rendering failed (no reader backend produced usable output)");
|
|
1141
|
+
|
|
1142
|
+
const llmResult = await tryLlmEndpoints(finalUrl, timeout, signal);
|
|
1143
|
+
if (llmResult) {
|
|
1144
|
+
notes.push(`Used llms.txt fallback: ${llmResult.endpoint}`);
|
|
1145
|
+
const output = finalizeOutput(llmResult.content);
|
|
1146
|
+
return {
|
|
1147
|
+
url,
|
|
1148
|
+
finalUrl,
|
|
1149
|
+
contentType: "text/plain",
|
|
1150
|
+
method: "llms.txt",
|
|
1151
|
+
content: output.content,
|
|
1152
|
+
fetchedAt,
|
|
1153
|
+
truncated: output.truncated,
|
|
1154
|
+
notes,
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1148
1158
|
const output = finalizeOutput(rawContent);
|
|
1149
1159
|
return {
|
|
1150
1160
|
url,
|
|
@@ -221,10 +221,24 @@ export function formatMeta(meta: string[], theme: Theme): string {
|
|
|
221
221
|
return meta.length > 0 ? ` ${theme.fg("muted", meta.join(theme.sep.dot))}` : "";
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
-
|
|
224
|
+
function sanitizeErrorText(message: string | undefined): string {
|
|
225
225
|
const clean = (message ?? "").replace(/^Error:\s*/, "").trim();
|
|
226
|
-
|
|
227
|
-
|
|
226
|
+
return clean ? replaceTabs(truncateToWidth(clean, TRUNCATE_LENGTHS.LINE)) : "Unknown error";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function formatErrorMessage(message: string | undefined, theme: Theme): string {
|
|
230
|
+
return `${theme.styledSymbol("status.error", "error")} ${theme.fg("error", `Error: ${sanitizeErrorText(message)}`)}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Error message rendered as a subordinate detail line beneath a status header
|
|
235
|
+
* that already carries the error icon (e.g. `✘ Write: <path>`). The header's
|
|
236
|
+
* icon already signals failure, so this omits the redundant error symbol and
|
|
237
|
+
* "Error:" prefix that `formatErrorMessage` adds for standalone single-line
|
|
238
|
+
* errors, indenting two columns to sit under the header title instead.
|
|
239
|
+
*/
|
|
240
|
+
export function formatErrorDetail(message: string | undefined, theme: Theme): string {
|
|
241
|
+
return ` ${theme.fg("error", sanitizeErrorText(message))}`;
|
|
228
242
|
}
|
|
229
243
|
|
|
230
244
|
export function formatEmptyMessage(message: string, theme: Theme): string {
|
package/src/tools/renderers.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { sshToolRenderer } from "./ssh";
|
|
|
31
31
|
import { todoToolRenderer } from "./todo";
|
|
32
32
|
import { writeToolRenderer } from "./write";
|
|
33
33
|
|
|
34
|
-
type ToolRenderer = {
|
|
34
|
+
export type ToolRenderer = {
|
|
35
35
|
renderCall: (args: unknown, options: RenderResultOptions, theme: Theme) => Component;
|
|
36
36
|
renderResult: (
|
|
37
37
|
result: { content: Array<{ type: string; text?: string }>; details?: unknown; isError?: boolean },
|
|
@@ -40,6 +40,21 @@ type ToolRenderer = {
|
|
|
40
40
|
args?: unknown,
|
|
41
41
|
) => Component;
|
|
42
42
|
mergeCallAndResult?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* While a tool's preview is still streaming, report whether the
|
|
45
|
+
* currently-rendered preview is append-only: its rows only grow at the bottom
|
|
46
|
+
* and never re-layout above the bottom live region (a full, top-anchored
|
|
47
|
+
* content/code preview). The transcript reports this up to the TUI so a
|
|
48
|
+
* streaming preview taller than the viewport commits its scrolled-off head to
|
|
49
|
+
* native scrollback instead of dropping it (see
|
|
50
|
+
* `ToolExecutionComponent.isTranscriptBlockAppendOnly`). `result` is the
|
|
51
|
+
* latest (possibly partial) tool result, or `undefined` before one exists —
|
|
52
|
+
* `eval`/`bash` use its presence to defer committing until the streamed input
|
|
53
|
+
* (code) has finalized. Omit (or return `false`) for previews that slide a
|
|
54
|
+
* tail window or later collapse to a compact result — committing their head
|
|
55
|
+
* would strand stale rows.
|
|
56
|
+
*/
|
|
57
|
+
isStreamingPreviewAppendOnly?: (args: unknown, options: RenderResultOptions, result?: unknown) => boolean;
|
|
43
58
|
/** Render without background box, inline in the response flow */
|
|
44
59
|
inline?: boolean;
|
|
45
60
|
};
|
|
@@ -41,7 +41,7 @@ function buildReportToolIssueParams(activeBuiltinNames: readonly string[]) {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
export function isAutoQaEnabled(settings?: Settings): boolean {
|
|
44
|
-
return $flag("PI_AUTO_QA"
|
|
44
|
+
return $flag("PI_AUTO_QA", !!settings?.get("dev.autoqa"));
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// ───────────────────────────────────────────────────────────────────────────
|