@oh-my-pi/pi-coding-agent 17.3.3 → 17.3.5
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 +81 -0
- package/dist/{CHANGELOG-s5t1xdmy.md → CHANGELOG-tt9k4jpr.md} +81 -0
- package/dist/cli.js +3724 -3705
- package/dist/docs-index.generated.txt +1 -1
- package/dist/types/config/model-discovery.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/debug/report-bundle.d.ts +7 -2
- package/dist/types/extensibility/extensions/loader.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -3
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
- package/dist/types/launch/presence.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +22 -0
- package/dist/types/lsp/workspace-diagnostics.d.ts +2 -0
- package/dist/types/markit/converters/pdf/index.d.ts +2 -1
- package/dist/types/mcp/config.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +3 -1
- package/dist/types/registry/agent-lifecycle.d.ts +11 -0
- package/dist/types/registry/agent-registry.d.ts +4 -0
- package/dist/types/session/agent-session-types.d.ts +0 -2
- package/dist/types/session/agent-session.d.ts +20 -0
- package/dist/types/session/date-cwd-reminder.d.ts +22 -0
- package/dist/types/session/irc-bridge.d.ts +2 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-loader.d.ts +18 -11
- package/dist/types/session/session-maintenance.d.ts +6 -2
- package/dist/types/session/session-manager.d.ts +19 -3
- package/dist/types/session/session-tools.d.ts +0 -1
- package/dist/types/session/turn-recovery.d.ts +7 -7
- package/dist/types/tools/browser/attach.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +2 -2
- package/dist/types/tools/browser.d.ts +1 -1
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/hub/jobs.d.ts +6 -0
- package/dist/types/tools/hub/types.d.ts +6 -0
- 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/dist/types/vibe/runtime.d.ts +3 -3
- package/package.json +13 -16
- package/scripts/build-binary.ts +0 -2
- package/scripts/bundle-dist.ts +0 -1
- package/src/auto-thinking/classifier.ts +37 -23
- package/src/cli/models-cli.ts +1 -1
- package/src/cli/read-cli.ts +2 -0
- package/src/commit/analysis/conventional.ts +15 -9
- package/src/commit/analysis/summary.ts +15 -9
- package/src/commit/changelog/generate.ts +15 -9
- package/src/commit/map-reduce/map-phase.ts +3 -19
- package/src/commit/map-reduce/reduce-phase.ts +15 -9
- package/src/config/model-discovery.ts +3 -3
- package/src/config/model-registry.ts +29 -16
- package/src/config/settings-schema.ts +13 -0
- package/src/debug/report-bundle.ts +25 -59
- package/src/discovery/claude-plugins.ts +2 -1
- package/src/discovery/claude.ts +4 -2
- package/src/discovery/helpers.ts +5 -0
- package/src/edit/renderer.ts +48 -19
- package/src/extensibility/extensions/loader.ts +9 -5
- package/src/extensibility/extensions/runner.ts +124 -21
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/extensibility/extensions/wrapper.ts +16 -10
- package/src/extensibility/legacy-pi-ai-shim.ts +4 -0
- package/src/extensibility/plugins/marketplace/manager.ts +15 -18
- package/src/irc/bus.ts +1 -1
- package/src/launch/broker.ts +8 -1
- package/src/launch/presence.ts +77 -1
- package/src/lsp/client.ts +35 -3
- package/src/lsp/clients/biome-client.ts +47 -93
- package/src/lsp/servers.ts +17 -11
- package/src/lsp/tool.ts +2 -4
- package/src/lsp/workspace-diagnostics.ts +24 -2
- package/src/markit/NOTICE +8 -8
- package/src/markit/converters/pdf/index.ts +14 -126
- package/src/mcp/client.ts +8 -9
- package/src/mcp/config.ts +50 -6
- package/src/memories/index.ts +29 -25
- package/src/mnemopi/backend.ts +13 -11
- package/src/modes/components/ask-dialog.ts +25 -5
- package/src/modes/components/tool-execution.ts +10 -6
- package/src/modes/components/welcome.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +52 -32
- package/src/modes/controllers/selector-controller.ts +6 -0
- package/src/modes/controllers/tan-command-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +49 -10
- package/src/modes/rpc/rpc-client.ts +7 -0
- package/src/modes/theme/defaults/birch.json +2 -2
- package/src/modes/utils/hotkeys-markdown.ts +1 -1
- package/src/prompts/system/checkpoint-active-notice.md +5 -0
- package/src/prompts/system/date-cwd-reminder.md +3 -0
- package/src/prompts/system/project-prompt.md +0 -1
- package/src/prompts/system/rewind-report.md +1 -3
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/rewind.md +1 -13
- package/src/registry/agent-lifecycle.ts +34 -0
- package/src/registry/agent-registry.ts +27 -2
- package/src/registry/persisted-agents.ts +40 -20
- package/src/sdk.ts +32 -2
- package/src/session/agent-session-types.ts +0 -2
- package/src/session/agent-session.ts +178 -46
- package/src/session/date-cwd-reminder.ts +77 -0
- package/src/session/irc-bridge.ts +5 -0
- package/src/session/messages.ts +5 -1
- package/src/session/session-loader.ts +106 -64
- package/src/session/session-maintenance.ts +189 -115
- package/src/session/session-manager.ts +142 -35
- package/src/session/session-persistence.ts +4 -4
- package/src/session/session-storage.ts +18 -5
- package/src/session/session-tools.ts +5 -10
- package/src/session/turn-recovery.ts +102 -20
- package/src/session/unexpected-stop-classifier.ts +33 -21
- package/src/system-prompt.ts +0 -5
- package/src/task/executor.ts +4 -15
- package/src/task/persisted-revive.ts +3 -6
- package/src/tools/ask.ts +10 -3
- package/src/tools/browser/attach.ts +80 -25
- package/src/tools/browser/launch.ts +44 -15
- package/src/tools/browser/relay/daemon.ts +3 -7
- package/src/tools/browser/render.ts +1 -1
- package/src/tools/browser/shared-daemon.ts +3 -7
- package/src/tools/browser.ts +5 -5
- package/src/tools/builtin-names.ts +7 -3
- package/src/tools/checkpoint.ts +1 -7
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +20 -3
- package/src/tools/hub/types.ts +6 -0
- package/src/tools/read-pdf.ts +135 -0
- package/src/tools/read.ts +63 -37
- package/src/tools/render-utils.ts +53 -21
- package/src/tools/think.ts +15 -3
- package/src/tts/speech-enhancer.ts +19 -14
- package/src/utils/commit-message-generator.ts +14 -12
- package/src/utils/external-editor.ts +24 -5
- package/src/utils/markit.ts +6 -44
- package/src/utils/title-generator.ts +23 -19
- package/src/vibe/runtime.ts +32 -17
- 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
|
@@ -17,6 +17,7 @@ import { daemonClientForGlobal } from "../../../launch/client";
|
|
|
17
17
|
import { describeQuietly, stopQuietly, waitReady } from "../../../launch/ensure";
|
|
18
18
|
import { resolveWorkerSpawnCmd } from "../../../subprocess/worker-client";
|
|
19
19
|
import { throwIfAborted } from "../../tool-errors";
|
|
20
|
+
import { probeCdpStatus } from "../attach";
|
|
20
21
|
|
|
21
22
|
/** Stable broker daemon name for the relay server. */
|
|
22
23
|
export const RELAY_DAEMON_NAME = "omp.browser.relay";
|
|
@@ -30,13 +31,8 @@ const ENSURE_ATTEMPTS = 3;
|
|
|
30
31
|
|
|
31
32
|
/** True when the relay HTTP server answers /json/version at all (200 = extension connected, 503 = waiting for it). */
|
|
32
33
|
export async function probeRelayServer(cdpUrl: string): Promise<boolean> {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
await res.body?.cancel();
|
|
36
|
-
return res.ok || res.status === 503;
|
|
37
|
-
} catch {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
34
|
+
const status = await probeCdpStatus(`${cdpUrl}/json/version`, { timeoutMs: PROBE_TIMEOUT_MS });
|
|
35
|
+
return status === 503 || (status !== null && status >= 200 && status < 300);
|
|
40
36
|
}
|
|
41
37
|
|
|
42
38
|
/** Auto-start is only safe for endpoints this machine can own. */
|
|
@@ -162,7 +162,7 @@ function renderOpenOrCloseLine(
|
|
|
162
162
|
let title: string;
|
|
163
163
|
if (action === "close") {
|
|
164
164
|
const all = args.all === true || (args.name === undefined && details?.name === undefined);
|
|
165
|
-
title = all ? "
|
|
165
|
+
title = all ? "Release all tabs" : `Release ${tabLabel(args, details)}`;
|
|
166
166
|
if (args.kill) title += " (kill)";
|
|
167
167
|
} else {
|
|
168
168
|
title = `Open ${tabLabel(args, details)}`;
|
|
@@ -16,6 +16,7 @@ import { describeQuietly, stopQuietly, waitReady } from "../../launch/ensure";
|
|
|
16
16
|
import { daemonRuntimeDir } from "../../launch/paths";
|
|
17
17
|
import type { DaemonSnapshot } from "../../launch/protocol";
|
|
18
18
|
import { throwIfAborted } from "../tool-errors";
|
|
19
|
+
import { probeCdpStatus } from "./attach";
|
|
19
20
|
import { resolveSharedBrowserLaunchSpec } from "./launch";
|
|
20
21
|
|
|
21
22
|
/** Chrome prints this on stderr once the CDP listener is up; the broker's ready probe captures the line. */
|
|
@@ -50,13 +51,8 @@ async function probeEndpoint(wsEndpoint: string): Promise<boolean> {
|
|
|
50
51
|
} catch {
|
|
51
52
|
return false;
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
await res.body?.cancel();
|
|
56
|
-
return res.ok;
|
|
57
|
-
} catch {
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
54
|
+
const status = await probeCdpStatus(`http://${host}/json/version`, { timeoutMs: PROBE_TIMEOUT_MS });
|
|
55
|
+
return status !== null && status >= 200 && status < 300;
|
|
60
56
|
}
|
|
61
57
|
|
|
62
58
|
/**
|
package/src/tools/browser.ts
CHANGED
|
@@ -69,7 +69,7 @@ const browserSchema = type({
|
|
|
69
69
|
"dialogs?": type("'accept' | 'dismiss'").describe("auto-handle dialogs"),
|
|
70
70
|
"code?": type("string").describe("js body to run in tab"),
|
|
71
71
|
"timeout?": type("number").describe("timeout in seconds"),
|
|
72
|
-
"all?": type("boolean").describe("
|
|
72
|
+
"all?": type("boolean").describe("release every managed tab"),
|
|
73
73
|
"kill?": type("boolean").describe("also kill spawned-app browsers"),
|
|
74
74
|
});
|
|
75
75
|
|
|
@@ -133,7 +133,7 @@ function resolveBrowserKind(params: BrowserParams, session: ToolSession): Browse
|
|
|
133
133
|
/**
|
|
134
134
|
* Browser tool: stateful, multi-tab. Three actions:
|
|
135
135
|
* - `open` → acquire/create a named tab on a browser kind (headless | spawned | connected) and optionally goto a url.
|
|
136
|
-
* - `close` → release a named tab (or all
|
|
136
|
+
* - `close` → release a named tab handle (or all handles); attached/relay pages remain open, and spawned pages remain unless killed.
|
|
137
137
|
* - `run` → execute JS code against an existing tab with `page`/`browser`/`tab` helpers in scope.
|
|
138
138
|
*/
|
|
139
139
|
export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolDetails> {
|
|
@@ -204,7 +204,7 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
204
204
|
},
|
|
205
205
|
},
|
|
206
206
|
{
|
|
207
|
-
caption: "
|
|
207
|
+
caption: "Release every managed tab and kill spawned-app processes",
|
|
208
208
|
call: { action: "close", all: true, kill: true },
|
|
209
209
|
},
|
|
210
210
|
];
|
|
@@ -362,11 +362,11 @@ export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolD
|
|
|
362
362
|
const kill = !!params.kill;
|
|
363
363
|
if (params.all) {
|
|
364
364
|
const count = await untilAborted(signal, () => releaseAllTabs({ kill, timeoutMs }));
|
|
365
|
-
details.result = `
|
|
365
|
+
details.result = `Released ${count} managed tab${count === 1 ? "" : "s"}`;
|
|
366
366
|
return toolResult(details).text(details.result).done();
|
|
367
367
|
}
|
|
368
368
|
const closed = await untilAborted(signal, () => releaseTab(name, { kill, timeoutMs }));
|
|
369
|
-
details.result = closed ? `
|
|
369
|
+
details.result = closed ? `Released managed tab ${JSON.stringify(name)}` : `No tab named ${JSON.stringify(name)}`;
|
|
370
370
|
return toolResult(details).text(details.result).done();
|
|
371
371
|
}
|
|
372
372
|
|
|
@@ -41,10 +41,14 @@ const LEGACY_BUILTIN_TOOL_NAME_ALIASES: ReadonlyMap<string, BuiltinToolName> = n
|
|
|
41
41
|
["find", "glob"],
|
|
42
42
|
]);
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
const CANONICAL_TOOL_NAMES: Record<string, true> = Object.fromEntries(
|
|
45
|
+
[...BUILTIN_TOOL_NAMES, ...HIDDEN_TOOL_NAMES].map(name => [name, true]),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
/** Canonicalize built-in IDs and legacy aliases. Leave plugin names unchanged. */
|
|
45
49
|
export function normalizeToolName(name: string): string {
|
|
46
|
-
const
|
|
47
|
-
return LEGACY_BUILTIN_TOOL_NAME_ALIASES.get(
|
|
50
|
+
const lower = name.toLowerCase();
|
|
51
|
+
return LEGACY_BUILTIN_TOOL_NAME_ALIASES.get(lower) ?? (Object.hasOwn(CANONICAL_TOOL_NAMES, lower) ? lower : name);
|
|
48
52
|
}
|
|
49
53
|
|
|
50
54
|
/** Normalize and deduplicate tool names while preserving first-seen order. */
|
package/src/tools/checkpoint.ts
CHANGED
|
@@ -81,13 +81,7 @@ export class CheckpointTool implements AgentTool<typeof checkpointSchema, Checkp
|
|
|
81
81
|
}
|
|
82
82
|
const startedAt = new Date().toISOString();
|
|
83
83
|
return toolResult<CheckpointToolDetails>({ goal: params.goal, startedAt })
|
|
84
|
-
.text(
|
|
85
|
-
[
|
|
86
|
-
"Checkpoint created.",
|
|
87
|
-
`Goal: ${params.goal}`,
|
|
88
|
-
"Run your investigation, then call rewind with a concise report.",
|
|
89
|
-
].join("\n"),
|
|
90
|
-
)
|
|
84
|
+
.text([`Checkpoint: ${params.goal}`, "Finish exploration and formulate findings."].join("\n"))
|
|
91
85
|
.done();
|
|
92
86
|
}
|
|
93
87
|
}
|
package/src/tools/hub/index.ts
CHANGED
|
@@ -377,7 +377,7 @@ export class HubTool implements AgentTool<typeof hubSchema, HubDetails> {
|
|
|
377
377
|
// instead of blocking a full message-timeout window.
|
|
378
378
|
const hasRunningPeer = messaging.registry
|
|
379
379
|
.listVisibleTo(messaging.senderId)
|
|
380
|
-
.some(ref => ref
|
|
380
|
+
.some(ref => messaging.registry.isRunning(ref));
|
|
381
381
|
if (!hasRunningPeer) return nothingToWaitForResult(this.session);
|
|
382
382
|
}
|
|
383
383
|
return executeMessageWait(messaging, { from, timeoutMs: params.timeoutMs }, signal);
|
package/src/tools/hub/jobs.ts
CHANGED
|
@@ -87,6 +87,12 @@ export function visibleJobs(manager: AsyncJobManager, ids: string[], ownerId: st
|
|
|
87
87
|
* that activity instead of implying the system is quiet. Existence is
|
|
88
88
|
* already public via the peer roster, so listing ids here leaks nothing new;
|
|
89
89
|
* job *control* stays owner-scoped.
|
|
90
|
+
*
|
|
91
|
+
* Reporting deliberately uses the claimed `status`, not the session-corroborated
|
|
92
|
+
* `registry.isRunning` used by the wait-sustaining gates: a ref that claims
|
|
93
|
+
* `running` with no live turn is exactly the stale entry an operator must see
|
|
94
|
+
* here to cancel it (#8634). Hiding it would match the badge count to nothing
|
|
95
|
+
* and remove the only discovery path for the id.
|
|
90
96
|
*/
|
|
91
97
|
export function runningAgentsOutsideJobs(session: ToolSession): AgentActivitySnapshot[] {
|
|
92
98
|
const registry = session.agentRegistry;
|
|
@@ -113,6 +119,7 @@ export function runningAgentsOutsideJobs(session: ToolSession): AgentActivitySna
|
|
|
113
119
|
...(ref.parentId ? { parentId: ref.parentId } : {}),
|
|
114
120
|
...(ref.activity ? { activity: ref.activity } : {}),
|
|
115
121
|
ageMs: Math.max(0, now - ref.createdAt),
|
|
122
|
+
live: registry.isRunning(ref),
|
|
116
123
|
});
|
|
117
124
|
}
|
|
118
125
|
return out;
|
|
@@ -124,9 +131,15 @@ function describeAgents(agents: AgentActivitySnapshot[]): string[] {
|
|
|
124
131
|
for (const agent of agents) {
|
|
125
132
|
const parent = agent.parentId ? ` (spawned by \`${agent.parentId}\`)` : "";
|
|
126
133
|
const activity = agent.activity ? ` — ${agent.activity}` : "";
|
|
127
|
-
|
|
134
|
+
const stale = agent.live ? "" : " — no turn in flight (stale registration?)";
|
|
135
|
+
lines.push(`- \`${agent.id}\`${parent} — up ${formatDuration(agent.ageMs)}${activity}${stale}`);
|
|
128
136
|
}
|
|
129
137
|
lines.push("", "These agents have no job entry; message them via `hub` send, transcripts at `history://<id>`.");
|
|
138
|
+
if (agents.some(agent => !agent.live)) {
|
|
139
|
+
lines.push(
|
|
140
|
+
"An agent with no turn in flight cannot answer a message and never satisfies a bare `wait`; clear it with `hub` cancel.",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
130
143
|
return lines;
|
|
131
144
|
}
|
|
132
145
|
|
|
@@ -690,8 +703,12 @@ export function jobsRenderResult(
|
|
|
690
703
|
maxCollapsed: COLLAPSED_LIST_LIMIT,
|
|
691
704
|
itemType: "agent",
|
|
692
705
|
renderItem: agent => {
|
|
693
|
-
const icon =
|
|
694
|
-
|
|
706
|
+
const icon = agent.live
|
|
707
|
+
? formatStatusIcon("running", uiTheme, options.spinnerFrame)
|
|
708
|
+
: formatStatusIcon("warning", uiTheme);
|
|
709
|
+
const badge = agent.live
|
|
710
|
+
? formatBadge("agent", "accent", uiTheme)
|
|
711
|
+
: formatBadge("agent · no turn", "warning", uiTheme);
|
|
695
712
|
const gist = agent.activity
|
|
696
713
|
? ` ${uiTheme.fg("toolOutput", truncateToWidth(replaceTabs(agent.activity), LABEL_MAX_WIDTH, Ellipsis.Unicode))}`
|
|
697
714
|
: "";
|
package/src/tools/hub/types.ts
CHANGED
|
@@ -74,6 +74,12 @@ export interface AgentActivitySnapshot {
|
|
|
74
74
|
activity?: string;
|
|
75
75
|
/** Time since the agent was registered. */
|
|
76
76
|
ageMs: number;
|
|
77
|
+
/**
|
|
78
|
+
* Whether an attached session corroborates the `running` claim. False marks
|
|
79
|
+
* a ref that says `running` with no turn in flight — either a spawn still
|
|
80
|
+
* wiring up or a stale registration that `hub cancel <id>` clears (#8634).
|
|
81
|
+
*/
|
|
82
|
+
live: boolean;
|
|
77
83
|
}
|
|
78
84
|
|
|
79
85
|
/** Result details for messaging and job ops; fields are disjoint per op. */
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import type { ToolSession } from "../sdk";
|
|
4
|
+
import type { BrowserHandle } from "./browser/registry";
|
|
5
|
+
import type { ScreenshotResult } from "./browser/tab-protocol";
|
|
6
|
+
import { ToolAbortError, ToolError } from "./tool-errors";
|
|
7
|
+
|
|
8
|
+
const PDF_IMAGE_MEMBER_RE = /^(.*\.pdf):(.*)$/i;
|
|
9
|
+
const PDF_PAGE_MEMBER_RE = /^(?:p|page[-_]?)(\d+)(?:[-_].*)?\.png$/i;
|
|
10
|
+
const PDF_RENDER_TIMEOUT_MS = 30_000;
|
|
11
|
+
|
|
12
|
+
// Chromium's PDF plugin paints in an out-of-process frame after navigation has
|
|
13
|
+
// completed. Wait for document dimensions, then cross compositor boundaries
|
|
14
|
+
// before capturing; otherwise the screenshot can contain only the viewer shell.
|
|
15
|
+
const PDF_SCREENSHOT_CODE = `
|
|
16
|
+
let viewerFrame;
|
|
17
|
+
await wait(async () => {
|
|
18
|
+
for (const frame of page.frames()) {
|
|
19
|
+
try {
|
|
20
|
+
const loaded = await frame.evaluate(() => {
|
|
21
|
+
const viewer = document.querySelector("pdf-viewer");
|
|
22
|
+
const toolbar = viewer?.shadowRoot?.querySelector("viewer-toolbar");
|
|
23
|
+
const pageLength = toolbar
|
|
24
|
+
?.shadowRoot?.querySelector("viewer-page-selector")
|
|
25
|
+
?.shadowRoot?.querySelector("#pagelength")
|
|
26
|
+
?.textContent;
|
|
27
|
+
if (Number(pageLength) > 0 && !toolbar?.hasAttribute("loading_")) return true;
|
|
28
|
+
|
|
29
|
+
const plugin = document.querySelector('embed[type="application/x-google-chrome-pdf"]');
|
|
30
|
+
const sizer = document.querySelector("#sizer");
|
|
31
|
+
return plugin !== null && sizer !== null && sizer.clientWidth > 0 && sizer.clientHeight > 0;
|
|
32
|
+
});
|
|
33
|
+
if (loaded) {
|
|
34
|
+
viewerFrame = frame;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
} catch {}
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
});
|
|
41
|
+
await page.screenshot({ type: "png" });
|
|
42
|
+
await viewerFrame.evaluate(() => {
|
|
43
|
+
const { promise, resolve } = Promise.withResolvers();
|
|
44
|
+
requestAnimationFrame(() =>
|
|
45
|
+
requestAnimationFrame(() =>
|
|
46
|
+
requestAnimationFrame(() => requestAnimationFrame(resolve)),
|
|
47
|
+
),
|
|
48
|
+
);
|
|
49
|
+
return promise;
|
|
50
|
+
});
|
|
51
|
+
return await tab.screenshot({ fullPage: true, silent: true });
|
|
52
|
+
`;
|
|
53
|
+
|
|
54
|
+
/** A legacy PDF image-member path interpreted as a page screenshot request. */
|
|
55
|
+
export interface PdfImageReadTarget {
|
|
56
|
+
/** PDF path before the member delimiter. */
|
|
57
|
+
pdfPath: string;
|
|
58
|
+
/** Original member text after the delimiter. */
|
|
59
|
+
member: string;
|
|
60
|
+
/** One-indexed page inferred from names such as `p2-img0.png`; defaults to page 1. */
|
|
61
|
+
page: number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Parse a former PDF image-member path as a Chromium page screenshot request. */
|
|
65
|
+
export function splitPdfImageReadPath(readPath: string): PdfImageReadTarget | null {
|
|
66
|
+
const match = PDF_IMAGE_MEMBER_RE.exec(readPath);
|
|
67
|
+
const pdfPath = match?.[1];
|
|
68
|
+
const member = match?.[2];
|
|
69
|
+
if (!pdfPath || member === undefined) return null;
|
|
70
|
+
const pageText = PDF_PAGE_MEMBER_RE.exec(member)?.[1];
|
|
71
|
+
const parsedPage = pageText === undefined ? 1 : Number(pageText);
|
|
72
|
+
const page = Number.isSafeInteger(parsedPage) && parsedPage > 0 ? parsedPage : 1;
|
|
73
|
+
return { pdfPath, member, page };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Render one PDF page through the browser tool's shared headless Chromium. */
|
|
77
|
+
export async function renderPdfPageScreenshot(
|
|
78
|
+
session: ToolSession,
|
|
79
|
+
absolutePdfPath: string,
|
|
80
|
+
page: number,
|
|
81
|
+
signal?: AbortSignal,
|
|
82
|
+
): Promise<ScreenshotResult> {
|
|
83
|
+
const [{ acquireBrowser, holdBrowser, releaseBrowser }, { acquireTab, releaseTab, runInTab }] = await Promise.all([
|
|
84
|
+
import("./browser/registry"),
|
|
85
|
+
import("./browser/tab-supervisor"),
|
|
86
|
+
]);
|
|
87
|
+
const timeoutSignal = AbortSignal.timeout(PDF_RENDER_TIMEOUT_MS);
|
|
88
|
+
const renderSignal = signal ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal;
|
|
89
|
+
const tabName = `read-pdf-${Bun.randomUUIDv7()}`;
|
|
90
|
+
const url = pathToFileURL(absolutePdfPath);
|
|
91
|
+
url.hash = `page=${page}&toolbar=0&navpanes=0&view=Fit`;
|
|
92
|
+
|
|
93
|
+
let browserLease = false;
|
|
94
|
+
let tabOpened = false;
|
|
95
|
+
let browser: BrowserHandle | undefined;
|
|
96
|
+
try {
|
|
97
|
+
const acquiredBrowser = await untilAborted(renderSignal, () =>
|
|
98
|
+
acquireBrowser({ kind: "headless", headless: true }, { cwd: session.cwd, signal: renderSignal }),
|
|
99
|
+
);
|
|
100
|
+
browser = acquiredBrowser;
|
|
101
|
+
holdBrowser(acquiredBrowser);
|
|
102
|
+
browserLease = true;
|
|
103
|
+
await untilAborted(renderSignal, () =>
|
|
104
|
+
acquireTab(tabName, acquiredBrowser, {
|
|
105
|
+
url: url.href,
|
|
106
|
+
waitUntil: "load",
|
|
107
|
+
timeoutMs: PDF_RENDER_TIMEOUT_MS,
|
|
108
|
+
signal: renderSignal,
|
|
109
|
+
ownerSessionId: session.getSessionId?.() ?? undefined,
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
tabOpened = true;
|
|
113
|
+
await releaseBrowser(acquiredBrowser, { kill: false });
|
|
114
|
+
browserLease = false;
|
|
115
|
+
|
|
116
|
+
const result = await runInTab(tabName, {
|
|
117
|
+
code: PDF_SCREENSHOT_CODE,
|
|
118
|
+
timeoutMs: PDF_RENDER_TIMEOUT_MS,
|
|
119
|
+
signal: renderSignal,
|
|
120
|
+
session,
|
|
121
|
+
});
|
|
122
|
+
const screenshot = result.screenshots.at(-1);
|
|
123
|
+
if (!screenshot) throw new ToolError(`Chromium did not capture PDF page ${page}.`);
|
|
124
|
+
return screenshot;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (signal?.aborted) throw new ToolAbortError();
|
|
127
|
+
if (timeoutSignal.aborted) {
|
|
128
|
+
throw new ToolError(`Timed out rendering PDF page ${page} in Chromium.`);
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
} finally {
|
|
132
|
+
if (tabOpened) await releaseTab(tabName, { kill: false });
|
|
133
|
+
if (browserLease && browser) await releaseBrowser(browser, { kill: false });
|
|
134
|
+
}
|
|
135
|
+
}
|
package/src/tools/read.ts
CHANGED
|
@@ -100,7 +100,7 @@ import {
|
|
|
100
100
|
isRemoteMountPath,
|
|
101
101
|
type SuffixMatchCache,
|
|
102
102
|
} from "./read-path-resolution";
|
|
103
|
-
import {
|
|
103
|
+
import { type PdfImageReadTarget, renderPdfPageScreenshot, splitPdfImageReadPath } from "./read-pdf";
|
|
104
104
|
import { isMultiRange, isRawSelector, type ParsedSelector, parseSel, selToOffsetLimit } from "./read-selector";
|
|
105
105
|
import { readSqlite, resolveSqliteReadPath } from "./read-sqlite";
|
|
106
106
|
import { isProseSummaryPath, renderSummary, routeReadThroughBridge, trySummarize } from "./read-summary";
|
|
@@ -398,8 +398,13 @@ type ReadParams = ReadToolInput;
|
|
|
398
398
|
*/
|
|
399
399
|
export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
400
400
|
readonly name = "read";
|
|
401
|
-
readonly approval = (args: unknown): ToolTier =>
|
|
402
|
-
|
|
401
|
+
readonly approval = (args: unknown): ToolTier => {
|
|
402
|
+
let readPath = "";
|
|
403
|
+
if (args && typeof args === "object" && "path" in args) readPath = String(args.path ?? "");
|
|
404
|
+
if (pathTargetsSsh(readPath)) return "exec";
|
|
405
|
+
const target = splitPathAndSel(readPath);
|
|
406
|
+
return target.sel === undefined && splitPdfImageReadPath(readPath) ? "exec" : "read";
|
|
407
|
+
};
|
|
403
408
|
readonly label = "Read";
|
|
404
409
|
readonly loadMode = "essential";
|
|
405
410
|
description: string;
|
|
@@ -546,6 +551,40 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
546
551
|
return toolResult<ReadToolDetails>({ notes, displayReadTargets }).content(content).done();
|
|
547
552
|
}
|
|
548
553
|
|
|
554
|
+
async #readPdfPageScreenshot(options: {
|
|
555
|
+
readPath: string;
|
|
556
|
+
absolutePdfPath: string;
|
|
557
|
+
page: number;
|
|
558
|
+
pdfFileSize: number;
|
|
559
|
+
suffixResolution?: { from: string; to: string };
|
|
560
|
+
signal?: AbortSignal;
|
|
561
|
+
}): Promise<AgentToolResult<ReadToolDetails>> {
|
|
562
|
+
const { readPath, absolutePdfPath, page, pdfFileSize, suffixResolution, signal } = options;
|
|
563
|
+
const screenshot = await renderPdfPageScreenshot(this.session, absolutePdfPath, page, signal);
|
|
564
|
+
const screenshotFile = Bun.file(screenshot.dest);
|
|
565
|
+
const screenshotMetadata = await readImageMetadata(screenshot.dest);
|
|
566
|
+
const loaded = await this.#loadImageContent({
|
|
567
|
+
readPath,
|
|
568
|
+
absolutePath: screenshot.dest,
|
|
569
|
+
mimeType: screenshot.mimeType,
|
|
570
|
+
imageMetadata: screenshotMetadata,
|
|
571
|
+
fileSize: screenshotFile.size,
|
|
572
|
+
});
|
|
573
|
+
if (suffixResolution) {
|
|
574
|
+
const firstText = loaded.content.find((entry): entry is TextContent => entry.type === "text");
|
|
575
|
+
if (firstText) firstText.text = prependSuffixResolutionNotice(firstText.text, suffixResolution);
|
|
576
|
+
}
|
|
577
|
+
const image = loaded.content.find((entry): entry is ImageContent => entry.type === "image");
|
|
578
|
+
const details: ReadToolDetails = {
|
|
579
|
+
...loaded.details,
|
|
580
|
+
resolvedPath: absolutePdfPath,
|
|
581
|
+
contentType: image?.mimeType ?? screenshot.mimeType,
|
|
582
|
+
fileSize: pdfFileSize,
|
|
583
|
+
suffixResolution,
|
|
584
|
+
};
|
|
585
|
+
return toolResult(details).content(loaded.content).sourcePath(loaded.sourcePath).done();
|
|
586
|
+
}
|
|
587
|
+
|
|
549
588
|
/**
|
|
550
589
|
* Build content blocks for an on-disk image file: an `inspect_image`
|
|
551
590
|
* metadata note when inspection is active, otherwise the decoded image
|
|
@@ -892,7 +931,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
892
931
|
|
|
893
932
|
// Prefer a literal filesystem match over selector interpretation so real
|
|
894
933
|
// POSIX filenames containing selector-looking suffixes win over structured
|
|
895
|
-
// archive / sqlite /
|
|
934
|
+
// archive / sqlite / unsupported PDF-image dispatch. A selector promoted from local://
|
|
896
935
|
// remains separate so it cannot be mistaken for part of the resolved path.
|
|
897
936
|
const literalSplit =
|
|
898
937
|
promotedSelector === undefined
|
|
@@ -903,6 +942,8 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
903
942
|
? readPath.includes(":") && (await probeLiteralPathExists(readPath, this.session.cwd)) !== "missing"
|
|
904
943
|
: literalSplit.sel === undefined && splitPathAndSel(readPath).sel !== undefined;
|
|
905
944
|
|
|
945
|
+
let pdfImageRead: PdfImageReadTarget | null = null;
|
|
946
|
+
|
|
906
947
|
if (!rawPathIsLiteral) {
|
|
907
948
|
const archivePath = await resolveArchiveReadPath(this.session, readPath, suffixCache, signal);
|
|
908
949
|
if (archivePath) {
|
|
@@ -925,39 +966,14 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
925
966
|
return readSqlite(sqlitePath, signal);
|
|
926
967
|
}
|
|
927
968
|
|
|
928
|
-
const
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
const stat = await Bun.file(absolutePdfPath).stat();
|
|
934
|
-
if (stat.isDirectory())
|
|
935
|
-
throw new ToolError(`Path '${pdfImageMemberPath.pdfPath}' is a directory, not a PDF file`);
|
|
936
|
-
} catch (error) {
|
|
937
|
-
if (!isNotFoundError(error) || isRemoteMountPath(absolutePdfPath)) throw error;
|
|
938
|
-
const suffixMatch = await findSuffixMatchCached(
|
|
939
|
-
this.session,
|
|
940
|
-
suffixCache,
|
|
941
|
-
pdfImageMemberPath.pdfPath,
|
|
942
|
-
signal,
|
|
943
|
-
);
|
|
944
|
-
if (!suffixMatch) throw new ToolError(`Path '${pdfImageMemberPath.pdfPath}' not found`);
|
|
945
|
-
absolutePdfPath = suffixMatch.absolutePath;
|
|
946
|
-
suffixResolution = { from: pdfImageMemberPath.pdfPath, to: suffixMatch.displayPath };
|
|
947
|
-
}
|
|
948
|
-
return readPdfImageMember(
|
|
949
|
-
this.session,
|
|
950
|
-
this.#autoResizeImages,
|
|
951
|
-
absolutePdfPath,
|
|
952
|
-
pdfImageMemberPath.pdfPath,
|
|
953
|
-
pdfImageMemberPath.member,
|
|
954
|
-
suffixResolution,
|
|
955
|
-
signal,
|
|
956
|
-
);
|
|
957
|
-
}
|
|
969
|
+
const pdfCandidate = literalSplit.sel === undefined ? splitPdfImageReadPath(readPath) : null;
|
|
970
|
+
pdfImageRead =
|
|
971
|
+
pdfCandidate && (await probeLiteralPathExists(readPath, this.session.cwd)) === "missing"
|
|
972
|
+
? pdfCandidate
|
|
973
|
+
: null;
|
|
958
974
|
}
|
|
959
975
|
|
|
960
|
-
const localTarget = literalSplit;
|
|
976
|
+
const localTarget = pdfImageRead ? { path: pdfImageRead.pdfPath, sel: undefined } : literalSplit;
|
|
961
977
|
const localReadPath = localTarget.path;
|
|
962
978
|
const parsed = parseSel(localTarget.sel);
|
|
963
979
|
|
|
@@ -1036,6 +1052,17 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1036
1052
|
return this.#readFileConflicts(absolutePath, suffixResolution, signal);
|
|
1037
1053
|
}
|
|
1038
1054
|
|
|
1055
|
+
if (pdfImageRead) {
|
|
1056
|
+
return this.#readPdfPageScreenshot({
|
|
1057
|
+
readPath,
|
|
1058
|
+
absolutePdfPath: absolutePath,
|
|
1059
|
+
page: pdfImageRead.page,
|
|
1060
|
+
pdfFileSize: fileSize,
|
|
1061
|
+
suffixResolution,
|
|
1062
|
+
signal,
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1039
1066
|
const imageMetadata = await readImageMetadata(absolutePath);
|
|
1040
1067
|
const mimeType = imageMetadata?.mimeType;
|
|
1041
1068
|
const ext = path.extname(absolutePath).toLowerCase();
|
|
@@ -1102,8 +1129,7 @@ export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
|
1102
1129
|
// Convert document via markit.
|
|
1103
1130
|
const result = await convertFileWithMarkit(absolutePath, signal);
|
|
1104
1131
|
if (result.ok) {
|
|
1105
|
-
const renderedContent =
|
|
1106
|
-
ext === ".pdf" ? rewritePdfImagePlaceholders(result.content, resolvedDisplayPath) : result.content;
|
|
1132
|
+
const renderedContent = result.content;
|
|
1107
1133
|
// Route the converted markdown through the in-memory text builder
|
|
1108
1134
|
// so line-range selectors (`file.pdf:50-100`, `:5-16,40-80`) and
|
|
1109
1135
|
// raw mode apply against the converted output. Without this,
|
|
@@ -599,79 +599,111 @@ export function truncateDiffByHunk(
|
|
|
599
599
|
let keptHunks = 0;
|
|
600
600
|
|
|
601
601
|
for (const seg of segments) {
|
|
602
|
+
if (kept.length >= maxLines) break;
|
|
602
603
|
if (seg.isChange) {
|
|
604
|
+
if (keptHunks >= maxHunks) break;
|
|
603
605
|
keptHunks++;
|
|
604
|
-
if (keptHunks > maxHunks) break;
|
|
605
606
|
}
|
|
606
|
-
|
|
607
|
-
|
|
607
|
+
const take = Math.min(seg.lines.length, maxLines - kept.length);
|
|
608
|
+
for (let i = 0; i < take; i++) {
|
|
609
|
+
kept.push(seg.lines[i]!);
|
|
610
|
+
}
|
|
608
611
|
}
|
|
609
612
|
|
|
610
|
-
const keptStats = getDiffStats(kept.join("\n"));
|
|
611
613
|
return {
|
|
612
614
|
text: kept.join("\n"),
|
|
613
|
-
hiddenHunks: Math.max(0, totalStats.hunks -
|
|
615
|
+
hiddenHunks: Math.max(0, totalStats.hunks - keptHunks),
|
|
614
616
|
hiddenLines: Math.max(0, lines.length - kept.length),
|
|
615
617
|
};
|
|
616
618
|
}
|
|
617
619
|
|
|
618
620
|
const contextBudget = maxLines - changeLineCount;
|
|
619
|
-
const contextSegments = segments.filter(s => !s.isChange
|
|
621
|
+
const contextSegments = segments.filter(s => !s.isChange);
|
|
620
622
|
const totalContextLines = contextSegments.reduce((sum, s) => sum + s.lines.length, 0);
|
|
621
623
|
|
|
622
624
|
const kept: string[] = [];
|
|
623
625
|
let keptHunks = 0;
|
|
626
|
+
let keptSourceLines = 0;
|
|
624
627
|
|
|
625
628
|
if (totalContextLines <= contextBudget) {
|
|
626
629
|
for (const seg of segments) {
|
|
627
630
|
if (seg.isChange) {
|
|
631
|
+
if (keptHunks >= maxHunks) break;
|
|
628
632
|
keptHunks++;
|
|
629
|
-
if (keptHunks > maxHunks) break;
|
|
630
633
|
}
|
|
631
634
|
kept.push(...seg.lines);
|
|
635
|
+
keptSourceLines += seg.lines.length;
|
|
632
636
|
}
|
|
633
637
|
} else {
|
|
634
|
-
const contextRatio =
|
|
638
|
+
const contextRatio = totalContextLines > 0 ? contextBudget / totalContextLines : 0;
|
|
639
|
+
let remainingContextBudget = contextBudget;
|
|
635
640
|
|
|
636
641
|
for (let i = 0; i < segments.length; i++) {
|
|
637
642
|
const seg = segments[i];
|
|
638
643
|
|
|
639
644
|
if (seg.isChange) {
|
|
645
|
+
if (keptHunks >= maxHunks) break;
|
|
640
646
|
keptHunks++;
|
|
641
|
-
if (keptHunks > maxHunks) break;
|
|
642
|
-
kept.push(...seg.lines);
|
|
643
|
-
} else if (seg.isEllipsis) {
|
|
644
647
|
kept.push(...seg.lines);
|
|
648
|
+
keptSourceLines += seg.lines.length;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (remainingContextBudget <= 0) continue;
|
|
652
|
+
|
|
653
|
+
const allowedLines = Math.min(
|
|
654
|
+
remainingContextBudget,
|
|
655
|
+
Math.max(1, Math.floor(seg.lines.length * contextRatio)),
|
|
656
|
+
);
|
|
657
|
+
const outputStart = kept.length;
|
|
658
|
+
let sourceLinesAdded = 0;
|
|
659
|
+
|
|
660
|
+
if (seg.isEllipsis || seg.lines.length <= allowedLines) {
|
|
661
|
+
for (let j = 0; j < allowedLines; j++) {
|
|
662
|
+
kept.push(seg.lines[j]!);
|
|
663
|
+
}
|
|
664
|
+
sourceLinesAdded = allowedLines;
|
|
645
665
|
} else {
|
|
646
|
-
const allowedLines = Math.max(1, Math.floor(seg.lines.length * contextRatio));
|
|
647
666
|
const isBeforeChange = segments[i + 1]?.isChange;
|
|
648
667
|
const isAfterChange = segments[i - 1]?.isChange;
|
|
649
668
|
|
|
650
669
|
if (isBeforeChange && isAfterChange) {
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
670
|
+
if (allowedLines >= 3) {
|
|
671
|
+
const sourceBudget = allowedLines - 1;
|
|
672
|
+
const firstCount = Math.ceil(sourceBudget / 2);
|
|
673
|
+
const lastCount = sourceBudget - firstCount;
|
|
674
|
+
kept.push(...seg.lines.slice(0, firstCount));
|
|
654
675
|
kept.push("");
|
|
655
|
-
kept.push(...seg.lines.slice(-
|
|
676
|
+
if (lastCount > 0) kept.push(...seg.lines.slice(-lastCount));
|
|
677
|
+
sourceLinesAdded = sourceBudget;
|
|
656
678
|
} else {
|
|
657
|
-
|
|
679
|
+
const firstCount = Math.ceil(allowedLines / 2);
|
|
680
|
+
const lastCount = allowedLines - firstCount;
|
|
681
|
+
kept.push(...seg.lines.slice(0, firstCount));
|
|
682
|
+
if (lastCount > 0) kept.push(...seg.lines.slice(-lastCount));
|
|
683
|
+
sourceLinesAdded = allowedLines;
|
|
658
684
|
}
|
|
659
685
|
} else if (isBeforeChange) {
|
|
660
686
|
kept.push(...seg.lines.slice(-allowedLines));
|
|
687
|
+
sourceLinesAdded = allowedLines;
|
|
661
688
|
} else if (isAfterChange) {
|
|
662
689
|
kept.push(...seg.lines.slice(0, allowedLines));
|
|
690
|
+
sourceLinesAdded = allowedLines;
|
|
663
691
|
} else {
|
|
664
|
-
|
|
692
|
+
const take = Math.min(allowedLines, 2);
|
|
693
|
+
kept.push(...seg.lines.slice(0, take));
|
|
694
|
+
sourceLinesAdded = take;
|
|
665
695
|
}
|
|
666
696
|
}
|
|
697
|
+
|
|
698
|
+
keptSourceLines += sourceLinesAdded;
|
|
699
|
+
remainingContextBudget -= kept.length - outputStart;
|
|
667
700
|
}
|
|
668
701
|
}
|
|
669
702
|
|
|
670
|
-
const keptStats = getDiffStats(kept.join("\n"));
|
|
671
703
|
return {
|
|
672
704
|
text: kept.join("\n"),
|
|
673
|
-
hiddenHunks: Math.max(0, totalStats.hunks -
|
|
674
|
-
hiddenLines: Math.max(0, lines.length -
|
|
705
|
+
hiddenHunks: Math.max(0, totalStats.hunks - keptHunks),
|
|
706
|
+
hiddenLines: Math.max(0, lines.length - keptSourceLines),
|
|
675
707
|
};
|
|
676
708
|
}
|
|
677
709
|
|
package/src/tools/think.ts
CHANGED
|
@@ -8,14 +8,26 @@ import { getMarkdownTheme, type Theme } from "../modes/theme/theme";
|
|
|
8
8
|
/** Whether a model transport can suppress native reasoning while private scratchpad thoughts are active. */
|
|
9
9
|
export function supportsExternalThinking(model: Model | null | undefined): boolean {
|
|
10
10
|
if (!model) return false;
|
|
11
|
+
const compat = model.compat;
|
|
11
12
|
const requiresThinking =
|
|
12
13
|
model.api === "anthropic-messages" &&
|
|
13
|
-
|
|
14
|
-
"requiresThinkingEnabled" in
|
|
15
|
-
|
|
14
|
+
compat !== undefined &&
|
|
15
|
+
"requiresThinkingEnabled" in compat &&
|
|
16
|
+
compat.requiresThinkingEnabled === true;
|
|
16
17
|
if (model.reasoning && (requiresThinking || (model.thinking?.requiresEffort && !model.thinking.suppressWhenOff))) {
|
|
17
18
|
return false;
|
|
18
19
|
}
|
|
20
|
+
// Transports that reject `reasoning.effort` (xAI Grok 4 and the other
|
|
21
|
+
// reasoning-only Responses models) cannot honour `forceReasoningOff`, so the
|
|
22
|
+
// scratchpad would run alongside native reasoning instead of replacing it.
|
|
23
|
+
if (
|
|
24
|
+
model.reasoning &&
|
|
25
|
+
compat !== undefined &&
|
|
26
|
+
(("omitReasoningEffort" in compat && compat.omitReasoningEffort === true) ||
|
|
27
|
+
("supportsReasoningEffort" in compat && compat.supportsReasoningEffort === false))
|
|
28
|
+
) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
19
31
|
if (model.api === "google-generative-ai" || model.api === "google-gemini-cli" || model.api === "google-vertex") {
|
|
20
32
|
return !model.reasoning || model.thinking?.mode === "budget" || model.thinking?.suppressWhenOff === true;
|
|
21
33
|
}
|