@oh-my-pi/pi-coding-agent 16.1.22 → 16.2.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 +99 -1
- package/dist/cli.js +4387 -4164
- package/dist/types/advisor/__tests__/emission-guard.test.d.ts +1 -0
- package/dist/types/advisor/emission-guard.d.ts +73 -0
- package/dist/types/advisor/index.d.ts +1 -0
- package/dist/types/advisor/runtime.d.ts +7 -0
- package/dist/types/advisor/watchdog.d.ts +2 -0
- package/dist/types/cli/flag-tables.d.ts +1 -0
- package/dist/types/cli/gallery-fixtures/search.d.ts +1 -1
- package/dist/types/cli/gc-cli.d.ts +58 -0
- package/dist/types/collab/display-name.d.ts +3 -0
- package/dist/types/collab/host.d.ts +0 -2
- package/dist/types/commands/gc.d.ts +37 -0
- package/dist/types/commands/worktree.d.ts +0 -3
- package/dist/types/config/inline-tool-descriptors-mode.d.ts +13 -0
- package/dist/types/config/model-discovery.d.ts +6 -1
- package/dist/types/config/model-registry.d.ts +6 -2
- package/dist/types/config/model-resolver.d.ts +12 -0
- package/dist/types/config/models-config-schema.d.ts +29 -2
- package/dist/types/config/models-config.d.ts +22 -1
- package/dist/types/config/settings-schema.d.ts +145 -21
- package/dist/types/config/settings.d.ts +13 -0
- package/dist/types/dap/config.d.ts +1 -1
- package/dist/types/edit/hashline/filesystem.d.ts +4 -2
- package/dist/types/edit/renderer.d.ts +4 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -17
- package/dist/types/extensibility/hooks/types.d.ts +10 -10
- package/dist/types/extensibility/plugins/marketplace/types.d.ts +1 -0
- package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +1 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/registry-helpers.d.ts +2 -0
- package/dist/types/internal-urls/router.d.ts +1 -1
- package/dist/types/internal-urls/ssh-protocol.d.ts +10 -0
- package/dist/types/internal-urls/types.d.ts +19 -2
- package/dist/types/irc/bus.d.ts +6 -0
- package/dist/types/mcp/transports/stdio.d.ts +25 -1
- package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +1 -0
- package/dist/types/modes/components/custom-editor.d.ts +7 -0
- package/dist/types/modes/components/move-overlay.d.ts +23 -0
- package/dist/types/modes/components/plugin-selector.d.ts +2 -1
- package/dist/types/modes/components/queue-mode-selector.d.ts +2 -1
- package/dist/types/modes/components/select-list-mouse-routing.d.ts +2 -0
- package/dist/types/modes/components/settings-defs.d.ts +4 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/show-images-selector.d.ts +2 -1
- package/dist/types/modes/components/status-line/component.d.ts +2 -0
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/theme-selector.d.ts +2 -1
- package/dist/types/modes/components/thinking-selector.d.ts +2 -1
- package/dist/types/modes/controllers/command-controller.d.ts +10 -1
- package/dist/types/modes/controllers/streaming-reveal.d.ts +6 -0
- package/dist/types/modes/interactive-mode.d.ts +10 -1
- package/dist/types/modes/internal-url-autocomplete.d.ts +1 -1
- package/dist/types/modes/running-subagent-badge.d.ts +6 -0
- package/dist/types/modes/theme/mermaid-rendering.test.d.ts +1 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +9 -1
- package/dist/types/sdk.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +2 -1
- package/dist/types/session/session-listing.d.ts +10 -1
- package/dist/types/session/session-manager.d.ts +13 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +1 -0
- package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +1 -0
- package/dist/types/ssh/connection-manager.d.ts +2 -0
- package/dist/types/ssh/file-transfer.d.ts +79 -0
- package/dist/types/ssh/utils.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +5 -0
- package/dist/types/task/executor.d.ts +16 -0
- package/dist/types/tiny/text.d.ts +1 -1
- package/dist/types/tools/builtin-names.d.ts +5 -1
- package/dist/types/tools/{find.d.ts → glob.d.ts} +15 -15
- package/dist/types/tools/{search.d.ts → grep.d.ts} +14 -14
- package/dist/types/tools/index.d.ts +3 -3
- package/dist/types/tools/path-utils.d.ts +29 -0
- package/dist/types/tools/plan-mode-guard.d.ts +7 -0
- package/dist/types/tools/read.d.ts +2 -2
- package/dist/types/tools/render-utils.d.ts +8 -0
- package/dist/types/tools/renderers.d.ts +11 -0
- package/dist/types/tools/ssh.d.ts +2 -0
- package/dist/types/tools/todo.d.ts +0 -16
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/utils/active-repo-context.d.ts +8 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/utils/markit-cache.d.ts +23 -0
- package/dist/types/utils/markit.d.ts +5 -1
- package/dist/types/utils/prompt-path.d.ts +1 -0
- package/dist/types/web/search/providers/duckduckgo.d.ts +14 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +28 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +29 -0
- package/dist/types/web/search/providers/xai.d.ts +13 -0
- package/dist/types/web/search/types.d.ts +18 -2
- package/package.json +30 -15
- package/scripts/bench-guard.ts +1 -1
- package/scripts/build-binary.ts +9 -9
- package/scripts/bundle-dist.ts +2 -2
- package/src/advisor/__tests__/advisor.test.ts +40 -4
- package/src/advisor/__tests__/emission-guard.test.ts +147 -0
- package/src/advisor/advise-tool.ts +1 -1
- package/src/advisor/emission-guard.ts +172 -0
- package/src/advisor/index.ts +1 -0
- package/src/advisor/runtime.ts +11 -0
- package/src/advisor/watchdog.ts +12 -1
- package/src/cli/args.ts +5 -2
- package/src/cli/auth-broker-cli.ts +17 -0
- package/src/cli/config-cli.ts +4 -0
- package/src/cli/flag-tables.ts +7 -4
- package/src/cli/gallery-cli.ts +14 -2
- package/src/cli/gallery-fixtures/edit.ts +60 -0
- package/src/cli/gallery-fixtures/fs.ts +17 -17
- package/src/cli/gallery-fixtures/search.ts +4 -4
- package/src/cli/gc-cli.ts +939 -0
- package/src/cli/usage-cli.ts +20 -1
- package/src/cli/web-search-cli.ts +1 -1
- package/src/cli-commands.ts +1 -0
- package/src/collab/display-name.ts +13 -0
- package/src/collab/guest.ts +5 -1
- package/src/collab/host.ts +2 -13
- package/src/commands/gc.ts +46 -0
- package/src/commands/worktree.ts +6 -0
- package/src/config/inline-tool-descriptors-mode.ts +27 -0
- package/src/config/model-discovery.ts +77 -8
- package/src/config/model-registry.ts +89 -11
- package/src/config/model-resolver.ts +39 -1
- package/src/config/models-config-schema.ts +41 -5
- package/src/config/models-config.ts +4 -1
- package/src/config/settings-schema.ts +130 -27
- package/src/config/settings.ts +216 -7
- package/src/cursor.ts +1 -1
- package/src/dap/config.ts +152 -8
- package/src/dap/session.ts +1 -1
- package/src/discovery/helpers.ts +3 -1
- package/src/edit/hashline/diff.ts +14 -3
- package/src/edit/hashline/execute.ts +42 -6
- package/src/edit/hashline/filesystem.ts +49 -8
- package/src/edit/index.ts +1 -0
- package/src/edit/modes/patch.ts +11 -1
- package/src/edit/renderer.ts +140 -13
- package/src/eval/__tests__/agent-bridge.test.ts +101 -0
- package/src/eval/__tests__/julia-prelude.test.ts +18 -0
- package/src/eval/agent-bridge.ts +26 -3
- package/src/eval/jl/runner.jl +7 -1
- package/src/eval/js/tool-bridge.ts +2 -2
- package/src/export/html/index.ts +1 -1
- package/src/export/html/template.js +3 -1
- package/src/export/html/tool-views.generated.js +20 -20
- package/src/extensibility/extensions/types.ts +22 -22
- package/src/extensibility/hooks/types.ts +11 -11
- package/src/extensibility/plugins/legacy-pi-bundled-keys.ts +10 -3
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +31 -10
- package/src/extensibility/plugins/marketplace/manager.ts +22 -0
- package/src/extensibility/plugins/marketplace/types.ts +1 -0
- package/src/internal-urls/__tests__/ssh-protocol.test.ts +331 -0
- package/src/internal-urls/docs-index.generated.txt +2 -2
- package/src/internal-urls/docs-index.ts +2 -3
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/registry-helpers.ts +19 -4
- package/src/internal-urls/router.ts +5 -3
- package/src/internal-urls/ssh-protocol.ts +367 -0
- package/src/internal-urls/types.ts +19 -2
- package/src/irc/bus.ts +11 -3
- package/src/lsp/index.ts +8 -1
- package/src/mcp/oauth-discovery.ts +20 -20
- package/src/mcp/tool-bridge.ts +32 -2
- package/src/mcp/transports/stdio.test.ts +20 -3
- package/src/mcp/transports/stdio.ts +45 -14
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-event-mapper.ts +2 -2
- package/src/modes/components/__tests__/move-overlay.test.ts +166 -0
- package/src/modes/components/agent-hub.ts +3 -0
- package/src/modes/components/agent-transcript-viewer.ts +9 -7
- package/src/modes/components/custom-editor.ts +90 -5
- package/src/modes/components/move-overlay.ts +282 -0
- package/src/modes/components/plan-review-overlay.ts +35 -35
- package/src/modes/components/plugin-selector.ts +6 -1
- package/src/modes/components/queue-mode-selector.ts +6 -1
- package/src/modes/components/select-list-mouse-routing.ts +35 -0
- package/src/modes/components/session-selector.ts +11 -10
- package/src/modes/components/settings-defs.ts +14 -1
- package/src/modes/components/settings-selector.ts +196 -29
- package/src/modes/components/show-images-selector.ts +6 -1
- package/src/modes/components/status-line/component.ts +108 -28
- package/src/modes/components/status-line/segments.ts +5 -1
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/theme-selector.ts +6 -1
- package/src/modes/components/thinking-selector.ts +6 -1
- package/src/modes/components/tool-execution.ts +25 -9
- package/src/modes/components/tree-selector.ts +5 -5
- package/src/modes/controllers/command-controller.ts +140 -47
- package/src/modes/controllers/event-controller.ts +59 -3
- package/src/modes/controllers/input-controller.ts +70 -4
- package/src/modes/controllers/selector-controller.ts +15 -2
- package/src/modes/controllers/streaming-reveal.ts +17 -0
- package/src/modes/controllers/tool-args-reveal.ts +1 -1
- package/src/modes/interactive-mode.ts +142 -59
- package/src/modes/internal-url-autocomplete.ts +17 -2
- package/src/modes/prompt-action-autocomplete.ts +3 -1
- package/src/modes/running-subagent-badge.ts +13 -0
- package/src/modes/setup-wizard/scenes/glyph.ts +2 -13
- package/src/modes/setup-wizard/scenes/providers.ts +2 -1
- package/src/modes/setup-wizard/scenes/theme.ts +6 -12
- package/src/modes/setup-wizard/scenes/web-search.ts +8 -13
- package/src/modes/setup-wizard/wizard-overlay.ts +6 -5
- package/src/modes/theme/mermaid-rendering.test.ts +53 -0
- package/src/modes/theme/theme.ts +42 -15
- package/src/modes/types.ts +9 -1
- package/src/modes/utils/interactive-context-helpers.ts +1 -1
- package/src/priority.json +15 -0
- package/src/prompts/advisor/active-repo-watchdog.md +6 -0
- package/src/prompts/advisor/system.md +21 -7
- package/src/prompts/agents/designer.md +1 -1
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/agents/librarian.md +2 -2
- package/src/prompts/agents/plan.md +2 -2
- package/src/prompts/agents/reviewer.md +1 -1
- package/src/prompts/agents/task.md +2 -2
- package/src/prompts/system/active-repo-context.md +4 -0
- package/src/prompts/system/gemini-tool-call-reminder.md +9 -0
- package/src/prompts/system/plan-mode-active.md +12 -3
- package/src/prompts/system/project-prompt.md +2 -2
- package/src/prompts/system/system-prompt.md +8 -6
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/{find.md → glob.md} +1 -1
- package/src/prompts/tools/{search.md → grep.md} +3 -3
- package/src/prompts/tools/read.md +4 -2
- package/src/sdk.ts +98 -29
- package/src/session/agent-session.ts +828 -191
- package/src/session/session-history-format.ts +2 -2
- package/src/session/session-listing.ts +35 -2
- package/src/session/session-manager.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +158 -22
- package/src/slash-commands/helpers/usage-report.ts +23 -2
- package/src/ssh/__tests__/connection-manager-args.test.ts +69 -0
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +68 -0
- package/src/ssh/connection-manager.ts +12 -15
- package/src/ssh/file-transfer.ts +209 -0
- package/src/ssh/utils.ts +24 -0
- package/src/system-prompt.ts +60 -28
- package/src/task/executor.ts +96 -36
- package/src/task/index.ts +3 -3
- package/src/task/render.ts +14 -13
- package/src/task/worktree.ts +38 -1
- package/src/tiny/text.ts +49 -4
- package/src/tiny/worker.ts +3 -3
- package/src/tools/acp-bridge.ts +6 -1
- package/src/tools/bash.ts +2 -2
- package/src/tools/builtin-names.ts +26 -2
- package/src/tools/{find.ts → glob.ts} +48 -42
- package/src/tools/{search.ts → grep.ts} +311 -129
- package/src/tools/index.ts +13 -14
- package/src/tools/irc.ts +6 -2
- package/src/tools/path-utils.ts +64 -1
- package/src/tools/plan-mode-guard.ts +26 -13
- package/src/tools/read.ts +35 -6
- package/src/tools/render-utils.ts +14 -0
- package/src/tools/renderers.ts +15 -4
- package/src/tools/ssh.ts +17 -2
- package/src/tools/todo.ts +4 -26
- package/src/tools/write.ts +25 -9
- package/src/utils/active-repo-context.ts +143 -0
- package/src/utils/edit-mode.ts +19 -2
- package/src/utils/image-resize.ts +88 -7
- package/src/utils/lang-from-path.ts +3 -3
- package/src/utils/markit-cache.ts +166 -0
- package/src/utils/markit.ts +86 -18
- package/src/utils/prompt-path.ts +3 -0
- package/src/utils/shell-snapshot.ts +1 -1
- package/src/utils/title-generator.ts +1 -1
- package/src/web/search/provider.ts +54 -34
- package/src/web/search/providers/duckduckgo.ts +140 -0
- package/src/web/search/providers/firecrawl.ts +144 -0
- package/src/web/search/providers/tinyfish.ts +159 -0
- package/src/web/search/providers/xai.ts +292 -0
- package/src/web/search/providers/zai.ts +142 -56
- package/src/web/search/types.ts +6 -2
package/src/mcp/tool-bridge.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
|
7
7
|
import type { TSchema } from "@oh-my-pi/pi-ai";
|
|
8
8
|
import { normalizeSchemaForMCP } from "@oh-my-pi/pi-ai/utils/schema";
|
|
9
9
|
import { untilAborted } from "@oh-my-pi/pi-utils";
|
|
10
|
+
import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
|
|
10
11
|
import type { SourceMeta } from "../capability/types";
|
|
11
12
|
import type {
|
|
12
13
|
CustomTool,
|
|
@@ -83,6 +84,35 @@ function omitUnusedOptionalArgs(args: MCPToolArgs, inputSchema: MCPToolDefinitio
|
|
|
83
84
|
return cleaned ?? args;
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Drop the harness-internal intent field (`INTENT_FIELD`) before forwarding
|
|
89
|
+
* args to an MCP server. The harness injects `i` into every tool's wire
|
|
90
|
+
* schema; the direct model tool-call path strips it via `extractIntent`, but
|
|
91
|
+
* the `eval` `tool.*` bridge and any other in-process caller forwards args
|
|
92
|
+
* verbatim. Strict-schema servers (Linear, anything with
|
|
93
|
+
* `additionalProperties:false` / Zod `.strict()`) reject every call that
|
|
94
|
+
* carries `i`. The MCP boundary is the authoritative guard so callers don't
|
|
95
|
+
* have to pre-strip.
|
|
96
|
+
*
|
|
97
|
+
* Leaves `i` in place when the server's own `inputSchema.properties` declares
|
|
98
|
+
* it, so a server that legitimately uses `i` as a parameter is unaffected.
|
|
99
|
+
*/
|
|
100
|
+
function stripHarnessIntent(args: MCPToolArgs, inputSchema: MCPToolDefinition["inputSchema"]): MCPToolArgs {
|
|
101
|
+
if (!Object.hasOwn(args, INTENT_FIELD)) return args;
|
|
102
|
+
if (inputSchema.properties && Object.hasOwn(inputSchema.properties, INTENT_FIELD)) return args;
|
|
103
|
+
const { [INTENT_FIELD]: _intent, ...rest } = args;
|
|
104
|
+
return rest;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Normalize raw tool params into the outbound `tools/call` arguments: strip
|
|
109
|
+
* the harness intent field, then drop optional empty placeholders the server
|
|
110
|
+
* declares but doesn't require.
|
|
111
|
+
*/
|
|
112
|
+
function prepareOutboundArgs(params: unknown, inputSchema: MCPToolDefinition["inputSchema"]): MCPToolArgs {
|
|
113
|
+
return omitUnusedOptionalArgs(stripHarnessIntent(normalizeToolArgs(params), inputSchema), inputSchema);
|
|
114
|
+
}
|
|
115
|
+
|
|
86
116
|
/** Details included in MCP tool results for rendering */
|
|
87
117
|
export interface MCPToolDetails {
|
|
88
118
|
/** Server name */
|
|
@@ -286,7 +316,7 @@ export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
286
316
|
signal?: AbortSignal,
|
|
287
317
|
): Promise<CustomToolResult<MCPToolDetails>> {
|
|
288
318
|
throwIfAborted(signal);
|
|
289
|
-
const args =
|
|
319
|
+
const args = prepareOutboundArgs(params, this.tool.inputSchema);
|
|
290
320
|
const provider = this.connection._source?.provider;
|
|
291
321
|
const providerName = this.connection._source?.providerName;
|
|
292
322
|
|
|
@@ -385,7 +415,7 @@ export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
|
385
415
|
signal?: AbortSignal,
|
|
386
416
|
): Promise<CustomToolResult<MCPToolDetails>> {
|
|
387
417
|
throwIfAborted(signal);
|
|
388
|
-
const args =
|
|
418
|
+
const args = prepareOutboundArgs(params, this.tool.inputSchema);
|
|
389
419
|
const provider = this.#fallbackProvider;
|
|
390
420
|
const providerName = this.#fallbackProviderName;
|
|
391
421
|
|
|
@@ -3,19 +3,35 @@ import { describe, expect, it } from "bun:test";
|
|
|
3
3
|
import { resolveStdioSpawnCommand } from "./stdio";
|
|
4
4
|
|
|
5
5
|
describe("resolveStdioSpawnCommand", () => {
|
|
6
|
-
it("hides
|
|
6
|
+
it("hides Windows executable MCP servers when the host has no console", async () => {
|
|
7
|
+
// Hidden so a console-app child does not allocate a visible window when
|
|
8
|
+
// OMP is launched without a terminal console (#3536).
|
|
7
9
|
await expect(
|
|
8
10
|
resolveStdioSpawnCommand(
|
|
9
11
|
{ command: "server.exe", args: ["--stdio"] },
|
|
10
|
-
{ cwd: process.cwd(), env: {}, platform: "win32" },
|
|
12
|
+
{ cwd: process.cwd(), env: {}, platform: "win32", hostHasInheritableConsole: false },
|
|
11
13
|
),
|
|
12
14
|
).resolves.toEqual({
|
|
13
15
|
cmd: ["server.exe", "--stdio"],
|
|
14
16
|
windowsHide: true,
|
|
17
|
+
detached: false,
|
|
15
18
|
});
|
|
16
19
|
});
|
|
17
20
|
|
|
18
|
-
it("
|
|
21
|
+
it("inherits an attached Windows console instead of forcing CREATE_NO_WINDOW", async () => {
|
|
22
|
+
await expect(
|
|
23
|
+
resolveStdioSpawnCommand(
|
|
24
|
+
{ command: "server.exe", args: ["--stdio"] },
|
|
25
|
+
{ cwd: process.cwd(), env: {}, platform: "win32", hostHasInheritableConsole: true },
|
|
26
|
+
),
|
|
27
|
+
).resolves.toEqual({
|
|
28
|
+
cmd: ["server.exe", "--stdio"],
|
|
29
|
+
windowsHide: false,
|
|
30
|
+
detached: false,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("detaches off-Windows MCP servers so terminal job-control signals cannot stop them", async () => {
|
|
19
35
|
await expect(
|
|
20
36
|
resolveStdioSpawnCommand(
|
|
21
37
|
{ command: "server.exe", args: ["--stdio"] },
|
|
@@ -23,6 +39,7 @@ describe("resolveStdioSpawnCommand", () => {
|
|
|
23
39
|
),
|
|
24
40
|
).resolves.toEqual({
|
|
25
41
|
cmd: ["server.exe", "--stdio"],
|
|
42
|
+
detached: true,
|
|
26
43
|
});
|
|
27
44
|
});
|
|
28
45
|
});
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
|
|
8
8
|
import * as fs from "node:fs/promises";
|
|
9
9
|
import * as path from "node:path";
|
|
10
|
-
|
|
11
10
|
import { getProjectDir, readJsonl, Snowflake } from "@oh-my-pi/pi-utils";
|
|
12
11
|
import { type Subprocess, spawn } from "bun";
|
|
12
|
+
import { hostHasInheritableConsole } from "../../eval/py/spawn-options";
|
|
13
13
|
import type {
|
|
14
14
|
JsonRpcError,
|
|
15
15
|
JsonRpcMessage,
|
|
@@ -22,16 +22,40 @@ import type {
|
|
|
22
22
|
import { toJsonRpcError } from "../../mcp/types";
|
|
23
23
|
import { isMCPTimeoutEnabled, resolveMCPTimeoutMs } from "../timeout";
|
|
24
24
|
|
|
25
|
-
/** Subprocess argv for
|
|
25
|
+
/** Subprocess argv and platform-derived spawn flags for an MCP stdio server. */
|
|
26
26
|
export interface StdioSpawnCommand {
|
|
27
27
|
cmd: string[];
|
|
28
|
+
/**
|
|
29
|
+
* Hide the Windows console window for the direct child.
|
|
30
|
+
*
|
|
31
|
+
* Windows uses this only when the OMP host has no console to share. When
|
|
32
|
+
* the host is running inside a terminal, `windowsHide: true` maps to
|
|
33
|
+
* `CREATE_NO_WINDOW`, which strips that inheritable console from hidden
|
|
34
|
+
* `cmd.exe` / PowerShell wrapper chains. Their console grandchildren then
|
|
35
|
+
* allocate fresh visible conhost windows during startup or reconnects
|
|
36
|
+
* (#3567).
|
|
37
|
+
*/
|
|
28
38
|
windowsHide?: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Run the subprocess in its own session.
|
|
41
|
+
*
|
|
42
|
+
* POSIX: `true`. Detach → `setsid`, so the MCP process tree has no
|
|
43
|
+
* controlling terminal and terminal job-control signals (Ctrl+Z SIGTSTP,
|
|
44
|
+
* background-read SIGTTIN) cannot stop stdio servers such as
|
|
45
|
+
* `chrome-devtools-mcp` and leave our read loop blocked on silent pipes.
|
|
46
|
+
*
|
|
47
|
+
* Windows: `false`. There is no SIGTSTP/SIGTTIN to escape, and Windows
|
|
48
|
+
* wrapper chains must stay in the OMP console session so nested console
|
|
49
|
+
* grandchildren keep stdout routed through our pipe (#3544).
|
|
50
|
+
*/
|
|
51
|
+
detached: boolean;
|
|
29
52
|
}
|
|
30
53
|
|
|
31
54
|
/** Inputs used to resolve platform-specific stdio spawn behavior. */
|
|
32
55
|
export interface ResolveStdioSpawnOptions {
|
|
33
56
|
cwd: string;
|
|
34
57
|
env: Record<string, string | undefined>;
|
|
58
|
+
hostHasInheritableConsole?: boolean;
|
|
35
59
|
platform?: NodeJS.Platform;
|
|
36
60
|
}
|
|
37
61
|
|
|
@@ -142,6 +166,7 @@ async function resolveWindowsNpmShimCommand(
|
|
|
142
166
|
command: string,
|
|
143
167
|
args: readonly string[],
|
|
144
168
|
cwd: string,
|
|
169
|
+
windowsHide: boolean,
|
|
145
170
|
): Promise<StdioSpawnCommand | null> {
|
|
146
171
|
if (!isWindowsBatchCommand(command)) return null;
|
|
147
172
|
if (!hasPathSegment(command)) return null;
|
|
@@ -176,7 +201,8 @@ async function resolveWindowsNpmShimCommand(
|
|
|
176
201
|
const nodeCommand = (await fileExists(siblingNode)) ? siblingNode : "node";
|
|
177
202
|
return {
|
|
178
203
|
cmd: [nodeCommand, target, ...args],
|
|
179
|
-
windowsHide
|
|
204
|
+
windowsHide,
|
|
205
|
+
detached: false,
|
|
180
206
|
};
|
|
181
207
|
}
|
|
182
208
|
|
|
@@ -229,25 +255,28 @@ export async function resolveStdioSpawnCommand(
|
|
|
229
255
|
options: ResolveStdioSpawnOptions,
|
|
230
256
|
): Promise<StdioSpawnCommand> {
|
|
231
257
|
const args = config.args ?? [];
|
|
232
|
-
if (options.platform !== "win32") return { cmd: [config.command, ...args] };
|
|
258
|
+
if (options.platform !== "win32") return { cmd: [config.command, ...args], detached: true };
|
|
233
259
|
|
|
260
|
+
const windowsHide = options.hostHasInheritableConsole === undefined ? true : !options.hostHasInheritableConsole;
|
|
234
261
|
const resolved = await resolveWindowsCommandPath(config.command, options.cwd, options.env);
|
|
235
262
|
const resolvedCommand = resolved ?? config.command;
|
|
236
|
-
const npmShimCommand = await resolveWindowsNpmShimCommand(resolvedCommand, args, options.cwd);
|
|
263
|
+
const npmShimCommand = await resolveWindowsNpmShimCommand(resolvedCommand, args, options.cwd, windowsHide);
|
|
237
264
|
if (npmShimCommand) return npmShimCommand;
|
|
238
265
|
|
|
239
266
|
// Direct-spawn only when we resolved to a concrete file AND its extension
|
|
240
267
|
// is not a batch script. Everything else (resolved .cmd/.bat, or an
|
|
241
268
|
// unresolved extensionless command) goes through cmd.exe so PATHEXT runs.
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
|
|
269
|
+
// Windows stdio servers stay attached so wrapper grandchildren inherit the
|
|
270
|
+
// same console session. Only hide the child when OMP itself has no console
|
|
271
|
+
// to share; CREATE_NO_WINDOW breaks console inheritance for nested wrappers.
|
|
272
|
+
const detached = false;
|
|
245
273
|
const needsCmdExe = resolved === null || isWindowsBatchCommand(resolvedCommand);
|
|
246
|
-
if (!needsCmdExe) return { cmd: [resolvedCommand, ...args], windowsHide };
|
|
274
|
+
if (!needsCmdExe) return { cmd: [resolvedCommand, ...args], windowsHide, detached };
|
|
247
275
|
|
|
248
276
|
return {
|
|
249
277
|
cmd: [resolveComSpec(options.env), "/d", "/s", "/c", buildCmdExeCommand(resolvedCommand, args)],
|
|
250
278
|
windowsHide,
|
|
279
|
+
detached,
|
|
251
280
|
};
|
|
252
281
|
}
|
|
253
282
|
|
|
@@ -341,12 +370,14 @@ export class StdioTransport implements MCPTransport {
|
|
|
341
370
|
cwd,
|
|
342
371
|
env,
|
|
343
372
|
platform: process.platform,
|
|
373
|
+
hostHasInheritableConsole: hostHasInheritableConsole(),
|
|
344
374
|
});
|
|
345
375
|
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
// SIGTSTP,
|
|
349
|
-
//
|
|
376
|
+
// Platform-derived session and console-window handling come from
|
|
377
|
+
// `resolveStdioSpawnCommand`: POSIX detaches into its own session to
|
|
378
|
+
// escape terminal job-control signals (SIGTSTP, SIGTTIN); Windows stays
|
|
379
|
+
// attached, and only hides the child when the host has no console to
|
|
380
|
+
// share. See `StdioSpawnCommand`.
|
|
350
381
|
this.#process = spawn({
|
|
351
382
|
cmd: spawnCommand.cmd,
|
|
352
383
|
cwd,
|
|
@@ -355,7 +386,7 @@ export class StdioTransport implements MCPTransport {
|
|
|
355
386
|
stdout: "pipe",
|
|
356
387
|
stderr: "pipe",
|
|
357
388
|
windowsHide: spawnCommand.windowsHide,
|
|
358
|
-
detached:
|
|
389
|
+
detached: spawnCommand.detached,
|
|
359
390
|
});
|
|
360
391
|
|
|
361
392
|
this.#connected = true;
|
package/src/memories/index.ts
CHANGED
|
@@ -559,7 +559,7 @@ function shouldPersistResponseItemForMemories(message: AgentMessage): boolean {
|
|
|
559
559
|
}
|
|
560
560
|
if (role !== "toolResult") return false;
|
|
561
561
|
const toolName = (message as { toolName?: string }).toolName;
|
|
562
|
-
if (toolName === "bash" || toolName === "eval" || toolName === "read" || toolName === "
|
|
562
|
+
if (toolName === "bash" || toolName === "eval" || toolName === "read" || toolName === "grep") {
|
|
563
563
|
const text = extractMessageText(message);
|
|
564
564
|
return text.length > 0 && text.length <= 32_000;
|
|
565
565
|
}
|
|
@@ -143,8 +143,8 @@ export function mapToolKind(toolName: string): ToolKind {
|
|
|
143
143
|
case "exec":
|
|
144
144
|
case "eval":
|
|
145
145
|
return "execute";
|
|
146
|
-
case "
|
|
147
|
-
case "
|
|
146
|
+
case "grep":
|
|
147
|
+
case "glob":
|
|
148
148
|
case "ast_grep":
|
|
149
149
|
return "search";
|
|
150
150
|
case "web_search":
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as fsp from "node:fs/promises";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { Settings } from "../../../config/settings";
|
|
7
|
+
import { getThemeByName, setThemeInstance, type Theme } from "../../theme/theme";
|
|
8
|
+
import { MoveOverlay, type MoveOverlayResult, resolveExistingDirectory, resolveMovePath } from "../move-overlay";
|
|
9
|
+
|
|
10
|
+
// Strip SGR colors so assertions see visible text only.
|
|
11
|
+
const strip = (lines: readonly string[]): string => lines.join("\n").replace(/\x1b\[[0-9;]*m/g, "");
|
|
12
|
+
|
|
13
|
+
describe("resolveMovePath", () => {
|
|
14
|
+
it("expands ~ to homedir", () => {
|
|
15
|
+
expect(resolveMovePath("~", "/anywhere")).toBe(os.homedir());
|
|
16
|
+
});
|
|
17
|
+
it("expands ~/sub to homedir/sub", () => {
|
|
18
|
+
expect(resolveMovePath("~/foo", "/anywhere")).toBe(path.join(os.homedir(), "foo"));
|
|
19
|
+
});
|
|
20
|
+
it("resolves relative paths against cwd", () => {
|
|
21
|
+
expect(resolveMovePath("foo/bar", "/parent")).toBe(path.resolve("/parent", "foo/bar"));
|
|
22
|
+
});
|
|
23
|
+
it("passes absolute paths through (normalized)", () => {
|
|
24
|
+
expect(resolveMovePath("/abs/path", "/anywhere")).toBe(path.normalize("/abs/path"));
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("resolveExistingDirectory", () => {
|
|
29
|
+
let tmp: string;
|
|
30
|
+
|
|
31
|
+
beforeEach(async () => {
|
|
32
|
+
tmp = await fsp.mkdtemp(path.join(os.tmpdir(), "omp-move-resolve-"));
|
|
33
|
+
});
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
await fsp.rm(tmp, { recursive: true, force: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns the resolved path for an existing directory", () => {
|
|
39
|
+
const sub = path.join(tmp, "sub");
|
|
40
|
+
fs.mkdirSync(sub);
|
|
41
|
+
expect(resolveExistingDirectory(sub, "/anywhere")).toBe(path.resolve(sub));
|
|
42
|
+
});
|
|
43
|
+
it("returns null for a non-existent path", () => {
|
|
44
|
+
expect(resolveExistingDirectory(path.join(tmp, "nope"), "/anywhere")).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
it("returns null for a file (not a directory)", () => {
|
|
47
|
+
const file = path.join(tmp, "file.txt");
|
|
48
|
+
fs.writeFileSync(file, "x");
|
|
49
|
+
expect(resolveExistingDirectory(file, "/anywhere")).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe("MoveOverlay", () => {
|
|
54
|
+
let tmp: string;
|
|
55
|
+
let cwd: string;
|
|
56
|
+
let uiTheme: Theme;
|
|
57
|
+
|
|
58
|
+
beforeAll(async () => {
|
|
59
|
+
await Settings.init({ inMemory: true });
|
|
60
|
+
const loaded = await getThemeByName("dark");
|
|
61
|
+
if (!loaded) throw new Error("theme unavailable");
|
|
62
|
+
uiTheme = loaded;
|
|
63
|
+
setThemeInstance(uiTheme);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
tmp = await fsp.mkdtemp(path.join(os.tmpdir(), "omp-move-overlay-"));
|
|
68
|
+
cwd = tmp;
|
|
69
|
+
fs.mkdirSync(path.join(tmp, "alpha"));
|
|
70
|
+
fs.mkdirSync(path.join(tmp, "beta"));
|
|
71
|
+
fs.mkdirSync(path.join(tmp, ".hidden"));
|
|
72
|
+
fs.writeFileSync(path.join(tmp, "file.txt"), "x");
|
|
73
|
+
});
|
|
74
|
+
afterEach(async () => {
|
|
75
|
+
await fsp.rm(tmp, { recursive: true, force: true });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("renders a box with a title and input prompt", () => {
|
|
79
|
+
const overlay = new MoveOverlay(cwd, () => {});
|
|
80
|
+
const text = strip(overlay.render(80));
|
|
81
|
+
expect(text).toContain("Move to directory");
|
|
82
|
+
expect(text).toContain("Path:");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("lists child directories (excluding hidden and files) on empty input", () => {
|
|
86
|
+
const overlay = new MoveOverlay(cwd, () => {});
|
|
87
|
+
const text = strip(overlay.render(80));
|
|
88
|
+
expect(text).toContain("alpha/");
|
|
89
|
+
expect(text).toContain("beta/");
|
|
90
|
+
expect(text).not.toContain(".hidden/");
|
|
91
|
+
expect(text).not.toContain("file.txt");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("filters results as the user types", () => {
|
|
95
|
+
const overlay = new MoveOverlay(cwd, () => {});
|
|
96
|
+
overlay.handleInput("a");
|
|
97
|
+
overlay.handleInput("l");
|
|
98
|
+
const text = strip(overlay.render(80));
|
|
99
|
+
expect(text).toContain("alpha/");
|
|
100
|
+
expect(text).not.toContain("beta/");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("shows dot directories after a dot prefix is typed", () => {
|
|
104
|
+
const overlay = new MoveOverlay(cwd, () => {});
|
|
105
|
+
overlay.handleInput(".");
|
|
106
|
+
const text = strip(overlay.render(80));
|
|
107
|
+
expect(text).toContain(".hidden/");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("accepts bracketed paste and multi-byte input while filtering controls", () => {
|
|
111
|
+
let result: MoveOverlayResult | undefined;
|
|
112
|
+
const overlay = new MoveOverlay(cwd, r => {
|
|
113
|
+
result = r;
|
|
114
|
+
});
|
|
115
|
+
overlay.handleInput("\x1b[200~new\nø\x1b[201~");
|
|
116
|
+
overlay.handleInput("\r");
|
|
117
|
+
expect(result).toBeDefined();
|
|
118
|
+
expect(result!.directory).toBe("newø");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("calls done with undefined on Escape", () => {
|
|
122
|
+
let result: MoveOverlayResult | undefined = "sentinel" as unknown as MoveOverlayResult;
|
|
123
|
+
const overlay = new MoveOverlay(cwd, r => {
|
|
124
|
+
result = r;
|
|
125
|
+
});
|
|
126
|
+
overlay.handleInput("\x1b");
|
|
127
|
+
expect(result).toBeUndefined();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("calls done with the highlighted directory on Enter", () => {
|
|
131
|
+
let result: MoveOverlayResult | undefined;
|
|
132
|
+
const overlay = new MoveOverlay(cwd, r => {
|
|
133
|
+
result = r;
|
|
134
|
+
});
|
|
135
|
+
// First result should be "alpha/" (sorted alphabetically).
|
|
136
|
+
overlay.handleInput("\r");
|
|
137
|
+
expect(result).toBeDefined();
|
|
138
|
+
expect(result!.directory).toBe(path.join(cwd, "alpha"));
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("calls done with the typed path on Enter when no results match", () => {
|
|
142
|
+
let result: MoveOverlayResult | undefined;
|
|
143
|
+
const overlay = new MoveOverlay(cwd, r => {
|
|
144
|
+
result = r;
|
|
145
|
+
});
|
|
146
|
+
// Type a path that won't match any directory in cwd.
|
|
147
|
+
overlay.handleInput("z");
|
|
148
|
+
overlay.handleInput("z");
|
|
149
|
+
overlay.handleInput("\r");
|
|
150
|
+
expect(result).toBeDefined();
|
|
151
|
+
expect(result!.directory).toBe("zz");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("Tab accepts the highlighted suggestion into the input", () => {
|
|
155
|
+
let result: MoveOverlayResult | undefined;
|
|
156
|
+
const overlay = new MoveOverlay(cwd, r => {
|
|
157
|
+
result = r;
|
|
158
|
+
});
|
|
159
|
+
overlay.handleInput("\t");
|
|
160
|
+
// After tab, the input should be the full path of the first result.
|
|
161
|
+
// Press Enter to confirm — the result should be the alpha directory.
|
|
162
|
+
overlay.handleInput("\r");
|
|
163
|
+
expect(result).toBeDefined();
|
|
164
|
+
expect(result!.directory).toBe(path.join(cwd, "alpha"));
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -425,6 +425,9 @@ export class AgentHubOverlayComponent extends Container {
|
|
|
425
425
|
const parts: string[] = [statusBadge(ref.status), theme.bold(replaceTabs(ref.id))];
|
|
426
426
|
parts.push(theme.fg("dim", replaceTabs(ref.displayName)));
|
|
427
427
|
parts.push(theme.fg("dim", ref.parentId ? `${ref.kind} · of ${ref.parentId}` : ref.kind));
|
|
428
|
+
if (ref.kind === "advisor") {
|
|
429
|
+
parts.push(theme.fg("warning", "read-only"));
|
|
430
|
+
}
|
|
428
431
|
const observed = this.#observableFor(ref.id);
|
|
429
432
|
const task = observed?.description ?? observed?.progress?.task;
|
|
430
433
|
if (task) {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import * as fs from "node:fs";
|
|
17
17
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
18
|
-
import { type Component, Editor, matchesKey,
|
|
18
|
+
import { type Component, Editor, matchesKey, routeSgrMouseInput, ScrollView, type TUI } from "@oh-my-pi/pi-tui";
|
|
19
19
|
import { formatDuration, formatNumber, logger } from "@oh-my-pi/pi-utils";
|
|
20
20
|
import type { KeyId } from "../../config/keybindings";
|
|
21
21
|
import type { MessageRenderer } from "../../extensibility/extensions/types";
|
|
@@ -413,12 +413,14 @@ export class AgentTranscriptViewer implements Component {
|
|
|
413
413
|
|
|
414
414
|
handleInput(data: string): void {
|
|
415
415
|
if (data.startsWith("\x1b[<")) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
416
|
+
routeSgrMouseInput(data, event => {
|
|
417
|
+
if (event.wheel !== null) {
|
|
418
|
+
this.#scrollView.scroll(event.wheel * 3);
|
|
419
|
+
this.#syncFollow();
|
|
420
|
+
this.deps.requestRender();
|
|
421
|
+
}
|
|
422
|
+
return true;
|
|
423
|
+
});
|
|
422
424
|
return;
|
|
423
425
|
}
|
|
424
426
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
import type { ImageContent } from "@oh-my-pi/pi-ai";
|
|
3
3
|
import { addKeyAliases, canonicalKeyId, Editor, type KeyId, parseKey, parseKittySequence } from "@oh-my-pi/pi-tui";
|
|
4
|
+
import { BracketedPasteHandler } from "@oh-my-pi/pi-tui/bracketed-paste";
|
|
4
5
|
import type { AppKeybinding } from "../../config/keybindings";
|
|
5
6
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
6
7
|
import { imageReferenceHyperlink, PLACEHOLDER_REGEX, renderPlaceholders } from "../image-references";
|
|
@@ -229,6 +230,17 @@ export function extractBracketedImagePastePaths(data: string): string[] | undefi
|
|
|
229
230
|
return paths?.every(isImagePath) ? paths : undefined;
|
|
230
231
|
}
|
|
231
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Same shape as {@link extractBracketedImagePastePaths} but operates on a
|
|
235
|
+
* payload that has already been stripped of the `\x1b[200~` / `\x1b[201~`
|
|
236
|
+
* markers — used by the assembled-paste router in {@link CustomEditor.handleInput}
|
|
237
|
+
* so split bracketed pastes get the same image-path detection as single-chunk ones.
|
|
238
|
+
*/
|
|
239
|
+
export function extractImagePastePathsFromText(text: string): string[] | undefined {
|
|
240
|
+
const paths = extractPastePathsFromText(text);
|
|
241
|
+
return paths?.every(isImagePath) ? paths : undefined;
|
|
242
|
+
}
|
|
243
|
+
|
|
232
244
|
export function extractBracketedImagePastePath(data: string): string | undefined {
|
|
233
245
|
const paths = extractBracketedImagePastePaths(data);
|
|
234
246
|
return paths?.length === 1 ? paths[0] : undefined;
|
|
@@ -422,6 +434,19 @@ export class CustomEditor extends Editor {
|
|
|
422
434
|
/** Custom key handlers from extensions and non-built-in app actions. */
|
|
423
435
|
#customKeyHandlers = new Map<KeyId, () => void>();
|
|
424
436
|
#customMatchKeys = new Map<string, () => void>();
|
|
437
|
+
/** Bracketed-paste assembler that runs ahead of the inherited handler so terminals which
|
|
438
|
+
* deliver `\x1b[200~` and `\x1b[201~` in separate stdin chunks still resolve to a single
|
|
439
|
+
* assembled payload here; the empty-paste / image-path branches must see the full content,
|
|
440
|
+
* not the raw single-chunk byte sequence. */
|
|
441
|
+
#pasteHandler = new BracketedPasteHandler();
|
|
442
|
+
/** Number of async pastes (clipboard-image reads / image-path attachments) currently in flight.
|
|
443
|
+
* While > 0, `handleInput` queues subsequent keystrokes into {@link #pendingInput} instead of
|
|
444
|
+
* dispatching them so a trailing `Enter` after `Cmd+V` can't submit before the image lands on
|
|
445
|
+
* `pendingImages` (Codex PR #3602 review). */
|
|
446
|
+
#pasteInFlight = 0;
|
|
447
|
+
/** Input chunks deferred behind an in-flight paste, drained in FIFO order once the paste
|
|
448
|
+
* count returns to zero. */
|
|
449
|
+
#pendingInput: string[] = [];
|
|
425
450
|
/** Spaces actually inserted in the current run; tracked back out when a hold is recognized. */
|
|
426
451
|
#spaceRunInserted = 0;
|
|
427
452
|
/** Consecutive "mechanical" deltas (fast + steady); a sustained run of these confirms a held bar. */
|
|
@@ -575,7 +600,34 @@ export class CustomEditor extends Editor {
|
|
|
575
600
|
this.onSpaceHoldEnd?.();
|
|
576
601
|
}
|
|
577
602
|
|
|
603
|
+
/** Decrement {@link #pasteInFlight} once an async paste settles and, when the count returns
|
|
604
|
+
* to zero, drain {@link #pendingInput} through `handleInput` so requeueing still works if a
|
|
605
|
+
* drained chunk triggers another async paste. Bound member so it can be passed straight to
|
|
606
|
+
* `Promise.then(callback, callback)`. */
|
|
607
|
+
#onPasteSettled = (): void => {
|
|
608
|
+
this.#pasteInFlight--;
|
|
609
|
+
if (this.#pasteInFlight > 0) return;
|
|
610
|
+
const drained = this.#pendingInput.splice(0);
|
|
611
|
+
for (const chunk of drained) this.handleInput(chunk);
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
/** Track `promise` as an in-flight paste so subsequent `handleInput` calls queue behind it,
|
|
615
|
+
* then drain the queue once it settles. Codex PR #3602 review: without this, a trailing
|
|
616
|
+
* keystroke (Enter most painfully) in the same stdin read processes synchronously while the
|
|
617
|
+
* clipboard read is still pending — submit fires with the text but `pendingImages` is still
|
|
618
|
+
* empty and the image lands on the *next* draft instead. */
|
|
619
|
+
#trackAsyncPaste(promise: Promise<unknown>): void {
|
|
620
|
+
this.#pasteInFlight++;
|
|
621
|
+
void promise.then(this.#onPasteSettled, this.#onPasteSettled);
|
|
622
|
+
}
|
|
623
|
+
|
|
578
624
|
handleInput(data: string): void {
|
|
625
|
+
// Serialize behind any in-flight async paste so a trailing Enter / follow-up key can't
|
|
626
|
+
// submit before the clipboard image reaches `pendingImages` (Codex PR #3602 review).
|
|
627
|
+
if (this.#pasteInFlight > 0) {
|
|
628
|
+
this.#pendingInput.push(data);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
579
631
|
const kittyParsed = parseKittySequence(data);
|
|
580
632
|
if (kittyParsed && (kittyParsed.modifier & 64) !== 0 && this.onCapsLock) {
|
|
581
633
|
// Caps Lock is modifier bit 64
|
|
@@ -583,11 +635,44 @@ export class CustomEditor extends Editor {
|
|
|
583
635
|
return;
|
|
584
636
|
}
|
|
585
637
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
638
|
+
// Bracketed-paste assembly. Some terminals fragment the start marker,
|
|
639
|
+
// the payload, and the end marker across separate stdin chunks
|
|
640
|
+
// (Windows Terminal under heavy load, certain SSH muxes, …); the
|
|
641
|
+
// inherited handler then sees a zero-length payload and silently
|
|
642
|
+
// drops it through the normal text-insert path. Running our own
|
|
643
|
+
// `BracketedPasteHandler` ahead of `super.handleInput` lets us route
|
|
644
|
+
// the assembled content regardless of chunk boundaries:
|
|
645
|
+
// - empty payload → `onPasteImage` (#3601: `Cmd+V`/`Ctrl+V` on an
|
|
646
|
+
// image-only macOS pasteboard the terminal stripped to `""` first);
|
|
647
|
+
// - explicit image-file paths → `onPasteImagePath` (#3506);
|
|
648
|
+
// - anything else → the base editor's `pasteText` so `[Paste #N]`
|
|
649
|
+
// markers, autocomplete, and undo state stay intact.
|
|
650
|
+
const paste = this.#pasteHandler.process(data);
|
|
651
|
+
if (paste.handled) {
|
|
652
|
+
if (paste.pasteContent === undefined) return; // still buffering — wait for end marker
|
|
653
|
+
const content = paste.pasteContent;
|
|
654
|
+
const remaining = paste.remaining;
|
|
655
|
+
// Queue any trailing bytes from the same read (typically a follow-up keystroke such as
|
|
656
|
+
// Enter that the user pressed right after Cmd+V) so they only fire *after* the paste
|
|
657
|
+
// completes — fixes the race where submit runs against an empty `pendingImages`.
|
|
658
|
+
if (remaining.length > 0) this.#pendingInput.push(remaining);
|
|
659
|
+
if (content.length === 0 && this.onPasteImage) {
|
|
660
|
+
this.#trackAsyncPaste(Promise.resolve(this.onPasteImage()));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
const imagePaths = extractImagePastePathsFromText(content);
|
|
664
|
+
if (imagePaths && this.onPasteImagePath) {
|
|
665
|
+
this.#trackAsyncPaste(
|
|
666
|
+
(async () => {
|
|
667
|
+
for (const p of imagePaths) await this.onPasteImagePath?.(p);
|
|
668
|
+
})(),
|
|
669
|
+
);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
this.pasteText(content);
|
|
673
|
+
// No async paste was started; drain the queued trailing bytes ourselves.
|
|
674
|
+
const drained = this.#pendingInput.splice(0);
|
|
675
|
+
for (const chunk of drained) this.handleInput(chunk);
|
|
591
676
|
return;
|
|
592
677
|
}
|
|
593
678
|
|