@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.7
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 +54 -0
- package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
- package/dist/cli.js +3342 -3274
- package/dist/types/capability/index.d.ts +1 -1
- package/dist/types/capability/types.d.ts +23 -1
- package/dist/types/config/settings-schema.d.ts +39 -2
- package/dist/types/cursor.d.ts +2 -1
- package/dist/types/extensibility/extensions/runner.d.ts +4 -0
- package/dist/types/extensibility/shared-events.d.ts +18 -1
- package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
- package/dist/types/internal-urls/parse.d.ts +12 -0
- package/dist/types/internal-urls/router.d.ts +6 -0
- package/dist/types/internal-urls/types.d.ts +6 -0
- package/dist/types/lsp/config.d.ts +1 -0
- package/dist/types/lsp/types.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +5 -0
- package/dist/types/mcp/tool-bridge.d.ts +13 -0
- package/dist/types/modes/components/custom-editor.d.ts +5 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
- package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
- package/dist/types/sdk.d.ts +3 -1
- package/dist/types/session/agent-session-types.d.ts +8 -6
- package/dist/types/session/agent-session.d.ts +28 -1
- package/dist/types/session/model-controls.d.ts +4 -1
- package/dist/types/session/session-advisors.d.ts +7 -1
- package/dist/types/session/session-tools.d.ts +44 -6
- package/dist/types/session/streaming-output.d.ts +7 -2
- package/dist/types/session/tool-choice-queue.d.ts +6 -4
- package/dist/types/session/turn-recovery.d.ts +5 -3
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/types.d.ts +3 -11
- package/dist/types/thinking.d.ts +21 -2
- package/dist/types/tools/index.d.ts +8 -3
- package/dist/types/tools/output-meta.d.ts +5 -0
- package/dist/types/tools/read.d.ts +9 -1
- package/dist/types/tools/xdev.d.ts +53 -67
- package/dist/types/utils/cpuprofile.d.ts +51 -0
- package/dist/types/utils/inspect-image-mode.d.ts +29 -0
- package/dist/types/utils/profile-tree.d.ts +47 -0
- package/dist/types/utils/sample-profile.d.ts +67 -0
- package/dist/types/utils/title-generator.d.ts +17 -16
- package/package.json +12 -12
- package/src/capability/index.ts +43 -12
- package/src/capability/mcp.ts +21 -0
- package/src/capability/types.ts +20 -1
- package/src/cli/read-cli.ts +44 -2
- package/src/config/settings-schema.ts +42 -2
- package/src/config/settings.ts +35 -0
- package/src/cursor.ts +4 -3
- package/src/discovery/builtin-rules/index.ts +2 -0
- package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
- package/src/eval/py/runner.py +16 -2
- package/src/extensibility/extensions/runner.ts +117 -5
- package/src/extensibility/extensions/types.ts +0 -1
- package/src/extensibility/extensions/wrapper.ts +74 -42
- package/src/extensibility/hooks/tool-wrapper.ts +11 -4
- package/src/extensibility/hooks/types.ts +0 -1
- package/src/extensibility/shared-events.ts +18 -1
- package/src/internal-urls/mcp-protocol.ts +17 -3
- package/src/internal-urls/parse.ts +31 -0
- package/src/internal-urls/router.ts +24 -4
- package/src/internal-urls/types.ts +6 -0
- package/src/live/transport.ts +2 -2
- package/src/lsp/client.ts +2 -2
- package/src/lsp/config.ts +4 -0
- package/src/lsp/types.ts +2 -0
- package/src/mcp/config.ts +26 -14
- package/src/mcp/manager.ts +26 -9
- package/src/mcp/tool-bridge.ts +52 -1
- package/src/memories/index.ts +25 -6
- package/src/modes/components/custom-editor.ts +39 -16
- package/src/modes/components/status-line/segments.ts +3 -1
- package/src/modes/components/tips.txt +2 -1
- package/src/modes/components/tool-execution.ts +8 -7
- package/src/modes/controllers/command-controller.ts +10 -10
- package/src/modes/controllers/extension-ui-controller.ts +7 -7
- package/src/modes/controllers/selector-controller.ts +7 -2
- package/src/modes/rpc/rpc-mode.ts +60 -47
- package/src/prompts/steering/user-interjection.md +2 -5
- package/src/prompts/tools/task.md +4 -2
- package/src/sdk.ts +64 -58
- package/src/session/agent-session-types.ts +8 -5
- package/src/session/agent-session.ts +144 -11
- package/src/session/model-controls.ts +48 -12
- package/src/session/session-advisors.ts +30 -14
- package/src/session/session-listing.ts +66 -4
- package/src/session/session-tools.ts +162 -54
- package/src/session/streaming-output.ts +18 -6
- package/src/session/tool-choice-queue.ts +19 -4
- package/src/session/turn-recovery.ts +25 -6
- package/src/slash-commands/builtin-registry.ts +69 -0
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +43 -32
- package/src/task/types.ts +12 -17
- package/src/thinking.ts +68 -5
- package/src/tools/bash.ts +16 -9
- package/src/tools/index.ts +36 -16
- package/src/tools/output-meta.ts +20 -0
- package/src/tools/read.ts +89 -15
- package/src/tools/write.ts +16 -7
- package/src/tools/xdev.ts +198 -210
- package/src/utils/cpuprofile.ts +235 -0
- package/src/utils/inspect-image-mode.ts +39 -0
- package/src/utils/profile-tree.ts +111 -0
- package/src/utils/sample-profile.ts +437 -0
- package/src/utils/title-generator.ts +88 -34
- package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
- package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
- package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
- package/dist/types/cli/update-cli.test.d.ts +0 -1
- package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
- package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
- package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
- package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
- package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
- package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
- package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
- package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
- package/dist/types/hindsight/client.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
- package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
- package/dist/types/launch/broker-list-order.test.d.ts +0 -1
- package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
- package/dist/types/launch/protocol.test.d.ts +0 -1
- package/dist/types/launch/spawn-options.test.d.ts +0 -1
- package/dist/types/launch/terminal-output.test.d.ts +0 -1
- package/dist/types/live/protocol.test.d.ts +0 -1
- package/dist/types/mcp/config-writer.test.d.ts +0 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
- package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
- package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
- package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
- package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
- package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
- package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
- package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
- package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
- package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
- package/dist/types/modes/print-mode.test.d.ts +0 -1
- package/dist/types/modes/session-teardown.test.d.ts +0 -1
- package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
- package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
- package/dist/types/modes/warp-events.test.d.ts +0 -1
- package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
- package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
- package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
- package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
- package/dist/types/session/blob-store.test.d.ts +0 -1
- package/dist/types/session/messages.test.d.ts +0 -1
- package/dist/types/session/session-context.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
- package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
- package/dist/types/system-prompt.test.d.ts +0 -1
- package/dist/types/task/render.test.d.ts +0 -1
- package/dist/types/task/spawn-policy.test.d.ts +0 -1
- package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
- package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
- package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
- package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
- package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
- package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
- package/src/advisor/__tests__/advisor.test.ts +0 -4889
- package/src/advisor/__tests__/config.test.ts +0 -349
- package/src/advisor/__tests__/emission-guard.test.ts +0 -147
- package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
- package/src/cli/update-cli.test.ts +0 -28
- package/src/config/__tests__/model-registry.test.ts +0 -182
- package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
- package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
- package/src/eval/__tests__/budget-bridge.test.ts +0 -80
- package/src/eval/__tests__/completion-bridge.test.ts +0 -412
- package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
- package/src/eval/__tests__/idle-timeout.test.ts +0 -80
- package/src/eval/__tests__/js-context-manager.test.ts +0 -456
- package/src/eval/__tests__/julia-prelude.test.ts +0 -66
- package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
- package/src/eval/__tests__/prelude-agent.test.ts +0 -156
- package/src/eval/__tests__/process-entry-import.test.ts +0 -137
- package/src/eval/py/__tests__/prelude.test.ts +0 -104
- package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
- package/src/hindsight/client.test.ts +0 -75
- package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
- package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
- package/src/launch/broker-list-order.test.ts +0 -89
- package/src/launch/broker-output-snapshot.test.ts +0 -126
- package/src/launch/protocol.test.ts +0 -59
- package/src/launch/spawn-options.test.ts +0 -31
- package/src/launch/terminal-output.test.ts +0 -107
- package/src/live/protocol.test.ts +0 -140
- package/src/mcp/config-writer.test.ts +0 -43
- package/src/mcp/smithery-auth.test.ts +0 -29
- package/src/mcp/smithery-registry.test.ts +0 -51
- package/src/mcp/transports/stdio.test.ts +0 -427
- package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
- package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
- package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
- package/src/modes/components/__tests__/skill-message.test.ts +0 -94
- package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
- package/src/modes/components/custom-editor.test.ts +0 -510
- package/src/modes/components/login-dialog.test.ts +0 -56
- package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
- package/src/modes/components/status-line/component.test.ts +0 -84
- package/src/modes/components/tool-execution.test.ts +0 -162
- package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
- package/src/modes/noninteractive-dispose.test.ts +0 -73
- package/src/modes/print-mode.test.ts +0 -71
- package/src/modes/session-teardown.test.ts +0 -219
- package/src/modes/theme/mermaid-rendering.test.ts +0 -53
- package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
- package/src/modes/warp-events.test.ts +0 -794
- package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
- package/src/plan-mode/model-transition.test.ts +0 -60
- package/src/plan-mode/reentry-prompt.test.ts +0 -41
- package/src/session/agent-session-error-log.test.ts +0 -59
- package/src/session/blob-store.test.ts +0 -56
- package/src/session/messages.test.ts +0 -282
- package/src/session/session-context.test.ts +0 -384
- package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
- package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
- package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
- package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
- package/src/system-prompt.test.ts +0 -236
- package/src/task/render.test.ts +0 -290
- package/src/task/spawn-policy.test.ts +0 -62
- package/src/tools/__tests__/eval-description.test.ts +0 -18
- package/src/tools/__tests__/glob.test.ts +0 -37
- package/src/tools/__tests__/json-tree.test.ts +0 -35
- package/src/tools/__tests__/vibe-render.test.ts +0 -210
- package/src/tools/hub/launch-compat.test.ts +0 -40
- package/src/vibe/__tests__/token-rate.test.ts +0 -96
|
@@ -1,510 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "bun:test";
|
|
2
|
-
import { CURSOR_MARKER } from "@oh-my-pi/pi-tui";
|
|
3
|
-
import { setKittyProtocolActive } from "@oh-my-pi/pi-tui/keys";
|
|
4
|
-
import { $ } from "bun";
|
|
5
|
-
import { getDefaultPasteImageKeys } from "../../config/keybindings";
|
|
6
|
-
import { getEditorTheme, initTheme, theme } from "../theme/theme";
|
|
7
|
-
import {
|
|
8
|
-
CustomEditor,
|
|
9
|
-
extractBracketedImagePastePaths,
|
|
10
|
-
extractBracketedPastePaths,
|
|
11
|
-
extractImagePastePathsFromText,
|
|
12
|
-
extractImagePathFromText,
|
|
13
|
-
extractPastePathsFromText,
|
|
14
|
-
SPACE_HOLD_MECHANICAL_RUN,
|
|
15
|
-
SPACE_HOLD_RELEASE_MS,
|
|
16
|
-
SPACE_REPEAT_MAX_GAP_MS,
|
|
17
|
-
} from "./custom-editor";
|
|
18
|
-
|
|
19
|
-
function makeEditor() {
|
|
20
|
-
const editor = new CustomEditor(getEditorTheme());
|
|
21
|
-
const events: string[] = [];
|
|
22
|
-
editor.sttHoldEnabled = () => true;
|
|
23
|
-
editor.onSpaceHoldStart = () => events.push("start");
|
|
24
|
-
editor.onSpaceHoldEnd = () => events.push("end");
|
|
25
|
-
return { editor, events };
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** A gap below SPACE_REPEAT_MAX_GAP_MS — looks like OS key auto-repeat (a held bar). */
|
|
29
|
-
const REPEAT_GAP_MS = 30;
|
|
30
|
-
/** A gap above the threshold — looks like a deliberate keypress. */
|
|
31
|
-
const TAP_GAP_MS = SPACE_REPEAT_MAX_GAP_MS + 80;
|
|
32
|
-
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
33
|
-
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
34
|
-
|
|
35
|
-
function bracketedPaste(text: string): string {
|
|
36
|
-
return `${BRACKETED_PASTE_START}${text}${BRACKETED_PASTE_END}`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Feed `count` spaces `gapMs` apart on the fake clock. The first space of a run has no prior
|
|
40
|
-
* space, so its gap is effectively infinite and it always reads as a deliberate tap. */
|
|
41
|
-
function feedSpaces(editor: CustomEditor, count: number, gapMs: number): void {
|
|
42
|
-
for (let i = 0; i < count; i++) {
|
|
43
|
-
vi.advanceTimersByTime(gapMs);
|
|
44
|
-
editor.handleInput(" ");
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Feed spaces at explicit per-press gaps (ms) on the fake clock — for simulating an irregular cadence. */
|
|
49
|
-
function feedGaps(editor: CustomEditor, gaps: number[]): void {
|
|
50
|
-
for (const gapMs of gaps) {
|
|
51
|
-
vi.advanceTimersByTime(gapMs);
|
|
52
|
-
editor.handleInput(" ");
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function decorateInFreshProcess(text: string, imageLinks?: readonly string[]): Promise<string> {
|
|
57
|
-
const customEditorUrl = new URL("./custom-editor.ts", import.meta.url).href;
|
|
58
|
-
const script = `
|
|
59
|
-
import { CustomEditor } from ${JSON.stringify(customEditorUrl)};
|
|
60
|
-
const editor = new CustomEditor({});
|
|
61
|
-
editor.imageLinks = ${JSON.stringify(imageLinks)};
|
|
62
|
-
process.stdout.write(editor.decorateText(${JSON.stringify(text)}));
|
|
63
|
-
`;
|
|
64
|
-
const child = await $`bun -e ${script}`.quiet().nothrow();
|
|
65
|
-
const stdout = child.stdout.toString();
|
|
66
|
-
const stderr = child.stderr.toString();
|
|
67
|
-
if (child.exitCode !== 0) throw new Error(stderr || stdout || `decorate subprocess exited with ${child.exitCode}`);
|
|
68
|
-
return stdout;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
describe("CustomEditor placeholder decoration", () => {
|
|
72
|
-
it("renders paste placeholders before theme initialization", async () => {
|
|
73
|
-
const output = await decorateInFreshProcess("[Paste #1, +30 lines]");
|
|
74
|
-
expect(output).toBe("[Paste #1, +30 lines]");
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it("renders linked image placeholders before theme and settings initialization", async () => {
|
|
78
|
-
const output = await decorateInFreshProcess("[Image #1]", ["/tmp/example.png"]);
|
|
79
|
-
expect(output).toBe("[Image #1]");
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe("CustomEditor queue shorthand decoration", () => {
|
|
84
|
-
beforeAll(async () => {
|
|
85
|
-
await initTheme();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("reserves the first line as soon as either queue prefix is completed", () => {
|
|
89
|
-
for (const prefix of ["->", "=>"]) {
|
|
90
|
-
const editor = new CustomEditor(getEditorTheme());
|
|
91
|
-
editor.handleInput(prefix[0] ?? "");
|
|
92
|
-
expect(editor.getText()).toBe(prefix[0]);
|
|
93
|
-
|
|
94
|
-
editor.handleInput(prefix[1] ?? "");
|
|
95
|
-
expect(editor.getText()).toBe(`${prefix}\n`);
|
|
96
|
-
expect(editor.getCursor()).toEqual({ line: 1, col: 0 });
|
|
97
|
-
|
|
98
|
-
editor.handleInput("\x7f");
|
|
99
|
-
expect(editor.getText()).toBe(`${prefix}\n`);
|
|
100
|
-
expect(editor.getCursor()).toEqual({ line: 1, col: 0 });
|
|
101
|
-
}
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it("renders the reserved line as a dim Queueing header", () => {
|
|
105
|
-
for (const prefix of ["->", "=>"]) {
|
|
106
|
-
const editor = new CustomEditor(getEditorTheme());
|
|
107
|
-
editor.setText(`${prefix}\nqueue this`);
|
|
108
|
-
|
|
109
|
-
expect(editor.decorateText(prefix)).toBe(theme.fg("dim", `Queueing ${theme.nav.selected}`));
|
|
110
|
-
editor.focused = true;
|
|
111
|
-
const rendered = editor.render(40).map(line => Bun.stripANSI(line.replace(CURSOR_MARKER, "")));
|
|
112
|
-
expect(rendered.some(line => line.includes(`Queueing ${theme.nav.selected}`))).toBe(true);
|
|
113
|
-
expect(rendered.every(line => Bun.stringWidth(line) === 40)).toBe(true);
|
|
114
|
-
expect(rendered.some(line => line.includes("queue this"))).toBe(true);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("highlights dot and parenthesis markers only for detected queue lists", () => {
|
|
119
|
-
for (const [input, marker] of [
|
|
120
|
-
["=>\n1. first\n2. second", "1."],
|
|
121
|
-
["=>\n1) first\n2) second", "1)"],
|
|
122
|
-
]) {
|
|
123
|
-
const editor = new CustomEditor(getEditorTheme());
|
|
124
|
-
editor.setText(input);
|
|
125
|
-
expect(editor.decorateText(`${marker} first`).startsWith(theme.fg("accent", marker))).toBe(true);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const unfinished = new CustomEditor(getEditorTheme());
|
|
129
|
-
unfinished.setText("=>\n1. first\n2. second\n3. third\n4.");
|
|
130
|
-
expect(unfinished.decorateText("1. first").startsWith(theme.fg("accent", "1."))).toBe(true);
|
|
131
|
-
expect(unfinished.decorateText("4.").startsWith(theme.fg("accent", "4."))).toBe(true);
|
|
132
|
-
|
|
133
|
-
const editor = new CustomEditor(getEditorTheme());
|
|
134
|
-
editor.setText("=>\n1. first\n3. third");
|
|
135
|
-
expect(editor.decorateText("1. first")).toBe("1. first");
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe("CustomEditor bracketed path paste", () => {
|
|
140
|
-
it("leaves a pasted bare .png filename on the normal text path", () => {
|
|
141
|
-
expect(extractBracketedImagePastePaths(bracketedPaste("icon-photo-default.png"))).toBeUndefined();
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("extracts explicit local image paths for attachment", () => {
|
|
145
|
-
expect(extractBracketedImagePastePaths(bracketedPaste("/tmp/icon-photo-default.png"))).toEqual([
|
|
146
|
-
"/tmp/icon-photo-default.png",
|
|
147
|
-
]);
|
|
148
|
-
expect(extractBracketedImagePastePaths(bracketedPaste("C:\\Users\\me\\icon-photo-default.png"))).toEqual([
|
|
149
|
-
"C:\\Users\\me\\icon-photo-default.png",
|
|
150
|
-
]);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("strips `file://` URLs to the local filesystem path before loading the image", () => {
|
|
154
|
-
// macOS / Ghostty / iTerm2 sometimes forward the pasteboard's
|
|
155
|
-
// `public.file-url` representation when the user does Finder→Copy
|
|
156
|
-
// then Cmd+V. Without decoding, `loadImageInput` would try to read a
|
|
157
|
-
// literal `file:///…` path and fail.
|
|
158
|
-
expect(extractBracketedImagePastePaths(bracketedPaste("file:///Users/me/Pictures/photo.png"))).toEqual([
|
|
159
|
-
"/Users/me/Pictures/photo.png",
|
|
160
|
-
]);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("percent-decodes spaces inside `file://` URLs", () => {
|
|
164
|
-
expect(extractBracketedImagePastePaths(bracketedPaste("file:///Users/me/My%20Pictures/photo.png"))).toEqual([
|
|
165
|
-
"/Users/me/My Pictures/photo.png",
|
|
166
|
-
]);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("extracts explicit non-image paths without classifying them as image paths", () => {
|
|
170
|
-
expect(extractBracketedPastePaths(bracketedPaste("/tmp/report.csv"))).toEqual(["/tmp/report.csv"]);
|
|
171
|
-
expect(extractBracketedImagePastePaths(bracketedPaste("/tmp/report.csv"))).toBeUndefined();
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it("inserts non-image path pastes as literal text instead of attaching them", () => {
|
|
175
|
-
const { editor } = makeEditor();
|
|
176
|
-
let imagePathCalls = 0;
|
|
177
|
-
editor.onPasteImagePath = () => {
|
|
178
|
-
imagePathCalls++;
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
editor.handleInput(bracketedPaste("/tmp/report.csv"));
|
|
182
|
-
|
|
183
|
-
expect(editor.getText()).toBe("/tmp/report.csv");
|
|
184
|
-
expect(imagePathCalls).toBe(0);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it("attaches a spaced screenshot path as an image instead of inserting it as literal text", () => {
|
|
188
|
-
// #6578: the raw macOS "copy screenshot path" payload has unescaped
|
|
189
|
-
// spaces, which defeats the segment splitter. Before the whole-text
|
|
190
|
-
// fallback the paste degraded to literal text in the prompt.
|
|
191
|
-
const { editor } = makeEditor();
|
|
192
|
-
const screenshot = "/Users/me/Desktop/Screenshot 2026-07-24 at 1.55.12 PM.png";
|
|
193
|
-
const pasted: string[] = [];
|
|
194
|
-
editor.onPasteImagePath = path => {
|
|
195
|
-
pasted.push(path);
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
editor.handleInput(bracketedPaste(screenshot));
|
|
199
|
-
|
|
200
|
-
expect(pasted).toEqual([screenshot]);
|
|
201
|
-
expect(editor.getText()).toBe("");
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("keeps a two-file drag with unescaped spaces as text instead of attaching one fused path", () => {
|
|
205
|
-
// PR #6582 review: selecting two screenshots and dropping them together
|
|
206
|
-
// emits a single space-separated payload the splitter also refuses
|
|
207
|
-
// (`PM.png` carries no directory). Fusing it into one path attaches
|
|
208
|
-
// nothing — `handleImagePathPaste` hits ENOENT and only shows a status,
|
|
209
|
-
// never restoring the text — so the drop must degrade to a text paste.
|
|
210
|
-
const { editor } = makeEditor();
|
|
211
|
-
const dropped =
|
|
212
|
-
"/Users/me/Desktop/Screenshot 2026-07-24 at 1.55.12 PM.png /Users/me/Desktop/Screenshot 2026-07-24 at 1.56.00 PM.png";
|
|
213
|
-
const pasted: string[] = [];
|
|
214
|
-
editor.onPasteImagePath = path => {
|
|
215
|
-
pasted.push(path);
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
editor.handleInput(bracketedPaste(dropped));
|
|
219
|
-
|
|
220
|
-
expect(pasted).toEqual([]);
|
|
221
|
-
expect(editor.getText()).toBe(dropped);
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
describe("CustomEditor configured paste image keys", () => {
|
|
225
|
-
it("routes Ghostty Cmd+V kitty key events through the macOS image-paste default", () => {
|
|
226
|
-
const { editor } = makeEditor();
|
|
227
|
-
const onPasteImage = vi.fn();
|
|
228
|
-
editor.onPasteImage = onPasteImage;
|
|
229
|
-
editor.setActionKeys("app.clipboard.pasteImage", getDefaultPasteImageKeys("darwin"));
|
|
230
|
-
setKittyProtocolActive(true);
|
|
231
|
-
|
|
232
|
-
try {
|
|
233
|
-
editor.handleInput("\x1b[118;9u");
|
|
234
|
-
} finally {
|
|
235
|
-
setKittyProtocolActive(false);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
expect(onPasteImage).toHaveBeenCalledTimes(1);
|
|
239
|
-
expect(editor.getText()).toBe("");
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
describe("extractImagePathFromText (issue #3506)", () => {
|
|
244
|
-
it("returns the path when the text is a single image file path", () => {
|
|
245
|
-
expect(extractImagePathFromText("/tmp/screenshot.png")).toBe("/tmp/screenshot.png");
|
|
246
|
-
expect(extractImagePathFromText("/Users/me/Pictures/photo.jpeg")).toBe("/Users/me/Pictures/photo.jpeg");
|
|
247
|
-
expect(extractImagePathFromText("C:\\Users\\me\\img.gif")).toBe("C:\\Users\\me\\img.gif");
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it("ignores surrounding whitespace from a clipboard read", () => {
|
|
251
|
-
expect(extractImagePathFromText(" /tmp/photo.webp\n")).toBe("/tmp/photo.webp");
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it("returns undefined for a bare filename (no explicit directory)", () => {
|
|
255
|
-
// Mirrors the bracketed-paste contract: a bare `.png` filename is
|
|
256
|
-
// almost always a project-relative reference the user wants as text,
|
|
257
|
-
// not a clipboard-anchored attachment.
|
|
258
|
-
expect(extractImagePathFromText("icon.png")).toBeUndefined();
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it("returns undefined for non-image extensions", () => {
|
|
262
|
-
expect(extractImagePathFromText("/tmp/report.csv")).toBeUndefined();
|
|
263
|
-
expect(extractImagePathFromText("/tmp/notes.txt")).toBeUndefined();
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
it("returns undefined when the text contains anything beyond a single path", () => {
|
|
267
|
-
expect(extractImagePathFromText("see /tmp/screenshot.png")).toBeUndefined();
|
|
268
|
-
expect(extractImagePathFromText("/tmp/a.png /tmp/b.png")).toBeUndefined();
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it("returns undefined for empty/whitespace-only input", () => {
|
|
272
|
-
expect(extractImagePathFromText("")).toBeUndefined();
|
|
273
|
-
expect(extractImagePathFromText(" ")).toBeUndefined();
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it("decodes a `file://` URL to its filesystem path", () => {
|
|
277
|
-
expect(extractImagePathFromText("file:///Users/me/Pictures/photo.png")).toBe("/Users/me/Pictures/photo.png");
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
it("recovers a single anchored image path containing unescaped spaces (macOS screenshot name)", () => {
|
|
281
|
-
const macScreenshot = "/Users/me/Desktop/Screenshot 2026-06-25 at 1.23.45 PM.png";
|
|
282
|
-
expect(extractImagePathFromText(macScreenshot)).toBe(macScreenshot);
|
|
283
|
-
expect(extractImagePathFromText("~/Pictures/Cleanshot 2026-06-25 at 12.00.png")).toBe(
|
|
284
|
-
"~/Pictures/Cleanshot 2026-06-25 at 12.00.png",
|
|
285
|
-
);
|
|
286
|
-
expect(extractImagePathFromText("C:\\Users\\me\\My Pictures\\img with space.jpg")).toBe(
|
|
287
|
-
"C:\\Users\\me\\My Pictures\\img with space.jpg",
|
|
288
|
-
);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
it("returns undefined for two spaced paths the splitter could not separate", () => {
|
|
292
|
-
// Only the whole-text pass survives the splitter here, and it must not
|
|
293
|
-
// fuse the pair into one path the loader can never resolve.
|
|
294
|
-
expect(extractImagePathFromText("/tmp/a.png /tmp/b shot.png")).toBeUndefined();
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
it("does not hijack prose that happens to contain a path-shaped fragment", () => {
|
|
298
|
-
// The whole-text branch is gated on ABSOLUTE_PATH_PREFIX_REGEX, so a
|
|
299
|
-
// non-anchored prefix ("see ...") never triggers it.
|
|
300
|
-
expect(extractImagePathFromText("see /Users/me/Desktop/Screenshot 1.png")).toBeUndefined();
|
|
301
|
-
});
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
describe("extractPastePathsFromText", () => {
|
|
305
|
-
it("delegates to the same logic the bracketed variant uses for path detection", () => {
|
|
306
|
-
expect(extractPastePathsFromText("/tmp/a.png /tmp/b.png")).toEqual(["/tmp/a.png", "/tmp/b.png"]);
|
|
307
|
-
expect(extractPastePathsFromText("just text")).toBeUndefined();
|
|
308
|
-
});
|
|
309
|
-
});
|
|
310
|
-
|
|
311
|
-
describe("extractImagePastePathsFromText (issue #6578)", () => {
|
|
312
|
-
const MAC_SCREENSHOT =
|
|
313
|
-
"/var/folders/xx/T/TemporaryItems/NSIRD_screencaptureui_ab/Screenshot 2026-07-24 at 1.55.12 PM.png";
|
|
314
|
-
const WINDOWS_SPACED = "C:\\Users\\me\\My Pictures\\shot 1.png";
|
|
315
|
-
|
|
316
|
-
// Every case must resolve identically on the stripped-marker route
|
|
317
|
-
// (assembled pastes) and the bracketed route, since the latter now
|
|
318
|
-
// delegates to the former.
|
|
319
|
-
const cases: { name: string; text: string; expected: string[] | undefined }[] = [
|
|
320
|
-
{ name: "a macOS screenshot path with unescaped spaces", text: MAC_SCREENSHOT, expected: [MAC_SCREENSHOT] },
|
|
321
|
-
{ name: "a Windows drive path with unescaped spaces", text: WINDOWS_SPACED, expected: [WINDOWS_SPACED] },
|
|
322
|
-
{
|
|
323
|
-
name: "a home-anchored path with unescaped spaces",
|
|
324
|
-
text: "~/Pictures/Cleanshot 2026-07-24 at 12.00.png",
|
|
325
|
-
expected: ["~/Pictures/Cleanshot 2026-07-24 at 12.00.png"],
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
name: "a shell-escaped spaced path",
|
|
329
|
-
text: "/tmp/My\\ Photos/shot\\ 1.png",
|
|
330
|
-
expected: ["/tmp/My Photos/shot 1.png"],
|
|
331
|
-
},
|
|
332
|
-
{
|
|
333
|
-
name: "a double-quoted spaced path",
|
|
334
|
-
text: '"/tmp/My Photos/shot 1.png"',
|
|
335
|
-
expected: ["/tmp/My Photos/shot 1.png"],
|
|
336
|
-
},
|
|
337
|
-
{ name: "a spaced path with a non-image extension", text: "/tmp/my report 2026.csv", expected: undefined },
|
|
338
|
-
{
|
|
339
|
-
// Ends in a real image extension, so only the absolute-prefix
|
|
340
|
-
// anchor keeps the whole-text fallback from swallowing the prose.
|
|
341
|
-
name: "prose ending in a path-shaped fragment",
|
|
342
|
-
text: "see /Users/me/Desktop/Screen Shot 1.png",
|
|
343
|
-
expected: undefined,
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
name: "two spaced image paths on separate lines",
|
|
347
|
-
text: "/tmp/a shot.png\n/tmp/b shot.png",
|
|
348
|
-
expected: undefined,
|
|
349
|
-
},
|
|
350
|
-
{
|
|
351
|
-
name: "a bare spaced filename with no leading separator",
|
|
352
|
-
text: "Screenshot 2026-07-24 at 1.55.12 PM.png",
|
|
353
|
-
expected: undefined,
|
|
354
|
-
},
|
|
355
|
-
{
|
|
356
|
-
name: "two POSIX paths dragged together when one has unescaped spaces",
|
|
357
|
-
text: "/tmp/a.png /tmp/b shot.png",
|
|
358
|
-
expected: undefined,
|
|
359
|
-
},
|
|
360
|
-
{
|
|
361
|
-
name: "two macOS screenshots dragged together",
|
|
362
|
-
text: `${MAC_SCREENSHOT} /var/folders/xx/T/TemporaryItems/NSIRD_screencaptureui_ab/Screenshot 2026-07-24 at 1.56.00 PM.png`,
|
|
363
|
-
expected: undefined,
|
|
364
|
-
},
|
|
365
|
-
{
|
|
366
|
-
name: "two home-anchored paths with unescaped spaces",
|
|
367
|
-
text: "~/a.png ~/Pictures/b shot.png",
|
|
368
|
-
expected: undefined,
|
|
369
|
-
},
|
|
370
|
-
{
|
|
371
|
-
name: "two Windows drive paths with unescaped spaces",
|
|
372
|
-
text: `C:\\Users\\me\\a.png ${WINDOWS_SPACED}`,
|
|
373
|
-
expected: undefined,
|
|
374
|
-
},
|
|
375
|
-
{
|
|
376
|
-
name: "two `file://` URLs with unescaped spaces",
|
|
377
|
-
text: "file:///tmp/a.png file:///tmp/b shot.png",
|
|
378
|
-
expected: undefined,
|
|
379
|
-
},
|
|
380
|
-
{
|
|
381
|
-
name: "two UNC paths with unescaped spaces",
|
|
382
|
-
text: "\\\\srv\\share\\a.png \\\\srv\\share\\b shot.png",
|
|
383
|
-
expected: undefined,
|
|
384
|
-
},
|
|
385
|
-
{
|
|
386
|
-
name: "a tab-separated pair of dragged paths",
|
|
387
|
-
text: "/tmp/a.png\t/tmp/b shot.png",
|
|
388
|
-
expected: undefined,
|
|
389
|
-
},
|
|
390
|
-
{
|
|
391
|
-
name: "an absolute path followed by a dot-relative path with unescaped spaces",
|
|
392
|
-
text: "/tmp/a.png ./b shot.png",
|
|
393
|
-
expected: undefined,
|
|
394
|
-
},
|
|
395
|
-
{
|
|
396
|
-
name: "an absolute path followed by a parent-relative path with unescaped spaces",
|
|
397
|
-
text: "/tmp/a.png ../pics/b shot.png",
|
|
398
|
-
expected: undefined,
|
|
399
|
-
},
|
|
400
|
-
{
|
|
401
|
-
name: "a Windows drive path followed by a dot-relative path with unescaped spaces",
|
|
402
|
-
text: "C:\\Users\\me\\a.png .\\b shot.png",
|
|
403
|
-
expected: undefined,
|
|
404
|
-
},
|
|
405
|
-
{
|
|
406
|
-
// The interior `Photos/shot` token after an unescaped space is the
|
|
407
|
-
// shape of a spaced directory name, not of a second dragged path —
|
|
408
|
-
// this is why bare relatives are not multi-path anchors.
|
|
409
|
-
name: "a single path with an unescaped spaced directory name",
|
|
410
|
-
text: "/Users/me/My Photos/shot 1.png",
|
|
411
|
-
expected: ["/Users/me/My Photos/shot 1.png"],
|
|
412
|
-
},
|
|
413
|
-
{
|
|
414
|
-
// The escape asserts the space belongs to the path, so the `/sub`
|
|
415
|
-
// that follows is a component rather than a second drag payload.
|
|
416
|
-
name: "a path whose escaped space precedes a slash-led component",
|
|
417
|
-
text: "/tmp/odd dir\\ /sub/a b.png",
|
|
418
|
-
expected: ["/tmp/odd dir /sub/a b.png"],
|
|
419
|
-
},
|
|
420
|
-
{
|
|
421
|
-
// Splitter-success path: both segments are explicit, so the
|
|
422
|
-
// whole-text pass never runs and the pair still attaches as two.
|
|
423
|
-
name: "two explicit image paths the splitter can separate",
|
|
424
|
-
text: "/tmp/a.png /tmp/b.png",
|
|
425
|
-
expected: ["/tmp/a.png", "/tmp/b.png"],
|
|
426
|
-
},
|
|
427
|
-
];
|
|
428
|
-
|
|
429
|
-
for (const { name, text, expected } of cases) {
|
|
430
|
-
it(`${expected ? "recovers" : "rejects"} ${name} on both paste routes`, () => {
|
|
431
|
-
expect(extractImagePastePathsFromText(text)).toEqual(expected);
|
|
432
|
-
expect(extractBracketedImagePastePaths(bracketedPaste(text))).toEqual(expected);
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
describe("CustomEditor space-hold push-to-talk", () => {
|
|
438
|
-
beforeAll(async () => {
|
|
439
|
-
await initTheme();
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
beforeEach(() => {
|
|
443
|
-
vi.useFakeTimers();
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
afterEach(() => {
|
|
447
|
-
vi.useRealTimers();
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
it("types deliberate space taps without triggering, even several in a row", () => {
|
|
451
|
-
const { editor, events } = makeEditor();
|
|
452
|
-
feedSpaces(editor, 3, TAP_GAP_MS);
|
|
453
|
-
expect(editor.getText()).toBe(" ");
|
|
454
|
-
expect(events).toEqual([]);
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
it("recognizes a held bar from a steady fast cadence and tracks back the burst", () => {
|
|
458
|
-
const { editor, events } = makeEditor();
|
|
459
|
-
editor.handleInput("h");
|
|
460
|
-
editor.handleInput("i");
|
|
461
|
-
// Metronomic auto-repeat: the few pre-burst spaces typed are tracked back out when the hold is
|
|
462
|
-
// recognized, leaving only the pre-burst text.
|
|
463
|
-
feedSpaces(editor, SPACE_HOLD_MECHANICAL_RUN + 2, REPEAT_GAP_MS);
|
|
464
|
-
expect(editor.getText()).toBe("hi");
|
|
465
|
-
expect(events).toEqual(["start"]);
|
|
466
|
-
// Continued auto-repeat while the bar is held is swallowed: no spam, no re-trigger.
|
|
467
|
-
feedSpaces(editor, 5, REPEAT_GAP_MS);
|
|
468
|
-
expect(editor.getText()).toBe("hi");
|
|
469
|
-
expect(events).toEqual(["start"]);
|
|
470
|
-
// An idle gap with no further repeats means the bar was released -> stop + transcribe.
|
|
471
|
-
vi.advanceTimersByTime(SPACE_HOLD_RELEASE_MS + 1);
|
|
472
|
-
expect(events).toEqual(["start", "end"]);
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
it("does not trigger when the space bar is smashed at an irregular cadence", () => {
|
|
476
|
-
const { editor, events } = makeEditor();
|
|
477
|
-
// Fast but jittery, the way a human mashes — not the metronomic delta of OS auto-repeat.
|
|
478
|
-
const gaps = [40, 95, 45, 100, 35, 90, 50, 105];
|
|
479
|
-
feedGaps(editor, gaps);
|
|
480
|
-
expect(events).toEqual([]);
|
|
481
|
-
// Nothing is eaten: every smashed space still types a real space.
|
|
482
|
-
expect(editor.getText()).toBe(" ".repeat(gaps.length));
|
|
483
|
-
});
|
|
484
|
-
|
|
485
|
-
it("does not trigger on steady but slow spacing", () => {
|
|
486
|
-
const { editor, events } = makeEditor();
|
|
487
|
-
// Even cadence, but slower than auto-repeat: consistent deltas alone must not start recording.
|
|
488
|
-
feedSpaces(editor, 6, TAP_GAP_MS);
|
|
489
|
-
expect(events).toEqual([]);
|
|
490
|
-
expect(editor.getText()).toBe(" ".repeat(6));
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
it("does not trigger when a non-space breaks the run", () => {
|
|
494
|
-
const { editor, events } = makeEditor();
|
|
495
|
-
// Each partial run climbs the mechanical counter one short of the threshold; the non-space
|
|
496
|
-
// resets it so they never combine into a hold.
|
|
497
|
-
feedSpaces(editor, 3, REPEAT_GAP_MS);
|
|
498
|
-
editor.handleInput("x");
|
|
499
|
-
feedSpaces(editor, 3, REPEAT_GAP_MS);
|
|
500
|
-
expect(events).toEqual([]);
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
it("leaves the space bar typing normally when the gesture is disabled", () => {
|
|
504
|
-
const { editor, events } = makeEditor();
|
|
505
|
-
editor.sttHoldEnabled = () => false;
|
|
506
|
-
feedSpaces(editor, 8, REPEAT_GAP_MS);
|
|
507
|
-
expect(editor.getText()).toBe(" ".repeat(8));
|
|
508
|
-
expect(events).toEqual([]);
|
|
509
|
-
});
|
|
510
|
-
});
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { beforeAll, describe, expect, it } from "bun:test";
|
|
2
|
-
import type { TUI } from "@oh-my-pi/pi-tui";
|
|
3
|
-
import { initTheme } from "../theme/theme";
|
|
4
|
-
import { LoginDialogComponent } from "./login-dialog";
|
|
5
|
-
|
|
6
|
-
/** Minimal TUI stub — the dialog only calls requestRender/setFocus. */
|
|
7
|
-
function makeDialog(): LoginDialogComponent {
|
|
8
|
-
const tui = { requestRender() {}, setFocus() {} } as unknown as TUI;
|
|
9
|
-
return new LoginDialogComponent(tui, "openai-codex", () => {});
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
describe("LoginDialogComponent manual code input", () => {
|
|
13
|
-
beforeAll(async () => {
|
|
14
|
-
await initTheme();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it("captures a pasted fallback redirect URL and resolves on submit", async () => {
|
|
18
|
-
// Regression for #5339: paste-code providers (Codex) route the fallback
|
|
19
|
-
// URL through the focused dialog. Without a mounted input, the paste is
|
|
20
|
-
// dropped and login never completes.
|
|
21
|
-
const dialog = makeDialog();
|
|
22
|
-
dialog.showProgress("Waiting for callback");
|
|
23
|
-
|
|
24
|
-
const pending = dialog.showManualInput("Paste the authorization code:");
|
|
25
|
-
expect(dialog.render(80).join("\n")).toContain("Paste the authorization code");
|
|
26
|
-
|
|
27
|
-
const url = "http://localhost:1455/auth/callback?code=THECODE&state=abc";
|
|
28
|
-
dialog.pasteText(url);
|
|
29
|
-
dialog.handleInput("\r");
|
|
30
|
-
|
|
31
|
-
expect(await pending).toBe(url);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("reuses the mounted input across re-prompts instead of stacking duplicates", async () => {
|
|
35
|
-
// The OAuth callback loop re-invokes onManualCodeInput after an invalid
|
|
36
|
-
// paste; the second prompt must not append a duplicate input/hint block.
|
|
37
|
-
const dialog = makeDialog();
|
|
38
|
-
dialog.showProgress("Waiting for callback");
|
|
39
|
-
|
|
40
|
-
const first = dialog.showManualInput("Paste the code:");
|
|
41
|
-
dialog.handleInput("garbage");
|
|
42
|
-
dialog.handleInput("\r");
|
|
43
|
-
expect(await first).toBe("garbage");
|
|
44
|
-
|
|
45
|
-
const second = dialog.showManualInput("Paste the code:");
|
|
46
|
-
const rendered = dialog.render(80).join("\n");
|
|
47
|
-
expect(rendered.split("Paste the code:").length - 1).toBe(1);
|
|
48
|
-
// A stale value from the first attempt must not leak into the retry.
|
|
49
|
-
expect(rendered).not.toContain("garbage");
|
|
50
|
-
|
|
51
|
-
const url = "http://localhost:1455/auth/callback?code=OK&state=abc";
|
|
52
|
-
dialog.pasteText(url);
|
|
53
|
-
dialog.handleInput("\r");
|
|
54
|
-
expect(await second).toBe(url);
|
|
55
|
-
});
|
|
56
|
-
});
|