@oh-my-pi/pi-coding-agent 16.1.23 → 16.2.1
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 +82 -0
- package/dist/cli.js +3785 -3695
- 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 +13 -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/collab/display-name.d.ts +3 -0
- package/dist/types/collab/host.d.ts +0 -2
- 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 +91 -21
- package/dist/types/config/settings.d.ts +2 -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/modes/acp/acp-event-mapper.d.ts +1 -0
- 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/theme.d.ts +1 -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 +8 -1
- package/dist/types/session/blob-store.d.ts +4 -0
- 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 +3 -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/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/build-binary.ts +9 -9
- package/scripts/bundle-dist.ts +2 -2
- package/src/advisor/__tests__/advisor.test.ts +61 -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 +27 -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/usage-cli.ts +20 -1
- package/src/cli/web-search-cli.ts +1 -1
- 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/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 +85 -27
- package/src/config/settings.ts +172 -1
- package/src/cursor.ts +1 -1
- package/src/dap/config.ts +152 -8
- package/src/dap/session.ts +1 -1
- package/src/discovery/claude-plugins.ts +3 -2
- 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 +38 -2
- 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/agent-bridge.ts +26 -3
- 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 +8 -3
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +25 -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/mcp/tool-bridge.ts +32 -2
- package/src/memories/index.ts +1 -1
- package/src/modes/acp/acp-agent.ts +14 -1
- package/src/modes/acp/acp-event-mapper.ts +91 -27
- 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 +68 -2
- package/src/modes/controllers/selector-controller.ts +5 -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 +116 -50
- 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/theme.ts +9 -1
- 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/context-files.md +8 -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 +3 -3
- package/src/prompts/system/project-prompt.md +2 -2
- package/src/prompts/system/system-prompt.md +6 -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 +65 -25
- package/src/session/agent-session.ts +519 -132
- package/src/session/blob-store.ts +24 -0
- package/src/session/session-history-format.ts +2 -2
- package/src/session/session-manager.ts +46 -0
- package/src/slash-commands/builtin-registry.ts +138 -20
- 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 +56 -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/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/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/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/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
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
} from "@agentclientprotocol/sdk";
|
|
43
43
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
44
44
|
import type { AssistantMessage, Model } from "@oh-my-pi/pi-ai";
|
|
45
|
-
import { isEnoent, logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
45
|
+
import { getBlobsDir, isEnoent, logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
46
46
|
import { disableProvider, enableProvider, reset as resetCapabilities } from "../../capability";
|
|
47
47
|
import { Settings } from "../../config/settings";
|
|
48
48
|
import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
|
|
@@ -62,6 +62,7 @@ import { loadAllExtensions } from "../../modes/components/extensions/state-manag
|
|
|
62
62
|
import { theme } from "../../modes/theme/theme";
|
|
63
63
|
import { type PlanApprovalDetails, resolveApprovedPlan } from "../../plan-mode/approved-plan";
|
|
64
64
|
import type { AgentSession, AgentSessionEvent } from "../../session/agent-session";
|
|
65
|
+
import { BlobStore, resolveImageDataSync } from "../../session/blob-store";
|
|
65
66
|
import { isSilentAbort, SKILL_PROMPT_MESSAGE_TYPE, USER_INTERRUPT_LABEL } from "../../session/messages";
|
|
66
67
|
import type { UsageStatistics } from "../../session/session-entries";
|
|
67
68
|
import type { SessionInfo as StoredSessionInfo } from "../../session/session-listing";
|
|
@@ -445,6 +446,7 @@ export class AcpAgent implements Agent {
|
|
|
445
446
|
#cleanupRegistered = false;
|
|
446
447
|
#clientCapabilities: ClientCapabilities | undefined;
|
|
447
448
|
#cancelCleanupTimeoutMs = ACP_CANCEL_CLEANUP_TIMEOUT_MS;
|
|
449
|
+
#blobs = new BlobStore(getBlobsDir());
|
|
448
450
|
|
|
449
451
|
constructor(connection: AgentSideConnection, createSession: CreateAcpSession, initialSession?: AgentSession) {
|
|
450
452
|
this.#connection = connection;
|
|
@@ -1187,11 +1189,21 @@ export class AcpAgent implements Agent {
|
|
|
1187
1189
|
}
|
|
1188
1190
|
|
|
1189
1191
|
this.#prepareLiveAssistantMessage(record, event);
|
|
1192
|
+
const imageDataCache = new Map<string, string>();
|
|
1193
|
+
const resolveImageDataForAcp = (data: string, mimeType: string | undefined): string => {
|
|
1194
|
+
const key = `${mimeType ?? ""}\u0000${data}`;
|
|
1195
|
+
const cached = imageDataCache.get(key);
|
|
1196
|
+
if (cached !== undefined) return cached;
|
|
1197
|
+
const resolved = resolveImageDataSync(this.#blobs, data);
|
|
1198
|
+
imageDataCache.set(key, resolved);
|
|
1199
|
+
return resolved;
|
|
1200
|
+
};
|
|
1190
1201
|
for (const notification of mapAgentSessionEventToAcpSessionUpdates(event, record.session.sessionId, {
|
|
1191
1202
|
getMessageId: message => this.#getLiveMessageId(record, message),
|
|
1192
1203
|
getMessageProgress: message => this.#getLiveMessageProgress(record, message),
|
|
1193
1204
|
getToolArgs: toolCallId => record.toolArgsById.get(toolCallId),
|
|
1194
1205
|
cwd: record.session.sessionManager.getCwd(),
|
|
1206
|
+
resolveImageData: resolveImageDataForAcp,
|
|
1195
1207
|
})) {
|
|
1196
1208
|
await this.#connection.sessionUpdate(notification);
|
|
1197
1209
|
}
|
|
@@ -2052,6 +2064,7 @@ export class AcpAgent implements Agent {
|
|
|
2052
2064
|
const notifications = mapAgentSessionEventToAcpSessionUpdates(endEvent, sessionId, {
|
|
2053
2065
|
cwd,
|
|
2054
2066
|
getToolArgs: toolCallId => (toolCallId === message.toolCallId ? options.toolArgs : undefined),
|
|
2067
|
+
resolveImageData: (data, _mimeType) => resolveImageDataSync(this.#blobs, data),
|
|
2055
2068
|
});
|
|
2056
2069
|
if (options.includeStart === false) {
|
|
2057
2070
|
return notifications;
|
|
@@ -20,6 +20,7 @@ interface AcpEventMapperOptions {
|
|
|
20
20
|
getMessageId?: (message: unknown) => string | undefined;
|
|
21
21
|
getMessageProgress?: (message: unknown) => MessageProgress | undefined;
|
|
22
22
|
getToolArgs?: (toolCallId: string) => unknown;
|
|
23
|
+
resolveImageData?: (data: string, mimeType: string | undefined) => string;
|
|
23
24
|
/**
|
|
24
25
|
* Session cwd. Tool call locations sent to ACP clients must be absolute
|
|
25
26
|
* (the editor host needs them to open or focus files). When provided,
|
|
@@ -143,8 +144,8 @@ export function mapToolKind(toolName: string): ToolKind {
|
|
|
143
144
|
case "exec":
|
|
144
145
|
case "eval":
|
|
145
146
|
return "execute";
|
|
146
|
-
case "
|
|
147
|
-
case "
|
|
147
|
+
case "grep":
|
|
148
|
+
case "glob":
|
|
148
149
|
case "ast_grep":
|
|
149
150
|
return "search";
|
|
150
151
|
case "web_search":
|
|
@@ -179,7 +180,7 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
179
180
|
case "tool_execution_update": {
|
|
180
181
|
const content = mergeToolUpdateContent(
|
|
181
182
|
buildToolStartContent(event.toolName, event.args),
|
|
182
|
-
extractToolCallContent(event.partialResult),
|
|
183
|
+
extractToolCallContent(event.partialResult, options),
|
|
183
184
|
);
|
|
184
185
|
const update: SessionUpdate = {
|
|
185
186
|
sessionUpdate: "tool_call_update",
|
|
@@ -197,7 +198,10 @@ export function mapAgentSessionEventToAcpSessionUpdates(
|
|
|
197
198
|
return [toSessionNotification(sessionId, update)];
|
|
198
199
|
}
|
|
199
200
|
case "tool_execution_end": {
|
|
200
|
-
const resultContent = [
|
|
201
|
+
const resultContent = [
|
|
202
|
+
...extractDiffToolCallContent(event.result),
|
|
203
|
+
...extractToolCallContent(event.result, options),
|
|
204
|
+
];
|
|
201
205
|
const content = mergeToolUpdateContent(
|
|
202
206
|
buildToolStartContent(event.toolName, getToolExecutionEndArgs(event, options)),
|
|
203
207
|
resultContent,
|
|
@@ -641,13 +645,15 @@ function terminalToolCallContent(terminalId: string): ToolCallContent {
|
|
|
641
645
|
return { type: "terminal", terminalId };
|
|
642
646
|
}
|
|
643
647
|
|
|
644
|
-
function extractToolCallContent(value: unknown): ToolCallContent[] {
|
|
645
|
-
const richContent = extractStructuredToolCallContent(value);
|
|
648
|
+
function extractToolCallContent(value: unknown, options: AcpEventMapperOptions): ToolCallContent[] {
|
|
649
|
+
const richContent = extractStructuredToolCallContent(value, options);
|
|
650
|
+
const detailsImageContent = extractDetailsImageToolCallContent(value, options, richContent);
|
|
651
|
+
const combinedContent = [...richContent, ...detailsImageContent];
|
|
646
652
|
const terminalId = extractTerminalId(value);
|
|
647
653
|
const content =
|
|
648
|
-
terminalId && !hasTerminalContent(
|
|
649
|
-
? [...
|
|
650
|
-
:
|
|
654
|
+
terminalId && !hasTerminalContent(combinedContent, terminalId)
|
|
655
|
+
? [...combinedContent, terminalToolCallContent(terminalId)]
|
|
656
|
+
: combinedContent;
|
|
651
657
|
const fallbackText = extractReadableText(value);
|
|
652
658
|
if (!fallbackText) {
|
|
653
659
|
return content;
|
|
@@ -658,7 +664,7 @@ function extractToolCallContent(value: unknown): ToolCallContent[] {
|
|
|
658
664
|
return [...content, textToolCallContent(fallbackText)];
|
|
659
665
|
}
|
|
660
666
|
|
|
661
|
-
function extractStructuredToolCallContent(value: unknown): ToolCallContent[] {
|
|
667
|
+
function extractStructuredToolCallContent(value: unknown, options: AcpEventMapperOptions): ToolCallContent[] {
|
|
662
668
|
const blocks = getContentBlocks(value);
|
|
663
669
|
if (!blocks) {
|
|
664
670
|
return [];
|
|
@@ -666,7 +672,7 @@ function extractStructuredToolCallContent(value: unknown): ToolCallContent[] {
|
|
|
666
672
|
|
|
667
673
|
const content: ToolCallContent[] = [];
|
|
668
674
|
for (const block of blocks) {
|
|
669
|
-
const toolCallContent = toToolCallContent(block);
|
|
675
|
+
const toolCallContent = toToolCallContent(block, options);
|
|
670
676
|
if (toolCallContent) {
|
|
671
677
|
content.push(toolCallContent);
|
|
672
678
|
}
|
|
@@ -685,7 +691,7 @@ function getContentBlocks(value: unknown): unknown[] | undefined {
|
|
|
685
691
|
return Array.isArray(content) ? content : undefined;
|
|
686
692
|
}
|
|
687
693
|
|
|
688
|
-
function toToolCallContent(value: unknown): ToolCallContent | undefined {
|
|
694
|
+
function toToolCallContent(value: unknown, options: AcpEventMapperOptions): ToolCallContent | undefined {
|
|
689
695
|
const type = getContentType(value);
|
|
690
696
|
if (!type) {
|
|
691
697
|
return undefined;
|
|
@@ -697,21 +703,8 @@ function toToolCallContent(value: unknown): ToolCallContent | undefined {
|
|
|
697
703
|
return text ? textToolCallContent(text) : undefined;
|
|
698
704
|
}
|
|
699
705
|
case "image":
|
|
700
|
-
case "audio":
|
|
701
|
-
|
|
702
|
-
const mimeType = extractStringProperty<BinaryLikeContent>(value, "mimeType");
|
|
703
|
-
if (!data || !mimeType) {
|
|
704
|
-
return undefined;
|
|
705
|
-
}
|
|
706
|
-
return {
|
|
707
|
-
type: "content",
|
|
708
|
-
content: {
|
|
709
|
-
type,
|
|
710
|
-
data,
|
|
711
|
-
mimeType,
|
|
712
|
-
},
|
|
713
|
-
};
|
|
714
|
-
}
|
|
706
|
+
case "audio":
|
|
707
|
+
return binaryToolCallContent(type, value, options);
|
|
715
708
|
case "resource_link": {
|
|
716
709
|
const uri = extractStringProperty<ResourceLinkLikeContent>(value, "uri");
|
|
717
710
|
const name = extractStringProperty<ResourceLinkLikeContent>(value, "name");
|
|
@@ -769,6 +762,64 @@ function toToolCallContent(value: unknown): ToolCallContent | undefined {
|
|
|
769
762
|
}
|
|
770
763
|
}
|
|
771
764
|
|
|
765
|
+
function binaryToolCallContent(
|
|
766
|
+
type: "image" | "audio",
|
|
767
|
+
value: unknown,
|
|
768
|
+
options: AcpEventMapperOptions,
|
|
769
|
+
): ToolCallContent | undefined {
|
|
770
|
+
const data = extractStringProperty<BinaryLikeContent>(value, "data");
|
|
771
|
+
const mimeType = extractStringProperty<BinaryLikeContent>(value, "mimeType");
|
|
772
|
+
if (!data || !mimeType) {
|
|
773
|
+
return undefined;
|
|
774
|
+
}
|
|
775
|
+
return {
|
|
776
|
+
type: "content",
|
|
777
|
+
content: {
|
|
778
|
+
type,
|
|
779
|
+
data: type === "image" ? (options.resolveImageData?.(data, mimeType) ?? data) : data,
|
|
780
|
+
mimeType,
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function extractDetailsImageToolCallContent(
|
|
786
|
+
value: unknown,
|
|
787
|
+
options: AcpEventMapperOptions,
|
|
788
|
+
existing: ToolCallContent[],
|
|
789
|
+
): ToolCallContent[] {
|
|
790
|
+
const images = extractDetailsImages(value);
|
|
791
|
+
if (!images) {
|
|
792
|
+
return [];
|
|
793
|
+
}
|
|
794
|
+
const seen = new Set(existing.map(imageContentKey).filter((key): key is string => key !== undefined));
|
|
795
|
+
const content: ToolCallContent[] = [];
|
|
796
|
+
for (const image of images) {
|
|
797
|
+
const toolCallContent = binaryToolCallContent("image", image, options);
|
|
798
|
+
const key = imageContentKey(toolCallContent);
|
|
799
|
+
if (!toolCallContent || !key || seen.has(key)) {
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
seen.add(key);
|
|
803
|
+
content.push(toolCallContent);
|
|
804
|
+
}
|
|
805
|
+
return content;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function extractDetailsImages(value: unknown): unknown[] | undefined {
|
|
809
|
+
if (typeof value !== "object" || value === null) return undefined;
|
|
810
|
+
const details = (value as DetailsContainer).details;
|
|
811
|
+
if (typeof details !== "object" || details === null) return undefined;
|
|
812
|
+
const images = (details as { images?: unknown }).images;
|
|
813
|
+
return Array.isArray(images) && images.length > 0 ? images : undefined;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function imageContentKey(value: ToolCallContent | undefined): string | undefined {
|
|
817
|
+
if (value?.type !== "content" || value.content.type !== "image") {
|
|
818
|
+
return undefined;
|
|
819
|
+
}
|
|
820
|
+
return `${value.content.mimeType}\u0000${value.content.data}`;
|
|
821
|
+
}
|
|
822
|
+
|
|
772
823
|
function extractEmbeddedResource(
|
|
773
824
|
value: unknown,
|
|
774
825
|
): { uri: string; text: string; mimeType?: string } | { uri: string; blob: string; mimeType?: string } | undefined {
|
|
@@ -846,6 +897,12 @@ function extractReadableText(value: unknown): string | undefined {
|
|
|
846
897
|
if (text.length > 0) {
|
|
847
898
|
return normalizeText(text);
|
|
848
899
|
}
|
|
900
|
+
if (hasBinaryContentBlock(contentBlocks)) {
|
|
901
|
+
return undefined;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
if (extractDetailsImages(value)) {
|
|
905
|
+
return undefined;
|
|
849
906
|
}
|
|
850
907
|
if (isTerminalOnlyDetails(value)) {
|
|
851
908
|
return undefined;
|
|
@@ -895,6 +952,13 @@ function getContentType(value: unknown): string | undefined {
|
|
|
895
952
|
return typeof type === "string" ? type : undefined;
|
|
896
953
|
}
|
|
897
954
|
|
|
955
|
+
function hasBinaryContentBlock(blocks: unknown[]): boolean {
|
|
956
|
+
return blocks.some(block => {
|
|
957
|
+
const type = getContentType(block);
|
|
958
|
+
return type === "image" || type === "audio";
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
|
|
898
962
|
function extractStringProperty<T extends object>(value: unknown, key: keyof T): string | undefined {
|
|
899
963
|
if (typeof value !== "object" || value === null || !(key in value)) {
|
|
900
964
|
return undefined;
|
|
@@ -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
|
|