@ashx-j/lunr 0.1.7 → 0.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/dist/builtin-extensions/lunr-todos.d.ts +7 -6
- package/dist/builtin-extensions/lunr-todos.d.ts.map +1 -1
- package/dist/builtin-extensions/lunr-todos.js +24 -11
- package/dist/builtin-extensions/lunr-todos.js.map +1 -1
- package/dist/builtin-extensions/narumiruna-pi-goal/src/commands.d.ts.map +1 -1
- package/dist/builtin-extensions/narumiruna-pi-goal/src/commands.js +9 -5
- package/dist/builtin-extensions/narumiruna-pi-goal/src/commands.js.map +1 -1
- package/dist/builtin-extensions/narumiruna-pi-goal/src/goal.d.ts.map +1 -1
- package/dist/builtin-extensions/narumiruna-pi-goal/src/goal.js +31 -3
- package/dist/builtin-extensions/narumiruna-pi-goal/src/goal.js.map +1 -1
- package/dist/builtin-extensions/narumiruna-pi-goal/src/runtime.d.ts +1 -0
- package/dist/builtin-extensions/narumiruna-pi-goal/src/runtime.d.ts.map +1 -1
- package/dist/builtin-extensions/narumiruna-pi-goal/src/runtime.js +14 -4
- package/dist/builtin-extensions/narumiruna-pi-goal/src/runtime.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/extension/index.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/extension/index.js +4 -3
- package/dist/builtin-extensions/pi-subagents/src/extension/index.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js +12 -1
- package/dist/builtin-extensions/pi-subagents/src/runs/foreground/execution.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts +3 -0
- package/dist/builtin-extensions/pi-subagents/src/shared/types.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/shared/types.js.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts +8 -0
- package/dist/builtin-extensions/pi-subagents/src/tui/render.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-subagents/src/tui/render.js +22 -2
- package/dist/builtin-extensions/pi-subagents/src/tui/render.js.map +1 -1
- package/dist/builtin-extensions/pi-web-access/index.d.ts.map +1 -1
- package/dist/builtin-extensions/pi-web-access/index.js +38 -230
- package/dist/builtin-extensions/pi-web-access/index.js.map +1 -1
- package/dist/builtin-extensions/pi-web-access/render-search-chrome.d.ts +35 -0
- package/dist/builtin-extensions/pi-web-access/render-search-chrome.d.ts.map +1 -0
- package/dist/builtin-extensions/pi-web-access/render-search-chrome.js +93 -0
- package/dist/builtin-extensions/pi-web-access/render-search-chrome.js.map +1 -0
- package/dist/catalog/models.json +1 -1
- package/dist/catalog/providers/cloudflare-workers-ai.json +1 -1
- package/dist/catalog/providers/huggingface.json +1 -1
- package/dist/catalog/providers/opencode-go.json +1 -1
- package/dist/catalog/providers/openrouter.json +1 -1
- package/dist/catalog/providers/vercel-ai-gateway.json +1 -1
- package/dist/catalog/publication.json +3 -3
- package/dist/core/context-breakdown.d.ts +21 -1
- package/dist/core/context-breakdown.d.ts.map +1 -1
- package/dist/core/context-breakdown.js +58 -2
- package/dist/core/context-breakdown.js.map +1 -1
- package/dist/core/permission-mode-control.d.ts +27 -0
- package/dist/core/permission-mode-control.d.ts.map +1 -0
- package/dist/core/permission-mode-control.js +39 -0
- package/dist/core/permission-mode-control.js.map +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +0 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +14 -20
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/render-utils.d.ts +2 -0
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js +10 -0
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts +1 -0
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +1 -0
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/context-view.d.ts.map +1 -1
- package/dist/modes/interactive/components/context-view.js +3 -1
- package/dist/modes/interactive/components/context-view.js.map +1 -1
- package/dist/modes/interactive/components/plan-message.d.ts +17 -0
- package/dist/modes/interactive/components/plan-message.d.ts.map +1 -0
- package/dist/modes/interactive/components/plan-message.js +43 -0
- package/dist/modes/interactive/components/plan-message.js.map +1 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +1 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +3 -1
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/components/usage-view.d.ts +4 -25
- package/dist/modes/interactive/components/usage-view.d.ts.map +1 -1
- package/dist/modes/interactive/components/usage-view.js +17 -69
- package/dist/modes/interactive/components/usage-view.js.map +1 -1
- package/dist/modes/interactive/components/user-message.d.ts +1 -0
- package/dist/modes/interactive/components/user-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/user-message.js +1 -0
- package/dist/modes/interactive/components/user-message.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +17 -6
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +196 -114
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/smooth-streaming.d.ts +39 -6
- package/dist/modes/interactive/smooth-streaming.d.ts.map +1 -1
- package/dist/modes/interactive/smooth-streaming.js +160 -73
- package/dist/modes/interactive/smooth-streaming.js.map +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +7 -3
- package/dist/utils/clipboard-image.js.map +1 -1
- package/docs/terminal-setup.md +13 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +4 -4
|
@@ -1,10 +1,43 @@
|
|
|
1
1
|
import type { AssistantMessage } from "@ashx-j/lunr-ai/compat";
|
|
2
|
-
/**
|
|
3
|
-
export declare
|
|
2
|
+
/** Default base reveal speed (graphemes per tick) and hard per-tick cap. */
|
|
3
|
+
export declare const SMOOTH_STREAMING_BASE_GRAPHEMES_PER_TICK = 4;
|
|
4
|
+
export declare const SMOOTH_STREAMING_MAX_GRAPHEMES_PER_TICK = 48;
|
|
5
|
+
export interface SmoothStreamingOptions {
|
|
6
|
+
/** When true, thinking blocks are excluded from the reveal budget and output. */
|
|
7
|
+
hideThinking?: boolean;
|
|
8
|
+
}
|
|
4
9
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* inside a grapheme). Tool call blocks pass through untouched.
|
|
10
|
+
* Catch-up step size: grow with backlog, but never explode markdown reparses.
|
|
11
|
+
* Exported for unit tests.
|
|
8
12
|
*/
|
|
9
|
-
export declare function
|
|
13
|
+
export declare function computeSmoothRevealStep(backlog: number, base?: number, max?: number): number;
|
|
14
|
+
/**
|
|
15
|
+
* Per-block grapheme counter. Handles in-place append mutations from streaming
|
|
16
|
+
* providers without re-segmenting the entire string every tick.
|
|
17
|
+
*/
|
|
18
|
+
export declare class BlockUnitCounter {
|
|
19
|
+
private source;
|
|
20
|
+
private ends;
|
|
21
|
+
get count(): number;
|
|
22
|
+
get text(): string;
|
|
23
|
+
/** Recompute or incrementally extend segments for `text`. */
|
|
24
|
+
update(text: string): void;
|
|
25
|
+
/** Prefix of `source` covering the first `maxGraphemes` clusters. */
|
|
26
|
+
slice(maxGraphemes: number): string;
|
|
27
|
+
}
|
|
28
|
+
/** Total visible graphemes across a message's text (and thinking, unless hidden). */
|
|
29
|
+
export declare function countMessageGraphemes(message: AssistantMessage, options?: SmoothStreamingOptions): number;
|
|
30
|
+
/**
|
|
31
|
+
* Visible graphemes strictly before `toolCallId`. Used to flush the typewriter
|
|
32
|
+
* through leading text when a tool starts executing so the card can appear.
|
|
33
|
+
* Returns the full visible count when the tool id is missing.
|
|
34
|
+
*/
|
|
35
|
+
export declare function countGraphemesBeforeToolCall(message: AssistantMessage, toolCallId: string, options?: SmoothStreamingOptions): number;
|
|
36
|
+
/**
|
|
37
|
+
* Shallow copy of `message` with visible text/thinking truncated to
|
|
38
|
+
* `maxGraphemes` graphemes in order. Tool-call blocks are a reveal boundary:
|
|
39
|
+
* they appear only after all leading visible text/thinking is fully revealed.
|
|
40
|
+
* Leading tool calls (no prior visible text) stay visible even at 0 graphemes.
|
|
41
|
+
*/
|
|
42
|
+
export declare function sliceMessageContent(message: AssistantMessage, maxGraphemes: number, options?: SmoothStreamingOptions): AssistantMessage;
|
|
10
43
|
//# sourceMappingURL=smooth-streaming.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"smooth-streaming.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/smooth-streaming.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,
|
|
1
|
+
{"version":3,"file":"smooth-streaming.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/smooth-streaming.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAA0C,MAAM,8BAA8B,CAAC;AAe7G,4EAA4E;AAC5E,eAAO,MAAM,wCAAwC,IAAI,CAAC;AAC1D,eAAO,MAAM,uCAAuC,KAAK,CAAC;AAE1D,MAAM,WAAW,sBAAsB;IACtC,iFAAiF;IACjF,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACtC,OAAO,EAAE,MAAM,EACf,IAAI,GAAE,MAAiD,EACvD,GAAG,GAAE,MAAgD,GACnD,MAAM,CAGR;AAED;;;GAGG;AACH,qBAAa,gBAAgB;IAC5B,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,IAAI,CAAgB;IAE5B,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,6DAA6D;IAC7D,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAwBzB;IAED,qEAAqE;IACrE,KAAK,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAIlC;CACD;AAuCD,qFAAqF;AACrF,wBAAgB,qBAAqB,CACpC,OAAO,EAAE,gBAAgB,EACzB,OAAO,GAAE,sBAA2B,GAClC,MAAM,CAWR;AAED;;;;GAIG;AACH,wBAAgB,4BAA4B,CAC3C,OAAO,EAAE,gBAAgB,EACzB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,sBAA2B,GAClC,MAAM,CAeR;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAClC,OAAO,EAAE,gBAAgB,EACzB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,sBAA2B,GAClC,gBAAgB,CAwClB","sourcesContent":["import type { AssistantMessage, TextContent, ThinkingContent, ToolCall } from \"@ashx-j/lunr-ai/compat\";\n\n/**\n * Smooth streaming helpers: reveal assistant text/thinking grapheme by\n * grapheme instead of flashing whole chunks. The displayed message is a\n * shallow copy of the streaming target, truncated to N graphemes total\n * across its visible text/thinking blocks.\n *\n * Providers mutate block.text / block.thinking in place on the same content\n * objects, so segment state is cached per block identity with append-only\n * incremental re-segmentation (only the last grapheme cluster may change).\n */\n\nconst graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: \"grapheme\" });\n\n/** Default base reveal speed (graphemes per tick) and hard per-tick cap. */\nexport const SMOOTH_STREAMING_BASE_GRAPHEMES_PER_TICK = 4;\nexport const SMOOTH_STREAMING_MAX_GRAPHEMES_PER_TICK = 48;\n\nexport interface SmoothStreamingOptions {\n\t/** When true, thinking blocks are excluded from the reveal budget and output. */\n\thideThinking?: boolean;\n}\n\n/**\n * Catch-up step size: grow with backlog, but never explode markdown reparses.\n * Exported for unit tests.\n */\nexport function computeSmoothRevealStep(\n\tbacklog: number,\n\tbase: number = SMOOTH_STREAMING_BASE_GRAPHEMES_PER_TICK,\n\tmax: number = SMOOTH_STREAMING_MAX_GRAPHEMES_PER_TICK,\n): number {\n\tif (backlog <= 0) return 0;\n\treturn Math.min(max, Math.max(base, Math.ceil(backlog / 8)));\n}\n\n/**\n * Per-block grapheme counter. Handles in-place append mutations from streaming\n * providers without re-segmenting the entire string every tick.\n */\nexport class BlockUnitCounter {\n\tprivate source = \"\";\n\tprivate ends: number[] = [];\n\n\tget count(): number {\n\t\treturn this.ends.length;\n\t}\n\n\tget text(): string {\n\t\treturn this.source;\n\t}\n\n\t/** Recompute or incrementally extend segments for `text`. */\n\tupdate(text: string): void {\n\t\tif (text === this.source) return;\n\n\t\t// Pure append (the common streaming path): only the last grapheme cluster\n\t\t// may still be incomplete (ZWJ emoji, combining marks, etc.), so drop it\n\t\t// and re-segment from the previous stable boundary.\n\t\tif (text.startsWith(this.source)) {\n\t\t\tif (this.ends.length > 0) {\n\t\t\t\tthis.ends.pop();\n\t\t\t}\n\t\t\tconst sliceFrom = this.ends.length > 0 ? this.ends[this.ends.length - 1]! : 0;\n\t\t\tfor (const segment of graphemeSegmenter.segment(text.slice(sliceFrom))) {\n\t\t\t\tthis.ends.push(sliceFrom + segment.index + segment.segment.length);\n\t\t\t}\n\t\t\tthis.source = text;\n\t\t\treturn;\n\t\t}\n\n\t\t// Truncation or non-append edit: full recompute.\n\t\tthis.ends = [];\n\t\tfor (const segment of graphemeSegmenter.segment(text)) {\n\t\t\tthis.ends.push(segment.index + segment.segment.length);\n\t\t}\n\t\tthis.source = text;\n\t}\n\n\t/** Prefix of `source` covering the first `maxGraphemes` clusters. */\n\tslice(maxGraphemes: number): string {\n\t\tif (maxGraphemes <= 0) return \"\";\n\t\tif (maxGraphemes >= this.ends.length) return this.source;\n\t\treturn this.source.slice(0, this.ends[maxGraphemes - 1]!);\n\t}\n}\n\n/** Block-identity cache: content objects are mutated in place during stream. */\nconst blockCounters = new WeakMap<object, BlockUnitCounter>();\n\nfunction getBlockCounter(block: object): BlockUnitCounter {\n\tlet counter = blockCounters.get(block);\n\tif (!counter) {\n\t\tcounter = new BlockUnitCounter();\n\t\tblockCounters.set(block, counter);\n\t}\n\treturn counter;\n}\n\nfunction isTextBlock(block: TextContent | ThinkingContent | ToolCall): block is TextContent {\n\treturn block.type === \"text\";\n}\n\nfunction isThinkingBlock(block: TextContent | ThinkingContent | ToolCall): block is ThinkingContent {\n\treturn block.type === \"thinking\";\n}\n\nfunction blockText(block: TextContent | ThinkingContent): string {\n\treturn isTextBlock(block) ? block.text : block.thinking;\n}\n\nfunction withBlockText(block: TextContent | ThinkingContent, value: string): TextContent | ThinkingContent {\n\tif (isTextBlock(block)) {\n\t\treturn value === block.text ? block : { ...block, text: value };\n\t}\n\treturn value === block.thinking ? block : { ...block, thinking: value };\n}\n\nfunction syncCounter(block: TextContent | ThinkingContent): BlockUnitCounter {\n\tconst counter = getBlockCounter(block);\n\tcounter.update(blockText(block));\n\treturn counter;\n}\n\n/** Total visible graphemes across a message's text (and thinking, unless hidden). */\nexport function countMessageGraphemes(\n\tmessage: AssistantMessage,\n\toptions: SmoothStreamingOptions = {},\n): number {\n\tconst hideThinking = options.hideThinking === true;\n\tlet total = 0;\n\tfor (const block of message.content) {\n\t\tif (isTextBlock(block)) {\n\t\t\ttotal += syncCounter(block).count;\n\t\t} else if (isThinkingBlock(block) && !hideThinking) {\n\t\t\ttotal += syncCounter(block).count;\n\t\t}\n\t}\n\treturn total;\n}\n\n/**\n * Visible graphemes strictly before `toolCallId`. Used to flush the typewriter\n * through leading text when a tool starts executing so the card can appear.\n * Returns the full visible count when the tool id is missing.\n */\nexport function countGraphemesBeforeToolCall(\n\tmessage: AssistantMessage,\n\ttoolCallId: string,\n\toptions: SmoothStreamingOptions = {},\n): number {\n\tconst hideThinking = options.hideThinking === true;\n\tlet total = 0;\n\tfor (const block of message.content) {\n\t\tif (block.type === \"toolCall\") {\n\t\t\tif (block.id === toolCallId) return total;\n\t\t\tcontinue;\n\t\t}\n\t\tif (isTextBlock(block)) {\n\t\t\ttotal += syncCounter(block).count;\n\t\t} else if (isThinkingBlock(block) && !hideThinking) {\n\t\t\ttotal += syncCounter(block).count;\n\t\t}\n\t}\n\treturn total;\n}\n\n/**\n * Shallow copy of `message` with visible text/thinking truncated to\n * `maxGraphemes` graphemes in order. Tool-call blocks are a reveal boundary:\n * they appear only after all leading visible text/thinking is fully revealed.\n * Leading tool calls (no prior visible text) stay visible even at 0 graphemes.\n */\nexport function sliceMessageContent(\n\tmessage: AssistantMessage,\n\tmaxGraphemes: number,\n\toptions: SmoothStreamingOptions = {},\n): AssistantMessage {\n\tconst hideThinking = options.hideThinking === true;\n\tlet remaining = Math.max(0, maxGraphemes);\n\tlet truncated = false;\n\tconst content: (TextContent | ThinkingContent | ToolCall)[] = [];\n\n\tfor (const block of message.content) {\n\t\tif (truncated) break;\n\n\t\tif (isThinkingBlock(block) && hideThinking) {\n\t\t\t// Hidden thinking does not consume budget and is omitted from display.\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (isTextBlock(block) || isThinkingBlock(block)) {\n\t\t\tif (remaining <= 0) {\n\t\t\t\t// No budget for further text; stop before later tool cards too.\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst counter = syncCounter(block);\n\t\t\tconst take = Math.min(remaining, counter.count);\n\t\t\tif (take >= counter.count) {\n\t\t\t\tcontent.push(block);\n\t\t\t\tremaining -= counter.count;\n\t\t\t} else {\n\t\t\t\tcontent.push(withBlockText(block, counter.slice(take)));\n\t\t\t\tremaining = 0;\n\t\t\t\t// Partial text: hold later tool cards until this block catches up.\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\t// toolCall: visible once all prior visible text/thinking is fully revealed.\n\t\t// Leading tools (no prior text) pass through even at maxGraphemes === 0.\n\t\tcontent.push(block);\n\t}\n\n\treturn { ...message, content };\n}\n"]}
|
|
@@ -2,94 +2,181 @@
|
|
|
2
2
|
* Smooth streaming helpers: reveal assistant text/thinking grapheme by
|
|
3
3
|
* grapheme instead of flashing whole chunks. The displayed message is a
|
|
4
4
|
* shallow copy of the streaming target, truncated to N graphemes total
|
|
5
|
-
* across its text
|
|
5
|
+
* across its visible text/thinking blocks.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Providers mutate block.text / block.thinking in place on the same content
|
|
8
|
+
* objects, so segment state is cached per block identity with append-only
|
|
9
|
+
* incremental re-segmentation (only the last grapheme cluster may change).
|
|
9
10
|
*/
|
|
10
11
|
const graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
/** Default base reveal speed (graphemes per tick) and hard per-tick cap. */
|
|
13
|
+
export const SMOOTH_STREAMING_BASE_GRAPHEMES_PER_TICK = 4;
|
|
14
|
+
export const SMOOTH_STREAMING_MAX_GRAPHEMES_PER_TICK = 48;
|
|
15
|
+
/**
|
|
16
|
+
* Catch-up step size: grow with backlog, but never explode markdown reparses.
|
|
17
|
+
* Exported for unit tests.
|
|
18
|
+
*/
|
|
19
|
+
export function computeSmoothRevealStep(backlog, base = SMOOTH_STREAMING_BASE_GRAPHEMES_PER_TICK, max = SMOOTH_STREAMING_MAX_GRAPHEMES_PER_TICK) {
|
|
20
|
+
if (backlog <= 0)
|
|
21
|
+
return 0;
|
|
22
|
+
return Math.min(max, Math.max(base, Math.ceil(backlog / 8)));
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Per-block grapheme counter. Handles in-place append mutations from streaming
|
|
26
|
+
* providers without re-segmenting the entire string every tick.
|
|
27
|
+
*/
|
|
28
|
+
export class BlockUnitCounter {
|
|
29
|
+
source = "";
|
|
30
|
+
ends = [];
|
|
31
|
+
get count() {
|
|
32
|
+
return this.ends.length;
|
|
16
33
|
}
|
|
17
|
-
|
|
34
|
+
get text() {
|
|
35
|
+
return this.source;
|
|
36
|
+
}
|
|
37
|
+
/** Recompute or incrementally extend segments for `text`. */
|
|
38
|
+
update(text) {
|
|
39
|
+
if (text === this.source)
|
|
40
|
+
return;
|
|
41
|
+
// Pure append (the common streaming path): only the last grapheme cluster
|
|
42
|
+
// may still be incomplete (ZWJ emoji, combining marks, etc.), so drop it
|
|
43
|
+
// and re-segment from the previous stable boundary.
|
|
44
|
+
if (text.startsWith(this.source)) {
|
|
45
|
+
if (this.ends.length > 0) {
|
|
46
|
+
this.ends.pop();
|
|
47
|
+
}
|
|
48
|
+
const sliceFrom = this.ends.length > 0 ? this.ends[this.ends.length - 1] : 0;
|
|
49
|
+
for (const segment of graphemeSegmenter.segment(text.slice(sliceFrom))) {
|
|
50
|
+
this.ends.push(sliceFrom + segment.index + segment.segment.length);
|
|
51
|
+
}
|
|
52
|
+
this.source = text;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// Truncation or non-append edit: full recompute.
|
|
56
|
+
this.ends = [];
|
|
57
|
+
for (const segment of graphemeSegmenter.segment(text)) {
|
|
58
|
+
this.ends.push(segment.index + segment.segment.length);
|
|
59
|
+
}
|
|
60
|
+
this.source = text;
|
|
61
|
+
}
|
|
62
|
+
/** Prefix of `source` covering the first `maxGraphemes` clusters. */
|
|
63
|
+
slice(maxGraphemes) {
|
|
64
|
+
if (maxGraphemes <= 0)
|
|
65
|
+
return "";
|
|
66
|
+
if (maxGraphemes >= this.ends.length)
|
|
67
|
+
return this.source;
|
|
68
|
+
return this.source.slice(0, this.ends[maxGraphemes - 1]);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Block-identity cache: content objects are mutated in place during stream. */
|
|
72
|
+
const blockCounters = new WeakMap();
|
|
73
|
+
function getBlockCounter(block) {
|
|
74
|
+
let counter = blockCounters.get(block);
|
|
75
|
+
if (!counter) {
|
|
76
|
+
counter = new BlockUnitCounter();
|
|
77
|
+
blockCounters.set(block, counter);
|
|
78
|
+
}
|
|
79
|
+
return counter;
|
|
80
|
+
}
|
|
81
|
+
function isTextBlock(block) {
|
|
82
|
+
return block.type === "text";
|
|
83
|
+
}
|
|
84
|
+
function isThinkingBlock(block) {
|
|
85
|
+
return block.type === "thinking";
|
|
86
|
+
}
|
|
87
|
+
function blockText(block) {
|
|
88
|
+
return isTextBlock(block) ? block.text : block.thinking;
|
|
18
89
|
}
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
90
|
+
function withBlockText(block, value) {
|
|
91
|
+
if (isTextBlock(block)) {
|
|
92
|
+
return value === block.text ? block : { ...block, text: value };
|
|
93
|
+
}
|
|
94
|
+
return value === block.thinking ? block : { ...block, thinking: value };
|
|
95
|
+
}
|
|
96
|
+
function syncCounter(block) {
|
|
97
|
+
const counter = getBlockCounter(block);
|
|
98
|
+
counter.update(blockText(block));
|
|
99
|
+
return counter;
|
|
100
|
+
}
|
|
101
|
+
/** Total visible graphemes across a message's text (and thinking, unless hidden). */
|
|
102
|
+
export function countMessageGraphemes(message, options = {}) {
|
|
103
|
+
const hideThinking = options.hideThinking === true;
|
|
23
104
|
let total = 0;
|
|
24
|
-
const
|
|
25
|
-
if (block
|
|
26
|
-
|
|
27
|
-
total += segs.ends.length;
|
|
28
|
-
return segs;
|
|
105
|
+
for (const block of message.content) {
|
|
106
|
+
if (isTextBlock(block)) {
|
|
107
|
+
total += syncCounter(block).count;
|
|
29
108
|
}
|
|
30
|
-
if (block
|
|
31
|
-
|
|
32
|
-
total += segs.ends.length;
|
|
33
|
-
return segs;
|
|
109
|
+
else if (isThinkingBlock(block) && !hideThinking) {
|
|
110
|
+
total += syncCounter(block).count;
|
|
34
111
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
cached = { blocks, total };
|
|
38
|
-
segmentCache.set(message, cached);
|
|
39
|
-
return cached;
|
|
40
|
-
}
|
|
41
|
-
/** Total graphemes across a message's text and thinking blocks. */
|
|
42
|
-
export function countMessageGraphemes(message) {
|
|
43
|
-
return getMessageSegments(message).total;
|
|
112
|
+
}
|
|
113
|
+
return total;
|
|
44
114
|
}
|
|
45
115
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
116
|
+
* Visible graphemes strictly before `toolCallId`. Used to flush the typewriter
|
|
117
|
+
* through leading text when a tool starts executing so the card can appear.
|
|
118
|
+
* Returns the full visible count when the tool id is missing.
|
|
49
119
|
*/
|
|
50
|
-
export function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
120
|
+
export function countGraphemesBeforeToolCall(message, toolCallId, options = {}) {
|
|
121
|
+
const hideThinking = options.hideThinking === true;
|
|
122
|
+
let total = 0;
|
|
123
|
+
for (const block of message.content) {
|
|
124
|
+
if (block.type === "toolCall") {
|
|
125
|
+
if (block.id === toolCallId)
|
|
126
|
+
return total;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (isTextBlock(block)) {
|
|
130
|
+
total += syncCounter(block).count;
|
|
131
|
+
}
|
|
132
|
+
else if (isThinkingBlock(block) && !hideThinking) {
|
|
133
|
+
total += syncCounter(block).count;
|
|
134
|
+
}
|
|
57
135
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
136
|
+
return total;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Shallow copy of `message` with visible text/thinking truncated to
|
|
140
|
+
* `maxGraphemes` graphemes in order. Tool-call blocks are a reveal boundary:
|
|
141
|
+
* they appear only after all leading visible text/thinking is fully revealed.
|
|
142
|
+
* Leading tool calls (no prior visible text) stay visible even at 0 graphemes.
|
|
143
|
+
*/
|
|
144
|
+
export function sliceMessageContent(message, maxGraphemes, options = {}) {
|
|
145
|
+
const hideThinking = options.hideThinking === true;
|
|
146
|
+
let remaining = Math.max(0, maxGraphemes);
|
|
147
|
+
let truncated = false;
|
|
148
|
+
const content = [];
|
|
149
|
+
for (const block of message.content) {
|
|
150
|
+
if (truncated)
|
|
151
|
+
break;
|
|
152
|
+
if (isThinkingBlock(block) && hideThinking) {
|
|
153
|
+
// Hidden thinking does not consume budget and is omitted from display.
|
|
154
|
+
continue;
|
|
67
155
|
}
|
|
68
|
-
if (block
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
156
|
+
if (isTextBlock(block) || isThinkingBlock(block)) {
|
|
157
|
+
if (remaining <= 0) {
|
|
158
|
+
// No budget for further text; stop before later tool cards too.
|
|
159
|
+
truncated = true;
|
|
160
|
+
break;
|
|
73
161
|
}
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
if (block.type === "thinking") {
|
|
81
|
-
const segs = blocks[index];
|
|
82
|
-
if (!segs || segs.source !== block.thinking) {
|
|
83
|
-
return block;
|
|
162
|
+
const counter = syncCounter(block);
|
|
163
|
+
const take = Math.min(remaining, counter.count);
|
|
164
|
+
if (take >= counter.count) {
|
|
165
|
+
content.push(block);
|
|
166
|
+
remaining -= counter.count;
|
|
84
167
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
168
|
+
else {
|
|
169
|
+
content.push(withBlockText(block, counter.slice(take)));
|
|
170
|
+
remaining = 0;
|
|
171
|
+
// Partial text: hold later tool cards until this block catches up.
|
|
172
|
+
truncated = true;
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
90
175
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
176
|
+
// toolCall: visible once all prior visible text/thinking is fully revealed.
|
|
177
|
+
// Leading tools (no prior text) pass through even at maxGraphemes === 0.
|
|
178
|
+
content.push(block);
|
|
179
|
+
}
|
|
180
|
+
return { ...message, content };
|
|
94
181
|
}
|
|
95
182
|
//# sourceMappingURL=smooth-streaming.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"smooth-streaming.js","sourceRoot":"","sources":["../../../src/modes/interactive/smooth-streaming.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssistantMessage } from \"@ashx-j/lunr-ai/compat\";\n\n/**\n * Smooth streaming helpers: reveal assistant text/thinking grapheme by\n * grapheme instead of flashing whole chunks. The displayed message is a\n * shallow copy of the streaming target, truncated to N graphemes total\n * across its text and thinking blocks.\n *\n * Grapheme segment boundaries are cached per message via a WeakMap so the\n * streaming tick does not re-segment the whole message every 20 ms.\n */\n\nconst graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: \"grapheme\" });\n\ninterface BlockSegments {\n\t/** Original text for quick identity checks. */\n\tsource: string;\n\t/** End index (byte/code-unit) for each grapheme cluster, in order. */\n\tends: number[];\n}\n\ninterface MessageSegments {\n\tblocks: (BlockSegments | undefined)[];\n\t/** Total graphemes across text/thinking blocks. */\n\ttotal: number;\n}\n\nconst segmentCache = new WeakMap<AssistantMessage, MessageSegments>();\n\nfunction computeBlockSegments(text: string): BlockSegments {\n\tconst ends: number[] = [];\n\tfor (const segment of graphemeSegmenter.segment(text)) {\n\t\tends.push(segment.index + segment.segment.length);\n\t}\n\treturn { source: text, ends };\n}\n\nfunction getMessageSegments(message: AssistantMessage): MessageSegments {\n\tlet cached = segmentCache.get(message);\n\tif (cached) return cached;\n\n\tlet total = 0;\n\tconst blocks: (BlockSegments | undefined)[] = message.content.map((block) => {\n\t\tif (block.type === \"text\") {\n\t\t\tconst segs = computeBlockSegments(block.text);\n\t\t\ttotal += segs.ends.length;\n\t\t\treturn segs;\n\t\t}\n\t\tif (block.type === \"thinking\") {\n\t\t\tconst segs = computeBlockSegments(block.thinking);\n\t\t\ttotal += segs.ends.length;\n\t\t\treturn segs;\n\t\t}\n\t\treturn undefined;\n\t});\n\tcached = { blocks, total };\n\tsegmentCache.set(message, cached);\n\treturn cached;\n}\n\n/** Total graphemes across a message's text and thinking blocks. */\nexport function countMessageGraphemes(message: AssistantMessage): number {\n\treturn getMessageSegments(message).total;\n}\n\n/**\n * Shallow copy of `message` with text/thinking blocks truncated to\n * `maxGraphemes` graphemes in total (block order preserved, never splits\n * inside a grapheme). Tool call blocks pass through untouched.\n */\nexport function sliceMessageContent(message: AssistantMessage, maxGraphemes: number): AssistantMessage {\n\tif (maxGraphemes <= 0) {\n\t\t// Empty display: preserve tool-call blocks so they remain visible.\n\t\treturn {\n\t\t\t...message,\n\t\t\tcontent: message.content.filter((block) => block.type !== \"text\" && block.type !== \"thinking\"),\n\t\t};\n\t}\n\n\tconst { blocks } = getMessageSegments(message);\n\tlet remaining = maxGraphemes;\n\tconst content = message.content.map((block, index) => {\n\t\tif (remaining <= 0) {\n\t\t\t// No budget left for this text/thinking block.\n\t\t\tif (block.type === \"text\" || block.type === \"thinking\") {\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t\treturn block;\n\t\t}\n\n\t\tif (block.type === \"text\") {\n\t\t\tconst segs = blocks[index];\n\t\t\tif (!segs || segs.source !== block.text) {\n\t\t\t\t// Cache stale (shouldn't happen with immutable messages); fallback.\n\t\t\t\treturn block;\n\t\t\t}\n\t\t\tconst take = Math.min(remaining, segs.ends.length);\n\t\t\tconst end = take === 0 ? 0 : segs.ends[take - 1];\n\t\t\tconst text = block.text.slice(0, end);\n\t\t\tremaining -= take;\n\t\t\treturn text === block.text ? block : { ...block, text };\n\t\t}\n\n\t\tif (block.type === \"thinking\") {\n\t\t\tconst segs = blocks[index];\n\t\t\tif (!segs || segs.source !== block.thinking) {\n\t\t\t\treturn block;\n\t\t\t}\n\t\t\tconst take = Math.min(remaining, segs.ends.length);\n\t\t\tconst end = take === 0 ? 0 : segs.ends[take - 1];\n\t\t\tconst thinking = block.thinking.slice(0, end);\n\t\t\tremaining -= take;\n\t\t\treturn thinking === block.thinking ? block : { ...block, thinking };\n\t\t}\n\n\t\treturn block;\n\t});\n\treturn { ...message, content: content.filter((b): b is NonNullable<typeof b> => b !== undefined) };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"smooth-streaming.js","sourceRoot":"","sources":["../../../src/modes/interactive/smooth-streaming.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AssistantMessage, TextContent, ThinkingContent, ToolCall } from \"@ashx-j/lunr-ai/compat\";\n\n/**\n * Smooth streaming helpers: reveal assistant text/thinking grapheme by\n * grapheme instead of flashing whole chunks. The displayed message is a\n * shallow copy of the streaming target, truncated to N graphemes total\n * across its visible text/thinking blocks.\n *\n * Providers mutate block.text / block.thinking in place on the same content\n * objects, so segment state is cached per block identity with append-only\n * incremental re-segmentation (only the last grapheme cluster may change).\n */\n\nconst graphemeSegmenter = new Intl.Segmenter(undefined, { granularity: \"grapheme\" });\n\n/** Default base reveal speed (graphemes per tick) and hard per-tick cap. */\nexport const SMOOTH_STREAMING_BASE_GRAPHEMES_PER_TICK = 4;\nexport const SMOOTH_STREAMING_MAX_GRAPHEMES_PER_TICK = 48;\n\nexport interface SmoothStreamingOptions {\n\t/** When true, thinking blocks are excluded from the reveal budget and output. */\n\thideThinking?: boolean;\n}\n\n/**\n * Catch-up step size: grow with backlog, but never explode markdown reparses.\n * Exported for unit tests.\n */\nexport function computeSmoothRevealStep(\n\tbacklog: number,\n\tbase: number = SMOOTH_STREAMING_BASE_GRAPHEMES_PER_TICK,\n\tmax: number = SMOOTH_STREAMING_MAX_GRAPHEMES_PER_TICK,\n): number {\n\tif (backlog <= 0) return 0;\n\treturn Math.min(max, Math.max(base, Math.ceil(backlog / 8)));\n}\n\n/**\n * Per-block grapheme counter. Handles in-place append mutations from streaming\n * providers without re-segmenting the entire string every tick.\n */\nexport class BlockUnitCounter {\n\tprivate source = \"\";\n\tprivate ends: number[] = [];\n\n\tget count(): number {\n\t\treturn this.ends.length;\n\t}\n\n\tget text(): string {\n\t\treturn this.source;\n\t}\n\n\t/** Recompute or incrementally extend segments for `text`. */\n\tupdate(text: string): void {\n\t\tif (text === this.source) return;\n\n\t\t// Pure append (the common streaming path): only the last grapheme cluster\n\t\t// may still be incomplete (ZWJ emoji, combining marks, etc.), so drop it\n\t\t// and re-segment from the previous stable boundary.\n\t\tif (text.startsWith(this.source)) {\n\t\t\tif (this.ends.length > 0) {\n\t\t\t\tthis.ends.pop();\n\t\t\t}\n\t\t\tconst sliceFrom = this.ends.length > 0 ? this.ends[this.ends.length - 1]! : 0;\n\t\t\tfor (const segment of graphemeSegmenter.segment(text.slice(sliceFrom))) {\n\t\t\t\tthis.ends.push(sliceFrom + segment.index + segment.segment.length);\n\t\t\t}\n\t\t\tthis.source = text;\n\t\t\treturn;\n\t\t}\n\n\t\t// Truncation or non-append edit: full recompute.\n\t\tthis.ends = [];\n\t\tfor (const segment of graphemeSegmenter.segment(text)) {\n\t\t\tthis.ends.push(segment.index + segment.segment.length);\n\t\t}\n\t\tthis.source = text;\n\t}\n\n\t/** Prefix of `source` covering the first `maxGraphemes` clusters. */\n\tslice(maxGraphemes: number): string {\n\t\tif (maxGraphemes <= 0) return \"\";\n\t\tif (maxGraphemes >= this.ends.length) return this.source;\n\t\treturn this.source.slice(0, this.ends[maxGraphemes - 1]!);\n\t}\n}\n\n/** Block-identity cache: content objects are mutated in place during stream. */\nconst blockCounters = new WeakMap<object, BlockUnitCounter>();\n\nfunction getBlockCounter(block: object): BlockUnitCounter {\n\tlet counter = blockCounters.get(block);\n\tif (!counter) {\n\t\tcounter = new BlockUnitCounter();\n\t\tblockCounters.set(block, counter);\n\t}\n\treturn counter;\n}\n\nfunction isTextBlock(block: TextContent | ThinkingContent | ToolCall): block is TextContent {\n\treturn block.type === \"text\";\n}\n\nfunction isThinkingBlock(block: TextContent | ThinkingContent | ToolCall): block is ThinkingContent {\n\treturn block.type === \"thinking\";\n}\n\nfunction blockText(block: TextContent | ThinkingContent): string {\n\treturn isTextBlock(block) ? block.text : block.thinking;\n}\n\nfunction withBlockText(block: TextContent | ThinkingContent, value: string): TextContent | ThinkingContent {\n\tif (isTextBlock(block)) {\n\t\treturn value === block.text ? block : { ...block, text: value };\n\t}\n\treturn value === block.thinking ? block : { ...block, thinking: value };\n}\n\nfunction syncCounter(block: TextContent | ThinkingContent): BlockUnitCounter {\n\tconst counter = getBlockCounter(block);\n\tcounter.update(blockText(block));\n\treturn counter;\n}\n\n/** Total visible graphemes across a message's text (and thinking, unless hidden). */\nexport function countMessageGraphemes(\n\tmessage: AssistantMessage,\n\toptions: SmoothStreamingOptions = {},\n): number {\n\tconst hideThinking = options.hideThinking === true;\n\tlet total = 0;\n\tfor (const block of message.content) {\n\t\tif (isTextBlock(block)) {\n\t\t\ttotal += syncCounter(block).count;\n\t\t} else if (isThinkingBlock(block) && !hideThinking) {\n\t\t\ttotal += syncCounter(block).count;\n\t\t}\n\t}\n\treturn total;\n}\n\n/**\n * Visible graphemes strictly before `toolCallId`. Used to flush the typewriter\n * through leading text when a tool starts executing so the card can appear.\n * Returns the full visible count when the tool id is missing.\n */\nexport function countGraphemesBeforeToolCall(\n\tmessage: AssistantMessage,\n\ttoolCallId: string,\n\toptions: SmoothStreamingOptions = {},\n): number {\n\tconst hideThinking = options.hideThinking === true;\n\tlet total = 0;\n\tfor (const block of message.content) {\n\t\tif (block.type === \"toolCall\") {\n\t\t\tif (block.id === toolCallId) return total;\n\t\t\tcontinue;\n\t\t}\n\t\tif (isTextBlock(block)) {\n\t\t\ttotal += syncCounter(block).count;\n\t\t} else if (isThinkingBlock(block) && !hideThinking) {\n\t\t\ttotal += syncCounter(block).count;\n\t\t}\n\t}\n\treturn total;\n}\n\n/**\n * Shallow copy of `message` with visible text/thinking truncated to\n * `maxGraphemes` graphemes in order. Tool-call blocks are a reveal boundary:\n * they appear only after all leading visible text/thinking is fully revealed.\n * Leading tool calls (no prior visible text) stay visible even at 0 graphemes.\n */\nexport function sliceMessageContent(\n\tmessage: AssistantMessage,\n\tmaxGraphemes: number,\n\toptions: SmoothStreamingOptions = {},\n): AssistantMessage {\n\tconst hideThinking = options.hideThinking === true;\n\tlet remaining = Math.max(0, maxGraphemes);\n\tlet truncated = false;\n\tconst content: (TextContent | ThinkingContent | ToolCall)[] = [];\n\n\tfor (const block of message.content) {\n\t\tif (truncated) break;\n\n\t\tif (isThinkingBlock(block) && hideThinking) {\n\t\t\t// Hidden thinking does not consume budget and is omitted from display.\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (isTextBlock(block) || isThinkingBlock(block)) {\n\t\t\tif (remaining <= 0) {\n\t\t\t\t// No budget for further text; stop before later tool cards too.\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tconst counter = syncCounter(block);\n\t\t\tconst take = Math.min(remaining, counter.count);\n\t\t\tif (take >= counter.count) {\n\t\t\t\tcontent.push(block);\n\t\t\t\tremaining -= counter.count;\n\t\t\t} else {\n\t\t\t\tcontent.push(withBlockText(block, counter.slice(take)));\n\t\t\t\tremaining = 0;\n\t\t\t\t// Partial text: hold later tool cards until this block catches up.\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\n\t\t// toolCall: visible once all prior visible text/thinking is fully revealed.\n\t\t// Leading tools (no prior text) pass through even at maxGraphemes === 0.\n\t\tcontent.push(block);\n\t}\n\n\treturn { ...message, content };\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clipboard-image.d.ts","sourceRoot":"","sources":["../../src/utils/clipboard-image.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,cAAc,GAAG;IAC5B,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AASF,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAE9E;AAMD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAazE;AAuQD,wBAAsB,kBAAkB,CAAC,OAAO,CAAC,EAAE;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC3B,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAkDjC","sourcesContent":["import { spawnSync } from \"child_process\";\nimport { randomUUID } from \"crypto\";\nimport { readFileSync, statSync, unlinkSync } from \"fs\";\nimport { tmpdir } from \"os\";\nimport { extname, join } from \"path\";\n\nimport { clipboard } from \"./clipboard-native.ts\";\nimport { loadPhoton } from \"./photon.ts\";\n\nexport type ClipboardImage = {\n\tbytes: Uint8Array;\n\tmimeType: string;\n};\n\nconst SUPPORTED_IMAGE_MIME_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"] as const;\n\nconst DEFAULT_LIST_TIMEOUT_MS = 1000;\nconst DEFAULT_READ_TIMEOUT_MS = 3000;\nconst DEFAULT_POWERSHELL_TIMEOUT_MS = 5000;\nconst DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024;\n\nexport function isWaylandSession(env: NodeJS.ProcessEnv = process.env): boolean {\n\treturn Boolean(env.WAYLAND_DISPLAY) || env.XDG_SESSION_TYPE === \"wayland\";\n}\n\nfunction baseMimeType(mimeType: string): string {\n\treturn mimeType.split(\";\")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();\n}\n\nexport function extensionForImageMimeType(mimeType: string): string | null {\n\tswitch (baseMimeType(mimeType)) {\n\t\tcase \"image/png\":\n\t\t\treturn \"png\";\n\t\tcase \"image/jpeg\":\n\t\t\treturn \"jpg\";\n\t\tcase \"image/webp\":\n\t\t\treturn \"webp\";\n\t\tcase \"image/gif\":\n\t\t\treturn \"gif\";\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n\nfunction selectPreferredImageMimeType(mimeTypes: string[]): string | null {\n\tconst normalized = mimeTypes\n\t\t.map((t) => t.trim())\n\t\t.filter(Boolean)\n\t\t.map((t) => ({ raw: t, base: baseMimeType(t) }));\n\n\tfor (const preferred of SUPPORTED_IMAGE_MIME_TYPES) {\n\t\tconst match = normalized.find((t) => t.base === preferred);\n\t\tif (match) {\n\t\t\treturn match.raw;\n\t\t}\n\t}\n\n\tconst anyImage = normalized.find((t) => t.base.startsWith(\"image/\"));\n\treturn anyImage?.raw ?? null;\n}\n\nfunction isSupportedImageMimeType(mimeType: string): boolean {\n\tconst base = baseMimeType(mimeType);\n\treturn SUPPORTED_IMAGE_MIME_TYPES.some((t) => t === base);\n}\n\n/**\n * Convert unsupported image formats to PNG using Photon.\n * Returns null if conversion is unavailable or fails.\n */\nasync function convertToPng(bytes: Uint8Array): Promise<Uint8Array | null> {\n\tconst photon = await loadPhoton();\n\tif (!photon) {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst image = photon.PhotonImage.new_from_byteslice(bytes);\n\t\ttry {\n\t\t\treturn image.get_bytes();\n\t\t} finally {\n\t\t\timage.free();\n\t\t}\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction runCommand(\n\tcommand: string,\n\targs: string[],\n\toptions?: { timeoutMs?: number; maxBufferBytes?: number; env?: NodeJS.ProcessEnv },\n): { stdout: Buffer; ok: boolean } {\n\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS;\n\tconst maxBufferBytes = options?.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;\n\n\tconst result = spawnSync(command, args, {\n\t\ttimeout: timeoutMs,\n\t\tmaxBuffer: maxBufferBytes,\n\t\tenv: options?.env,\n\t});\n\n\tif (result.error) {\n\t\treturn { ok: false, stdout: Buffer.alloc(0) };\n\t}\n\n\tif (result.status !== 0) {\n\t\treturn { ok: false, stdout: Buffer.alloc(0) };\n\t}\n\n\tconst stdout = Buffer.isBuffer(result.stdout)\n\t\t? result.stdout\n\t\t: Buffer.from(result.stdout ?? \"\", typeof result.stdout === \"string\" ? \"utf-8\" : undefined);\n\n\treturn { ok: true, stdout };\n}\n\nfunction readClipboardImageViaWlPaste(): ClipboardImage | null {\n\tconst list = runCommand(\"wl-paste\", [\"--list-types\"], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });\n\tif (!list.ok) {\n\t\treturn null;\n\t}\n\n\tconst types = list.stdout\n\t\t.toString(\"utf-8\")\n\t\t.split(/\\r?\\n/)\n\t\t.map((t) => t.trim())\n\t\t.filter(Boolean);\n\n\tconst selectedType = selectPreferredImageMimeType(types);\n\tif (!selectedType) {\n\t\treturn null;\n\t}\n\n\tconst data = runCommand(\"wl-paste\", [\"--type\", selectedType, \"--no-newline\"]);\n\tif (!data.ok || data.stdout.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn { bytes: data.stdout, mimeType: baseMimeType(selectedType) };\n}\n\nfunction isWSL(env: NodeJS.ProcessEnv = process.env): boolean {\n\tif (env.WSL_DISTRO_NAME || env.WSLENV) {\n\t\treturn true;\n\t}\n\n\ttry {\n\t\tconst release = readFileSync(\"/proc/version\", \"utf-8\");\n\t\treturn /microsoft|wsl/i.test(release);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * On WSL, the Linux clipboard (Wayland/X11) does not receive image data from\n * Windows screenshots (Win+Shift+S). PowerShell can access the Windows clipboard\n * directly, so we use it as a fallback. On native Windows it doubles as a\n * fallback when the native clipboard module is unavailable or reports no image.\n */\nfunction readClipboardImageViaPowerShell(useWslpath: boolean): ClipboardImage | null {\n\tconst tmpFile = join(tmpdir(), `pi-clip-${randomUUID()}.png`);\n\n\ttry {\n\t\tlet winPath = tmpFile;\n\t\tif (useWslpath) {\n\t\t\tconst winPathResult = runCommand(\"wslpath\", [\"-w\", tmpFile], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });\n\t\t\tif (!winPathResult.ok) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\twinPath = winPathResult.stdout.toString(\"utf-8\").trim();\n\t\t\tif (!winPath) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\tconst psQuotedWinPath = winPath.replaceAll(\"'\", \"''\");\n\t\tconst psScript = [\n\t\t\t\"Add-Type -AssemblyName System.Windows.Forms\",\n\t\t\t\"Add-Type -AssemblyName System.Drawing\",\n\t\t\t`$path = '${psQuotedWinPath}'`,\n\t\t\t\"$img = [System.Windows.Forms.Clipboard]::GetImage()\",\n\t\t\t\"if ($img) { $img.Save($path, [System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'ok' } else { Write-Output 'empty' }\",\n\t\t].join(\"; \");\n\n\t\tconst result = runCommand(\"powershell.exe\", [\"-NoProfile\", \"-Command\", psScript], {\n\t\t\ttimeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS,\n\t\t});\n\t\tif (!result.ok) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst output = result.stdout.toString(\"utf-8\").trim();\n\t\tif (output !== \"ok\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst bytes = readFileSync(tmpFile);\n\t\tif (bytes.length === 0) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn { bytes: new Uint8Array(bytes), mimeType: \"image/png\" };\n\t} catch {\n\t\treturn null;\n\t} finally {\n\t\ttry {\n\t\t\tunlinkSync(tmpFile);\n\t\t} catch {\n\t\t\t// Ignore cleanup errors.\n\t\t}\n\t}\n}\n\nconst FILE_DROP_IMAGE_MIME_TYPES: Record<string, string> = {\n\t\".png\": \"image/png\",\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".webp\": \"image/webp\",\n\t\".gif\": \"image/gif\",\n\t\".bmp\": \"image/bmp\",\n};\n\n/**\n * On Windows, copying an image file in Explorer puts a file reference\n * (CF_HDROP) on the clipboard instead of image data, so `hasImage()` is false.\n * Return the first image file from the drop list. Only used on native Windows.\n */\nfunction readClipboardImageFileDropViaPowerShell(): ClipboardImage | null {\n\tconst result = runCommand(\"powershell.exe\", [\"-NoProfile\", \"-Command\", \"Get-Clipboard -Format FileDropList\"], {\n\t\ttimeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS,\n\t});\n\tif (!result.ok || result.stdout.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst candidates = result.stdout\n\t\t.toString(\"utf-8\")\n\t\t.split(/\\r?\\n/)\n\t\t.map((p) => p.trim())\n\t\t.filter(Boolean);\n\n\tfor (const filePath of candidates) {\n\t\tconst mimeType = FILE_DROP_IMAGE_MIME_TYPES[extname(filePath).toLowerCase()];\n\t\tif (!mimeType) {\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stat = statSync(filePath);\n\t\t\tif (!stat.isFile() || stat.size === 0 || stat.size > DEFAULT_MAX_BUFFER_BYTES) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treturn { bytes: new Uint8Array(readFileSync(filePath)), mimeType };\n\t\t} catch {\n\t\t\t// Unreadable file; try the next candidate.\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction readClipboardImageViaXclip(): ClipboardImage | null {\n\tconst targets = runCommand(\"xclip\", [\"-selection\", \"clipboard\", \"-t\", \"TARGETS\", \"-o\"], {\n\t\ttimeoutMs: DEFAULT_LIST_TIMEOUT_MS,\n\t});\n\n\tlet candidateTypes: string[] = [];\n\tif (targets.ok) {\n\t\tcandidateTypes = targets.stdout\n\t\t\t.toString(\"utf-8\")\n\t\t\t.split(/\\r?\\n/)\n\t\t\t.map((t) => t.trim())\n\t\t\t.filter(Boolean);\n\t}\n\n\tconst preferred = candidateTypes.length > 0 ? selectPreferredImageMimeType(candidateTypes) : null;\n\tconst tryTypes = preferred ? [preferred, ...SUPPORTED_IMAGE_MIME_TYPES] : [...SUPPORTED_IMAGE_MIME_TYPES];\n\n\tfor (const mimeType of tryTypes) {\n\t\tconst data = runCommand(\"xclip\", [\"-selection\", \"clipboard\", \"-t\", mimeType, \"-o\"]);\n\t\tif (data.ok && data.stdout.length > 0) {\n\t\t\treturn { bytes: data.stdout, mimeType: baseMimeType(mimeType) };\n\t\t}\n\t}\n\n\treturn null;\n}\n\nasync function readClipboardImageViaNativeClipboard(): Promise<ClipboardImage | null> {\n\tif (!clipboard || !clipboard.hasImage()) {\n\t\treturn null;\n\t}\n\n\tconst imageData = await clipboard.getImageBinary();\n\tif (!imageData || imageData.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst bytes = imageData instanceof Uint8Array ? imageData : Uint8Array.from(imageData);\n\treturn { bytes, mimeType: \"image/png\" };\n}\n\nexport async function readClipboardImage(options?: {\n\tenv?: NodeJS.ProcessEnv;\n\tplatform?: NodeJS.Platform;\n}): Promise<ClipboardImage | null> {\n\tconst env = options?.env ?? process.env;\n\tconst platform = options?.platform ?? process.platform;\n\n\tif (env.TERMUX_VERSION) {\n\t\treturn null;\n\t}\n\n\tlet image: ClipboardImage | null = null;\n\n\tif (platform === \"linux\") {\n\t\tconst wsl = isWSL(env);\n\t\tconst wayland = isWaylandSession(env);\n\n\t\tif (wayland || wsl) {\n\t\t\timage = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip();\n\t\t}\n\n\t\tif (!image && wsl) {\n\t\t\timage = readClipboardImageViaPowerShell(true);\n\t\t}\n\n\t\tif (!image && !wayland) {\n\t\t\timage = (await readClipboardImageViaNativeClipboard()) ?? readClipboardImageViaXclip();\n\t\t}\n\t} else if (platform === \"win32\") {\n\t\t// Native module first; PowerShell covers a missing/failed native module and\n\t\t// file-drop clipboards (image copied as a file in Explorer).\n\t\timage =\n\t\t\t(await readClipboardImageViaNativeClipboard()) ??\n\t\t\treadClipboardImageViaPowerShell(false) ??\n\t\t\treadClipboardImageFileDropViaPowerShell();\n\t} else {\n\t\timage = await readClipboardImageViaNativeClipboard();\n\t}\n\n\tif (!image) {\n\t\treturn null;\n\t}\n\n\t// Convert unsupported formats (e.g., BMP from WSLg) to PNG\n\tif (!isSupportedImageMimeType(image.mimeType)) {\n\t\tconst pngBytes = await convertToPng(image.bytes);\n\t\tif (!pngBytes) {\n\t\t\treturn null;\n\t\t}\n\t\treturn { bytes: pngBytes, mimeType: \"image/png\" };\n\t}\n\n\treturn image;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"clipboard-image.d.ts","sourceRoot":"","sources":["../../src/utils/clipboard-image.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,cAAc,GAAG;IAC5B,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AASF,wBAAgB,gBAAgB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,OAAO,CAE9E;AAMD,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAazE;AA2QD,wBAAsB,kBAAkB,CAAC,OAAO,CAAC,EAAE;IAClD,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,QAAQ,CAAC,EAAE,MAAM,CAAC,QAAQ,CAAC;CAC3B,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC,CAkDjC","sourcesContent":["import { spawnSync } from \"child_process\";\nimport { randomUUID } from \"crypto\";\nimport { readFileSync, statSync, unlinkSync } from \"fs\";\nimport { tmpdir } from \"os\";\nimport { extname, join } from \"path\";\n\nimport { clipboard } from \"./clipboard-native.ts\";\nimport { loadPhoton } from \"./photon.ts\";\n\nexport type ClipboardImage = {\n\tbytes: Uint8Array;\n\tmimeType: string;\n};\n\nconst SUPPORTED_IMAGE_MIME_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"] as const;\n\nconst DEFAULT_LIST_TIMEOUT_MS = 1000;\nconst DEFAULT_READ_TIMEOUT_MS = 3000;\nconst DEFAULT_POWERSHELL_TIMEOUT_MS = 5000;\nconst DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024;\n\nexport function isWaylandSession(env: NodeJS.ProcessEnv = process.env): boolean {\n\treturn Boolean(env.WAYLAND_DISPLAY) || env.XDG_SESSION_TYPE === \"wayland\";\n}\n\nfunction baseMimeType(mimeType: string): string {\n\treturn mimeType.split(\";\")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();\n}\n\nexport function extensionForImageMimeType(mimeType: string): string | null {\n\tswitch (baseMimeType(mimeType)) {\n\t\tcase \"image/png\":\n\t\t\treturn \"png\";\n\t\tcase \"image/jpeg\":\n\t\t\treturn \"jpg\";\n\t\tcase \"image/webp\":\n\t\t\treturn \"webp\";\n\t\tcase \"image/gif\":\n\t\t\treturn \"gif\";\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n\nfunction selectPreferredImageMimeType(mimeTypes: string[]): string | null {\n\tconst normalized = mimeTypes\n\t\t.map((t) => t.trim())\n\t\t.filter(Boolean)\n\t\t.map((t) => ({ raw: t, base: baseMimeType(t) }));\n\n\tfor (const preferred of SUPPORTED_IMAGE_MIME_TYPES) {\n\t\tconst match = normalized.find((t) => t.base === preferred);\n\t\tif (match) {\n\t\t\treturn match.raw;\n\t\t}\n\t}\n\n\tconst anyImage = normalized.find((t) => t.base.startsWith(\"image/\"));\n\treturn anyImage?.raw ?? null;\n}\n\nfunction isSupportedImageMimeType(mimeType: string): boolean {\n\tconst base = baseMimeType(mimeType);\n\treturn SUPPORTED_IMAGE_MIME_TYPES.some((t) => t === base);\n}\n\n/**\n * Convert unsupported image formats to PNG using Photon.\n * Returns null if conversion is unavailable or fails.\n */\nasync function convertToPng(bytes: Uint8Array): Promise<Uint8Array | null> {\n\tconst photon = await loadPhoton();\n\tif (!photon) {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst image = photon.PhotonImage.new_from_byteslice(bytes);\n\t\ttry {\n\t\t\treturn image.get_bytes();\n\t\t} finally {\n\t\t\timage.free();\n\t\t}\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction runCommand(\n\tcommand: string,\n\targs: string[],\n\toptions?: { timeoutMs?: number; maxBufferBytes?: number; env?: NodeJS.ProcessEnv },\n): { stdout: Buffer; ok: boolean } {\n\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS;\n\tconst maxBufferBytes = options?.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;\n\n\tconst result = spawnSync(command, args, {\n\t\ttimeout: timeoutMs,\n\t\tmaxBuffer: maxBufferBytes,\n\t\tenv: options?.env,\n\t});\n\n\tif (result.error) {\n\t\treturn { ok: false, stdout: Buffer.alloc(0) };\n\t}\n\n\tif (result.status !== 0) {\n\t\treturn { ok: false, stdout: Buffer.alloc(0) };\n\t}\n\n\tconst stdout = Buffer.isBuffer(result.stdout)\n\t\t? result.stdout\n\t\t: Buffer.from(result.stdout ?? \"\", typeof result.stdout === \"string\" ? \"utf-8\" : undefined);\n\n\treturn { ok: true, stdout };\n}\n\nfunction readClipboardImageViaWlPaste(): ClipboardImage | null {\n\tconst list = runCommand(\"wl-paste\", [\"--list-types\"], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });\n\tif (!list.ok) {\n\t\treturn null;\n\t}\n\n\tconst types = list.stdout\n\t\t.toString(\"utf-8\")\n\t\t.split(/\\r?\\n/)\n\t\t.map((t) => t.trim())\n\t\t.filter(Boolean);\n\n\tconst selectedType = selectPreferredImageMimeType(types);\n\tif (!selectedType) {\n\t\treturn null;\n\t}\n\n\tconst data = runCommand(\"wl-paste\", [\"--type\", selectedType, \"--no-newline\"]);\n\tif (!data.ok || data.stdout.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn { bytes: data.stdout, mimeType: baseMimeType(selectedType) };\n}\n\nfunction isWSL(env: NodeJS.ProcessEnv = process.env): boolean {\n\tif (env.WSL_DISTRO_NAME || env.WSLENV) {\n\t\treturn true;\n\t}\n\n\ttry {\n\t\tconst release = readFileSync(\"/proc/version\", \"utf-8\");\n\t\treturn /microsoft|wsl/i.test(release);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * On WSL, the Linux clipboard (Wayland/X11) does not receive image data from\n * Windows screenshots (Win+Shift+S). PowerShell can access the Windows clipboard\n * directly, so we use it as a fallback. On native Windows it doubles as a\n * fallback when the native clipboard module is unavailable or reports no image.\n */\nfunction readClipboardImageViaPowerShell(useWslpath: boolean): ClipboardImage | null {\n\tconst tmpFile = join(tmpdir(), `pi-clip-${randomUUID()}.png`);\n\n\ttry {\n\t\tlet winPath = tmpFile;\n\t\tif (useWslpath) {\n\t\t\tconst winPathResult = runCommand(\"wslpath\", [\"-w\", tmpFile], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });\n\t\t\tif (!winPathResult.ok) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\twinPath = winPathResult.stdout.toString(\"utf-8\").trim();\n\t\t\tif (!winPath) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\tconst psQuotedWinPath = winPath.replaceAll(\"'\", \"''\");\n\t\t// -STA: System.Windows.Forms.Clipboard::GetImage() is empty in MTA.\n\t\t// Prefer Get-Clipboard -Format Image (Win+Shift+S / VS Code PNG/DIB), then GetImage().\n\t\tconst psScript = [\n\t\t\t\"Add-Type -AssemblyName System.Windows.Forms\",\n\t\t\t\"Add-Type -AssemblyName System.Drawing\",\n\t\t\t`$path = '${psQuotedWinPath}'`,\n\t\t\t\"$img = $null\",\n\t\t\t\"try { $img = Get-Clipboard -Format Image } catch {}\",\n\t\t\t\"if (-not $img) { $img = [System.Windows.Forms.Clipboard]::GetImage() }\",\n\t\t\t\"if ($img) { $img.Save($path, [System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'ok' } else { Write-Output 'empty' }\",\n\t\t].join(\"; \");\n\n\t\tconst result = runCommand(\"powershell.exe\", [\"-NoProfile\", \"-STA\", \"-Command\", psScript], {\n\t\t\ttimeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS,\n\t\t});\n\t\tif (!result.ok) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst output = result.stdout.toString(\"utf-8\").trim();\n\t\tif (output !== \"ok\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst bytes = readFileSync(tmpFile);\n\t\tif (bytes.length === 0) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn { bytes: new Uint8Array(bytes), mimeType: \"image/png\" };\n\t} catch {\n\t\treturn null;\n\t} finally {\n\t\ttry {\n\t\t\tunlinkSync(tmpFile);\n\t\t} catch {\n\t\t\t// Ignore cleanup errors.\n\t\t}\n\t}\n}\n\nconst FILE_DROP_IMAGE_MIME_TYPES: Record<string, string> = {\n\t\".png\": \"image/png\",\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".webp\": \"image/webp\",\n\t\".gif\": \"image/gif\",\n\t\".bmp\": \"image/bmp\",\n};\n\n/**\n * On Windows, copying an image file in Explorer puts a file reference\n * (CF_HDROP) on the clipboard instead of image data, so `hasImage()` is false.\n * Return the first image file from the drop list. Only used on native Windows.\n */\nfunction readClipboardImageFileDropViaPowerShell(): ClipboardImage | null {\n\tconst result = runCommand(\"powershell.exe\", [\"-NoProfile\", \"-STA\", \"-Command\", \"Get-Clipboard -Format FileDropList\"], {\n\t\ttimeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS,\n\t});\n\tif (!result.ok || result.stdout.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst candidates = result.stdout\n\t\t.toString(\"utf-8\")\n\t\t.split(/\\r?\\n/)\n\t\t.map((p) => p.trim())\n\t\t.filter(Boolean);\n\n\tfor (const filePath of candidates) {\n\t\tconst mimeType = FILE_DROP_IMAGE_MIME_TYPES[extname(filePath).toLowerCase()];\n\t\tif (!mimeType) {\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stat = statSync(filePath);\n\t\t\tif (!stat.isFile() || stat.size === 0 || stat.size > DEFAULT_MAX_BUFFER_BYTES) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treturn { bytes: new Uint8Array(readFileSync(filePath)), mimeType };\n\t\t} catch {\n\t\t\t// Unreadable file; try the next candidate.\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction readClipboardImageViaXclip(): ClipboardImage | null {\n\tconst targets = runCommand(\"xclip\", [\"-selection\", \"clipboard\", \"-t\", \"TARGETS\", \"-o\"], {\n\t\ttimeoutMs: DEFAULT_LIST_TIMEOUT_MS,\n\t});\n\n\tlet candidateTypes: string[] = [];\n\tif (targets.ok) {\n\t\tcandidateTypes = targets.stdout\n\t\t\t.toString(\"utf-8\")\n\t\t\t.split(/\\r?\\n/)\n\t\t\t.map((t) => t.trim())\n\t\t\t.filter(Boolean);\n\t}\n\n\tconst preferred = candidateTypes.length > 0 ? selectPreferredImageMimeType(candidateTypes) : null;\n\tconst tryTypes = preferred ? [preferred, ...SUPPORTED_IMAGE_MIME_TYPES] : [...SUPPORTED_IMAGE_MIME_TYPES];\n\n\tfor (const mimeType of tryTypes) {\n\t\tconst data = runCommand(\"xclip\", [\"-selection\", \"clipboard\", \"-t\", mimeType, \"-o\"]);\n\t\tif (data.ok && data.stdout.length > 0) {\n\t\t\treturn { bytes: data.stdout, mimeType: baseMimeType(mimeType) };\n\t\t}\n\t}\n\n\treturn null;\n}\n\nasync function readClipboardImageViaNativeClipboard(): Promise<ClipboardImage | null> {\n\tif (!clipboard || !clipboard.hasImage()) {\n\t\treturn null;\n\t}\n\n\tconst imageData = await clipboard.getImageBinary();\n\tif (!imageData || imageData.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst bytes = imageData instanceof Uint8Array ? imageData : Uint8Array.from(imageData);\n\treturn { bytes, mimeType: \"image/png\" };\n}\n\nexport async function readClipboardImage(options?: {\n\tenv?: NodeJS.ProcessEnv;\n\tplatform?: NodeJS.Platform;\n}): Promise<ClipboardImage | null> {\n\tconst env = options?.env ?? process.env;\n\tconst platform = options?.platform ?? process.platform;\n\n\tif (env.TERMUX_VERSION) {\n\t\treturn null;\n\t}\n\n\tlet image: ClipboardImage | null = null;\n\n\tif (platform === \"linux\") {\n\t\tconst wsl = isWSL(env);\n\t\tconst wayland = isWaylandSession(env);\n\n\t\tif (wayland || wsl) {\n\t\t\timage = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip();\n\t\t}\n\n\t\tif (!image && wsl) {\n\t\t\timage = readClipboardImageViaPowerShell(true);\n\t\t}\n\n\t\tif (!image && !wayland) {\n\t\t\timage = (await readClipboardImageViaNativeClipboard()) ?? readClipboardImageViaXclip();\n\t\t}\n\t} else if (platform === \"win32\") {\n\t\t// Native module first; PowerShell covers a missing/failed native module and\n\t\t// file-drop clipboards (image copied as a file in Explorer).\n\t\timage =\n\t\t\t(await readClipboardImageViaNativeClipboard()) ??\n\t\t\treadClipboardImageViaPowerShell(false) ??\n\t\t\treadClipboardImageFileDropViaPowerShell();\n\t} else {\n\t\timage = await readClipboardImageViaNativeClipboard();\n\t}\n\n\tif (!image) {\n\t\treturn null;\n\t}\n\n\t// Convert unsupported formats (e.g., BMP from WSLg) to PNG\n\tif (!isSupportedImageMimeType(image.mimeType)) {\n\t\tconst pngBytes = await convertToPng(image.bytes);\n\t\tif (!pngBytes) {\n\t\t\treturn null;\n\t\t}\n\t\treturn { bytes: pngBytes, mimeType: \"image/png\" };\n\t}\n\n\treturn image;\n}\n"]}
|
|
@@ -142,14 +142,18 @@ function readClipboardImageViaPowerShell(useWslpath) {
|
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
144
|
const psQuotedWinPath = winPath.replaceAll("'", "''");
|
|
145
|
+
// -STA: System.Windows.Forms.Clipboard::GetImage() is empty in MTA.
|
|
146
|
+
// Prefer Get-Clipboard -Format Image (Win+Shift+S / VS Code PNG/DIB), then GetImage().
|
|
145
147
|
const psScript = [
|
|
146
148
|
"Add-Type -AssemblyName System.Windows.Forms",
|
|
147
149
|
"Add-Type -AssemblyName System.Drawing",
|
|
148
150
|
`$path = '${psQuotedWinPath}'`,
|
|
149
|
-
"$img =
|
|
151
|
+
"$img = $null",
|
|
152
|
+
"try { $img = Get-Clipboard -Format Image } catch {}",
|
|
153
|
+
"if (-not $img) { $img = [System.Windows.Forms.Clipboard]::GetImage() }",
|
|
150
154
|
"if ($img) { $img.Save($path, [System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'ok' } else { Write-Output 'empty' }",
|
|
151
155
|
].join("; ");
|
|
152
|
-
const result = runCommand("powershell.exe", ["-NoProfile", "-Command", psScript], {
|
|
156
|
+
const result = runCommand("powershell.exe", ["-NoProfile", "-STA", "-Command", psScript], {
|
|
153
157
|
timeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS,
|
|
154
158
|
});
|
|
155
159
|
if (!result.ok) {
|
|
@@ -191,7 +195,7 @@ const FILE_DROP_IMAGE_MIME_TYPES = {
|
|
|
191
195
|
* Return the first image file from the drop list. Only used on native Windows.
|
|
192
196
|
*/
|
|
193
197
|
function readClipboardImageFileDropViaPowerShell() {
|
|
194
|
-
const result = runCommand("powershell.exe", ["-NoProfile", "-Command", "Get-Clipboard -Format FileDropList"], {
|
|
198
|
+
const result = runCommand("powershell.exe", ["-NoProfile", "-STA", "-Command", "Get-Clipboard -Format FileDropList"], {
|
|
195
199
|
timeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS,
|
|
196
200
|
});
|
|
197
201
|
if (!result.ok || result.stdout.length === 0) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clipboard-image.js","sourceRoot":"","sources":["../../src/utils/clipboard-image.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from \"child_process\";\nimport { randomUUID } from \"crypto\";\nimport { readFileSync, statSync, unlinkSync } from \"fs\";\nimport { tmpdir } from \"os\";\nimport { extname, join } from \"path\";\n\nimport { clipboard } from \"./clipboard-native.ts\";\nimport { loadPhoton } from \"./photon.ts\";\n\nexport type ClipboardImage = {\n\tbytes: Uint8Array;\n\tmimeType: string;\n};\n\nconst SUPPORTED_IMAGE_MIME_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"] as const;\n\nconst DEFAULT_LIST_TIMEOUT_MS = 1000;\nconst DEFAULT_READ_TIMEOUT_MS = 3000;\nconst DEFAULT_POWERSHELL_TIMEOUT_MS = 5000;\nconst DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024;\n\nexport function isWaylandSession(env: NodeJS.ProcessEnv = process.env): boolean {\n\treturn Boolean(env.WAYLAND_DISPLAY) || env.XDG_SESSION_TYPE === \"wayland\";\n}\n\nfunction baseMimeType(mimeType: string): string {\n\treturn mimeType.split(\";\")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();\n}\n\nexport function extensionForImageMimeType(mimeType: string): string | null {\n\tswitch (baseMimeType(mimeType)) {\n\t\tcase \"image/png\":\n\t\t\treturn \"png\";\n\t\tcase \"image/jpeg\":\n\t\t\treturn \"jpg\";\n\t\tcase \"image/webp\":\n\t\t\treturn \"webp\";\n\t\tcase \"image/gif\":\n\t\t\treturn \"gif\";\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n\nfunction selectPreferredImageMimeType(mimeTypes: string[]): string | null {\n\tconst normalized = mimeTypes\n\t\t.map((t) => t.trim())\n\t\t.filter(Boolean)\n\t\t.map((t) => ({ raw: t, base: baseMimeType(t) }));\n\n\tfor (const preferred of SUPPORTED_IMAGE_MIME_TYPES) {\n\t\tconst match = normalized.find((t) => t.base === preferred);\n\t\tif (match) {\n\t\t\treturn match.raw;\n\t\t}\n\t}\n\n\tconst anyImage = normalized.find((t) => t.base.startsWith(\"image/\"));\n\treturn anyImage?.raw ?? null;\n}\n\nfunction isSupportedImageMimeType(mimeType: string): boolean {\n\tconst base = baseMimeType(mimeType);\n\treturn SUPPORTED_IMAGE_MIME_TYPES.some((t) => t === base);\n}\n\n/**\n * Convert unsupported image formats to PNG using Photon.\n * Returns null if conversion is unavailable or fails.\n */\nasync function convertToPng(bytes: Uint8Array): Promise<Uint8Array | null> {\n\tconst photon = await loadPhoton();\n\tif (!photon) {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst image = photon.PhotonImage.new_from_byteslice(bytes);\n\t\ttry {\n\t\t\treturn image.get_bytes();\n\t\t} finally {\n\t\t\timage.free();\n\t\t}\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction runCommand(\n\tcommand: string,\n\targs: string[],\n\toptions?: { timeoutMs?: number; maxBufferBytes?: number; env?: NodeJS.ProcessEnv },\n): { stdout: Buffer; ok: boolean } {\n\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS;\n\tconst maxBufferBytes = options?.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;\n\n\tconst result = spawnSync(command, args, {\n\t\ttimeout: timeoutMs,\n\t\tmaxBuffer: maxBufferBytes,\n\t\tenv: options?.env,\n\t});\n\n\tif (result.error) {\n\t\treturn { ok: false, stdout: Buffer.alloc(0) };\n\t}\n\n\tif (result.status !== 0) {\n\t\treturn { ok: false, stdout: Buffer.alloc(0) };\n\t}\n\n\tconst stdout = Buffer.isBuffer(result.stdout)\n\t\t? result.stdout\n\t\t: Buffer.from(result.stdout ?? \"\", typeof result.stdout === \"string\" ? \"utf-8\" : undefined);\n\n\treturn { ok: true, stdout };\n}\n\nfunction readClipboardImageViaWlPaste(): ClipboardImage | null {\n\tconst list = runCommand(\"wl-paste\", [\"--list-types\"], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });\n\tif (!list.ok) {\n\t\treturn null;\n\t}\n\n\tconst types = list.stdout\n\t\t.toString(\"utf-8\")\n\t\t.split(/\\r?\\n/)\n\t\t.map((t) => t.trim())\n\t\t.filter(Boolean);\n\n\tconst selectedType = selectPreferredImageMimeType(types);\n\tif (!selectedType) {\n\t\treturn null;\n\t}\n\n\tconst data = runCommand(\"wl-paste\", [\"--type\", selectedType, \"--no-newline\"]);\n\tif (!data.ok || data.stdout.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn { bytes: data.stdout, mimeType: baseMimeType(selectedType) };\n}\n\nfunction isWSL(env: NodeJS.ProcessEnv = process.env): boolean {\n\tif (env.WSL_DISTRO_NAME || env.WSLENV) {\n\t\treturn true;\n\t}\n\n\ttry {\n\t\tconst release = readFileSync(\"/proc/version\", \"utf-8\");\n\t\treturn /microsoft|wsl/i.test(release);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * On WSL, the Linux clipboard (Wayland/X11) does not receive image data from\n * Windows screenshots (Win+Shift+S). PowerShell can access the Windows clipboard\n * directly, so we use it as a fallback. On native Windows it doubles as a\n * fallback when the native clipboard module is unavailable or reports no image.\n */\nfunction readClipboardImageViaPowerShell(useWslpath: boolean): ClipboardImage | null {\n\tconst tmpFile = join(tmpdir(), `pi-clip-${randomUUID()}.png`);\n\n\ttry {\n\t\tlet winPath = tmpFile;\n\t\tif (useWslpath) {\n\t\t\tconst winPathResult = runCommand(\"wslpath\", [\"-w\", tmpFile], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });\n\t\t\tif (!winPathResult.ok) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\twinPath = winPathResult.stdout.toString(\"utf-8\").trim();\n\t\t\tif (!winPath) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\tconst psQuotedWinPath = winPath.replaceAll(\"'\", \"''\");\n\t\tconst psScript = [\n\t\t\t\"Add-Type -AssemblyName System.Windows.Forms\",\n\t\t\t\"Add-Type -AssemblyName System.Drawing\",\n\t\t\t`$path = '${psQuotedWinPath}'`,\n\t\t\t\"$img = [System.Windows.Forms.Clipboard]::GetImage()\",\n\t\t\t\"if ($img) { $img.Save($path, [System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'ok' } else { Write-Output 'empty' }\",\n\t\t].join(\"; \");\n\n\t\tconst result = runCommand(\"powershell.exe\", [\"-NoProfile\", \"-Command\", psScript], {\n\t\t\ttimeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS,\n\t\t});\n\t\tif (!result.ok) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst output = result.stdout.toString(\"utf-8\").trim();\n\t\tif (output !== \"ok\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst bytes = readFileSync(tmpFile);\n\t\tif (bytes.length === 0) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn { bytes: new Uint8Array(bytes), mimeType: \"image/png\" };\n\t} catch {\n\t\treturn null;\n\t} finally {\n\t\ttry {\n\t\t\tunlinkSync(tmpFile);\n\t\t} catch {\n\t\t\t// Ignore cleanup errors.\n\t\t}\n\t}\n}\n\nconst FILE_DROP_IMAGE_MIME_TYPES: Record<string, string> = {\n\t\".png\": \"image/png\",\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".webp\": \"image/webp\",\n\t\".gif\": \"image/gif\",\n\t\".bmp\": \"image/bmp\",\n};\n\n/**\n * On Windows, copying an image file in Explorer puts a file reference\n * (CF_HDROP) on the clipboard instead of image data, so `hasImage()` is false.\n * Return the first image file from the drop list. Only used on native Windows.\n */\nfunction readClipboardImageFileDropViaPowerShell(): ClipboardImage | null {\n\tconst result = runCommand(\"powershell.exe\", [\"-NoProfile\", \"-Command\", \"Get-Clipboard -Format FileDropList\"], {\n\t\ttimeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS,\n\t});\n\tif (!result.ok || result.stdout.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst candidates = result.stdout\n\t\t.toString(\"utf-8\")\n\t\t.split(/\\r?\\n/)\n\t\t.map((p) => p.trim())\n\t\t.filter(Boolean);\n\n\tfor (const filePath of candidates) {\n\t\tconst mimeType = FILE_DROP_IMAGE_MIME_TYPES[extname(filePath).toLowerCase()];\n\t\tif (!mimeType) {\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stat = statSync(filePath);\n\t\t\tif (!stat.isFile() || stat.size === 0 || stat.size > DEFAULT_MAX_BUFFER_BYTES) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treturn { bytes: new Uint8Array(readFileSync(filePath)), mimeType };\n\t\t} catch {\n\t\t\t// Unreadable file; try the next candidate.\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction readClipboardImageViaXclip(): ClipboardImage | null {\n\tconst targets = runCommand(\"xclip\", [\"-selection\", \"clipboard\", \"-t\", \"TARGETS\", \"-o\"], {\n\t\ttimeoutMs: DEFAULT_LIST_TIMEOUT_MS,\n\t});\n\n\tlet candidateTypes: string[] = [];\n\tif (targets.ok) {\n\t\tcandidateTypes = targets.stdout\n\t\t\t.toString(\"utf-8\")\n\t\t\t.split(/\\r?\\n/)\n\t\t\t.map((t) => t.trim())\n\t\t\t.filter(Boolean);\n\t}\n\n\tconst preferred = candidateTypes.length > 0 ? selectPreferredImageMimeType(candidateTypes) : null;\n\tconst tryTypes = preferred ? [preferred, ...SUPPORTED_IMAGE_MIME_TYPES] : [...SUPPORTED_IMAGE_MIME_TYPES];\n\n\tfor (const mimeType of tryTypes) {\n\t\tconst data = runCommand(\"xclip\", [\"-selection\", \"clipboard\", \"-t\", mimeType, \"-o\"]);\n\t\tif (data.ok && data.stdout.length > 0) {\n\t\t\treturn { bytes: data.stdout, mimeType: baseMimeType(mimeType) };\n\t\t}\n\t}\n\n\treturn null;\n}\n\nasync function readClipboardImageViaNativeClipboard(): Promise<ClipboardImage | null> {\n\tif (!clipboard || !clipboard.hasImage()) {\n\t\treturn null;\n\t}\n\n\tconst imageData = await clipboard.getImageBinary();\n\tif (!imageData || imageData.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst bytes = imageData instanceof Uint8Array ? imageData : Uint8Array.from(imageData);\n\treturn { bytes, mimeType: \"image/png\" };\n}\n\nexport async function readClipboardImage(options?: {\n\tenv?: NodeJS.ProcessEnv;\n\tplatform?: NodeJS.Platform;\n}): Promise<ClipboardImage | null> {\n\tconst env = options?.env ?? process.env;\n\tconst platform = options?.platform ?? process.platform;\n\n\tif (env.TERMUX_VERSION) {\n\t\treturn null;\n\t}\n\n\tlet image: ClipboardImage | null = null;\n\n\tif (platform === \"linux\") {\n\t\tconst wsl = isWSL(env);\n\t\tconst wayland = isWaylandSession(env);\n\n\t\tif (wayland || wsl) {\n\t\t\timage = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip();\n\t\t}\n\n\t\tif (!image && wsl) {\n\t\t\timage = readClipboardImageViaPowerShell(true);\n\t\t}\n\n\t\tif (!image && !wayland) {\n\t\t\timage = (await readClipboardImageViaNativeClipboard()) ?? readClipboardImageViaXclip();\n\t\t}\n\t} else if (platform === \"win32\") {\n\t\t// Native module first; PowerShell covers a missing/failed native module and\n\t\t// file-drop clipboards (image copied as a file in Explorer).\n\t\timage =\n\t\t\t(await readClipboardImageViaNativeClipboard()) ??\n\t\t\treadClipboardImageViaPowerShell(false) ??\n\t\t\treadClipboardImageFileDropViaPowerShell();\n\t} else {\n\t\timage = await readClipboardImageViaNativeClipboard();\n\t}\n\n\tif (!image) {\n\t\treturn null;\n\t}\n\n\t// Convert unsupported formats (e.g., BMP from WSLg) to PNG\n\tif (!isSupportedImageMimeType(image.mimeType)) {\n\t\tconst pngBytes = await convertToPng(image.bytes);\n\t\tif (!pngBytes) {\n\t\t\treturn null;\n\t\t}\n\t\treturn { bytes: pngBytes, mimeType: \"image/png\" };\n\t}\n\n\treturn image;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"clipboard-image.js","sourceRoot":"","sources":["../../src/utils/clipboard-image.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from \"child_process\";\nimport { randomUUID } from \"crypto\";\nimport { readFileSync, statSync, unlinkSync } from \"fs\";\nimport { tmpdir } from \"os\";\nimport { extname, join } from \"path\";\n\nimport { clipboard } from \"./clipboard-native.ts\";\nimport { loadPhoton } from \"./photon.ts\";\n\nexport type ClipboardImage = {\n\tbytes: Uint8Array;\n\tmimeType: string;\n};\n\nconst SUPPORTED_IMAGE_MIME_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\", \"image/gif\"] as const;\n\nconst DEFAULT_LIST_TIMEOUT_MS = 1000;\nconst DEFAULT_READ_TIMEOUT_MS = 3000;\nconst DEFAULT_POWERSHELL_TIMEOUT_MS = 5000;\nconst DEFAULT_MAX_BUFFER_BYTES = 50 * 1024 * 1024;\n\nexport function isWaylandSession(env: NodeJS.ProcessEnv = process.env): boolean {\n\treturn Boolean(env.WAYLAND_DISPLAY) || env.XDG_SESSION_TYPE === \"wayland\";\n}\n\nfunction baseMimeType(mimeType: string): string {\n\treturn mimeType.split(\";\")[0]?.trim().toLowerCase() ?? mimeType.toLowerCase();\n}\n\nexport function extensionForImageMimeType(mimeType: string): string | null {\n\tswitch (baseMimeType(mimeType)) {\n\t\tcase \"image/png\":\n\t\t\treturn \"png\";\n\t\tcase \"image/jpeg\":\n\t\t\treturn \"jpg\";\n\t\tcase \"image/webp\":\n\t\t\treturn \"webp\";\n\t\tcase \"image/gif\":\n\t\t\treturn \"gif\";\n\t\tdefault:\n\t\t\treturn null;\n\t}\n}\n\nfunction selectPreferredImageMimeType(mimeTypes: string[]): string | null {\n\tconst normalized = mimeTypes\n\t\t.map((t) => t.trim())\n\t\t.filter(Boolean)\n\t\t.map((t) => ({ raw: t, base: baseMimeType(t) }));\n\n\tfor (const preferred of SUPPORTED_IMAGE_MIME_TYPES) {\n\t\tconst match = normalized.find((t) => t.base === preferred);\n\t\tif (match) {\n\t\t\treturn match.raw;\n\t\t}\n\t}\n\n\tconst anyImage = normalized.find((t) => t.base.startsWith(\"image/\"));\n\treturn anyImage?.raw ?? null;\n}\n\nfunction isSupportedImageMimeType(mimeType: string): boolean {\n\tconst base = baseMimeType(mimeType);\n\treturn SUPPORTED_IMAGE_MIME_TYPES.some((t) => t === base);\n}\n\n/**\n * Convert unsupported image formats to PNG using Photon.\n * Returns null if conversion is unavailable or fails.\n */\nasync function convertToPng(bytes: Uint8Array): Promise<Uint8Array | null> {\n\tconst photon = await loadPhoton();\n\tif (!photon) {\n\t\treturn null;\n\t}\n\n\ttry {\n\t\tconst image = photon.PhotonImage.new_from_byteslice(bytes);\n\t\ttry {\n\t\t\treturn image.get_bytes();\n\t\t} finally {\n\t\t\timage.free();\n\t\t}\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction runCommand(\n\tcommand: string,\n\targs: string[],\n\toptions?: { timeoutMs?: number; maxBufferBytes?: number; env?: NodeJS.ProcessEnv },\n): { stdout: Buffer; ok: boolean } {\n\tconst timeoutMs = options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS;\n\tconst maxBufferBytes = options?.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;\n\n\tconst result = spawnSync(command, args, {\n\t\ttimeout: timeoutMs,\n\t\tmaxBuffer: maxBufferBytes,\n\t\tenv: options?.env,\n\t});\n\n\tif (result.error) {\n\t\treturn { ok: false, stdout: Buffer.alloc(0) };\n\t}\n\n\tif (result.status !== 0) {\n\t\treturn { ok: false, stdout: Buffer.alloc(0) };\n\t}\n\n\tconst stdout = Buffer.isBuffer(result.stdout)\n\t\t? result.stdout\n\t\t: Buffer.from(result.stdout ?? \"\", typeof result.stdout === \"string\" ? \"utf-8\" : undefined);\n\n\treturn { ok: true, stdout };\n}\n\nfunction readClipboardImageViaWlPaste(): ClipboardImage | null {\n\tconst list = runCommand(\"wl-paste\", [\"--list-types\"], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });\n\tif (!list.ok) {\n\t\treturn null;\n\t}\n\n\tconst types = list.stdout\n\t\t.toString(\"utf-8\")\n\t\t.split(/\\r?\\n/)\n\t\t.map((t) => t.trim())\n\t\t.filter(Boolean);\n\n\tconst selectedType = selectPreferredImageMimeType(types);\n\tif (!selectedType) {\n\t\treturn null;\n\t}\n\n\tconst data = runCommand(\"wl-paste\", [\"--type\", selectedType, \"--no-newline\"]);\n\tif (!data.ok || data.stdout.length === 0) {\n\t\treturn null;\n\t}\n\n\treturn { bytes: data.stdout, mimeType: baseMimeType(selectedType) };\n}\n\nfunction isWSL(env: NodeJS.ProcessEnv = process.env): boolean {\n\tif (env.WSL_DISTRO_NAME || env.WSLENV) {\n\t\treturn true;\n\t}\n\n\ttry {\n\t\tconst release = readFileSync(\"/proc/version\", \"utf-8\");\n\t\treturn /microsoft|wsl/i.test(release);\n\t} catch {\n\t\treturn false;\n\t}\n}\n\n/**\n * On WSL, the Linux clipboard (Wayland/X11) does not receive image data from\n * Windows screenshots (Win+Shift+S). PowerShell can access the Windows clipboard\n * directly, so we use it as a fallback. On native Windows it doubles as a\n * fallback when the native clipboard module is unavailable or reports no image.\n */\nfunction readClipboardImageViaPowerShell(useWslpath: boolean): ClipboardImage | null {\n\tconst tmpFile = join(tmpdir(), `pi-clip-${randomUUID()}.png`);\n\n\ttry {\n\t\tlet winPath = tmpFile;\n\t\tif (useWslpath) {\n\t\t\tconst winPathResult = runCommand(\"wslpath\", [\"-w\", tmpFile], { timeoutMs: DEFAULT_LIST_TIMEOUT_MS });\n\t\t\tif (!winPathResult.ok) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\twinPath = winPathResult.stdout.toString(\"utf-8\").trim();\n\t\t\tif (!winPath) {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t}\n\n\t\tconst psQuotedWinPath = winPath.replaceAll(\"'\", \"''\");\n\t\t// -STA: System.Windows.Forms.Clipboard::GetImage() is empty in MTA.\n\t\t// Prefer Get-Clipboard -Format Image (Win+Shift+S / VS Code PNG/DIB), then GetImage().\n\t\tconst psScript = [\n\t\t\t\"Add-Type -AssemblyName System.Windows.Forms\",\n\t\t\t\"Add-Type -AssemblyName System.Drawing\",\n\t\t\t`$path = '${psQuotedWinPath}'`,\n\t\t\t\"$img = $null\",\n\t\t\t\"try { $img = Get-Clipboard -Format Image } catch {}\",\n\t\t\t\"if (-not $img) { $img = [System.Windows.Forms.Clipboard]::GetImage() }\",\n\t\t\t\"if ($img) { $img.Save($path, [System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'ok' } else { Write-Output 'empty' }\",\n\t\t].join(\"; \");\n\n\t\tconst result = runCommand(\"powershell.exe\", [\"-NoProfile\", \"-STA\", \"-Command\", psScript], {\n\t\t\ttimeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS,\n\t\t});\n\t\tif (!result.ok) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst output = result.stdout.toString(\"utf-8\").trim();\n\t\tif (output !== \"ok\") {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst bytes = readFileSync(tmpFile);\n\t\tif (bytes.length === 0) {\n\t\t\treturn null;\n\t\t}\n\n\t\treturn { bytes: new Uint8Array(bytes), mimeType: \"image/png\" };\n\t} catch {\n\t\treturn null;\n\t} finally {\n\t\ttry {\n\t\t\tunlinkSync(tmpFile);\n\t\t} catch {\n\t\t\t// Ignore cleanup errors.\n\t\t}\n\t}\n}\n\nconst FILE_DROP_IMAGE_MIME_TYPES: Record<string, string> = {\n\t\".png\": \"image/png\",\n\t\".jpg\": \"image/jpeg\",\n\t\".jpeg\": \"image/jpeg\",\n\t\".webp\": \"image/webp\",\n\t\".gif\": \"image/gif\",\n\t\".bmp\": \"image/bmp\",\n};\n\n/**\n * On Windows, copying an image file in Explorer puts a file reference\n * (CF_HDROP) on the clipboard instead of image data, so `hasImage()` is false.\n * Return the first image file from the drop list. Only used on native Windows.\n */\nfunction readClipboardImageFileDropViaPowerShell(): ClipboardImage | null {\n\tconst result = runCommand(\"powershell.exe\", [\"-NoProfile\", \"-STA\", \"-Command\", \"Get-Clipboard -Format FileDropList\"], {\n\t\ttimeoutMs: DEFAULT_POWERSHELL_TIMEOUT_MS,\n\t});\n\tif (!result.ok || result.stdout.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst candidates = result.stdout\n\t\t.toString(\"utf-8\")\n\t\t.split(/\\r?\\n/)\n\t\t.map((p) => p.trim())\n\t\t.filter(Boolean);\n\n\tfor (const filePath of candidates) {\n\t\tconst mimeType = FILE_DROP_IMAGE_MIME_TYPES[extname(filePath).toLowerCase()];\n\t\tif (!mimeType) {\n\t\t\tcontinue;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stat = statSync(filePath);\n\t\t\tif (!stat.isFile() || stat.size === 0 || stat.size > DEFAULT_MAX_BUFFER_BYTES) {\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\treturn { bytes: new Uint8Array(readFileSync(filePath)), mimeType };\n\t\t} catch {\n\t\t\t// Unreadable file; try the next candidate.\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction readClipboardImageViaXclip(): ClipboardImage | null {\n\tconst targets = runCommand(\"xclip\", [\"-selection\", \"clipboard\", \"-t\", \"TARGETS\", \"-o\"], {\n\t\ttimeoutMs: DEFAULT_LIST_TIMEOUT_MS,\n\t});\n\n\tlet candidateTypes: string[] = [];\n\tif (targets.ok) {\n\t\tcandidateTypes = targets.stdout\n\t\t\t.toString(\"utf-8\")\n\t\t\t.split(/\\r?\\n/)\n\t\t\t.map((t) => t.trim())\n\t\t\t.filter(Boolean);\n\t}\n\n\tconst preferred = candidateTypes.length > 0 ? selectPreferredImageMimeType(candidateTypes) : null;\n\tconst tryTypes = preferred ? [preferred, ...SUPPORTED_IMAGE_MIME_TYPES] : [...SUPPORTED_IMAGE_MIME_TYPES];\n\n\tfor (const mimeType of tryTypes) {\n\t\tconst data = runCommand(\"xclip\", [\"-selection\", \"clipboard\", \"-t\", mimeType, \"-o\"]);\n\t\tif (data.ok && data.stdout.length > 0) {\n\t\t\treturn { bytes: data.stdout, mimeType: baseMimeType(mimeType) };\n\t\t}\n\t}\n\n\treturn null;\n}\n\nasync function readClipboardImageViaNativeClipboard(): Promise<ClipboardImage | null> {\n\tif (!clipboard || !clipboard.hasImage()) {\n\t\treturn null;\n\t}\n\n\tconst imageData = await clipboard.getImageBinary();\n\tif (!imageData || imageData.length === 0) {\n\t\treturn null;\n\t}\n\n\tconst bytes = imageData instanceof Uint8Array ? imageData : Uint8Array.from(imageData);\n\treturn { bytes, mimeType: \"image/png\" };\n}\n\nexport async function readClipboardImage(options?: {\n\tenv?: NodeJS.ProcessEnv;\n\tplatform?: NodeJS.Platform;\n}): Promise<ClipboardImage | null> {\n\tconst env = options?.env ?? process.env;\n\tconst platform = options?.platform ?? process.platform;\n\n\tif (env.TERMUX_VERSION) {\n\t\treturn null;\n\t}\n\n\tlet image: ClipboardImage | null = null;\n\n\tif (platform === \"linux\") {\n\t\tconst wsl = isWSL(env);\n\t\tconst wayland = isWaylandSession(env);\n\n\t\tif (wayland || wsl) {\n\t\t\timage = readClipboardImageViaWlPaste() ?? readClipboardImageViaXclip();\n\t\t}\n\n\t\tif (!image && wsl) {\n\t\t\timage = readClipboardImageViaPowerShell(true);\n\t\t}\n\n\t\tif (!image && !wayland) {\n\t\t\timage = (await readClipboardImageViaNativeClipboard()) ?? readClipboardImageViaXclip();\n\t\t}\n\t} else if (platform === \"win32\") {\n\t\t// Native module first; PowerShell covers a missing/failed native module and\n\t\t// file-drop clipboards (image copied as a file in Explorer).\n\t\timage =\n\t\t\t(await readClipboardImageViaNativeClipboard()) ??\n\t\t\treadClipboardImageViaPowerShell(false) ??\n\t\t\treadClipboardImageFileDropViaPowerShell();\n\t} else {\n\t\timage = await readClipboardImageViaNativeClipboard();\n\t}\n\n\tif (!image) {\n\t\treturn null;\n\t}\n\n\t// Convert unsupported formats (e.g., BMP from WSLg) to PNG\n\tif (!isSupportedImageMimeType(image.mimeType)) {\n\t\tconst pngBytes = await convertToPng(image.bytes);\n\t\tif (!pngBytes) {\n\t\t\treturn null;\n\t\t}\n\t\treturn { bytes: pngBytes, mimeType: \"image/png\" };\n\t}\n\n\treturn image;\n}\n"]}
|