@hncode/hncode 0.4.2 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -15
- package/README_CN.md +1 -15
- package/bin/hncode +22 -9
- package/package.json +3 -2
- package/src/agent.js +149 -35
- package/src/bootstrap.js +56 -0
- package/src/btw-panel.js +224 -0
- package/src/cache.js +3 -2
- package/src/cell-buffer.js +173 -0
- package/src/ci.js +4 -1
- package/src/clipboard.js +202 -43
- package/src/colors.js +478 -242
- package/src/config.js +409 -40
- package/src/context-bar.js +237 -0
- package/src/doctor.js +518 -0
- package/src/experiments.js +122 -0
- package/src/file-history.js +112 -0
- package/src/file-tree.js +255 -0
- package/src/flush-tick.js +56 -0
- package/src/fork.js +44 -0
- package/src/fx.js +148 -0
- package/src/index.js +123 -11
- package/src/keybindings.js +229 -0
- package/src/llm.js +355 -34
- package/src/lsp.js +401 -0
- package/src/memory-profile.js +247 -0
- package/src/notebook.js +232 -0
- package/src/output-styles.js +183 -0
- package/src/patch.js +248 -0
- package/src/permissions.js +130 -1
- package/src/plan.js +3 -2
- package/src/plugin.js +611 -58
- package/src/prompt-presets.js +1 -1
- package/src/registry-browser.js +31 -13
- package/src/remote.js +20 -2
- package/src/rollout.js +151 -0
- package/src/row-cache.js +148 -0
- package/src/schedule.js +294 -0
- package/src/session-approvals.js +86 -0
- package/src/session-hub.js +1 -0
- package/src/session.js +178 -8
- package/src/settings-schema.js +301 -0
- package/src/shell-safety.js +473 -0
- package/src/side-thread.js +240 -0
- package/src/skills.js +27 -0
- package/src/stream-buffer.js +350 -0
- package/src/subagent-control.js +160 -0
- package/src/teleport.js +260 -0
- package/src/term-caps.js +411 -0
- package/src/term.js +283 -8
- package/src/tool-result.js +264 -236
- package/src/tools/agent-control.js +98 -0
- package/src/tools/agent-swarm.js +11 -2
- package/src/tools/agent.js +35 -1
- package/src/tools/diagnostics.js +175 -0
- package/src/tools/edit.js +25 -10
- package/src/tools/grep.js +62 -20
- package/src/tools/ignore.js +38 -14
- package/src/tools/index.js +10 -0
- package/src/tools/notebook.js +113 -0
- package/src/tools/read-media-file.js +10 -3
- package/src/tools/shell.js +34 -6
- package/src/tui.js +5637 -746
- package/src/web-public/app.js +7 -0
- package/src/web.js +3 -1
package/src/clipboard.js
CHANGED
|
@@ -7,6 +7,27 @@
|
|
|
7
7
|
// on an already-running shell costs ~100-230ms instead of seconds.
|
|
8
8
|
// If the helper is unusable, fall back to a one-shot PowerShell.
|
|
9
9
|
// Non-Windows paths (pbcopy/pbpaste/xclip) are cheap enough to run per call.
|
|
10
|
+
//
|
|
11
|
+
// THREE THINGS MADE THE FIRST PASTE FAIL, all measured on the shipping code:
|
|
12
|
+
// * The helper was "ready" the instant `spawn` returned, but the shell needs
|
|
13
|
+
// ~2-3s before it can answer anything. A read in that window was sent to a
|
|
14
|
+
// shell that had not started reading stdin yet, so it burned `helperRun`'s
|
|
15
|
+
// full 3s deadline and then fell through to the cold one-shot.
|
|
16
|
+
// * Worse, the warm sequence's sentinel was written WITHOUT a queued resolver
|
|
17
|
+
// ("so it drains from the buffer"). When a read pushed its resolver before
|
|
18
|
+
// that sentinel came back, the SENTINEL consumed the read's slot and resolved
|
|
19
|
+
// it with its own empty output — so the paste reported "Clipboard empty or
|
|
20
|
+
// unavailable" while the real answer (with the actual clipboard content) was
|
|
21
|
+
// dropped on the floor. Reproduced deliberately: the read got "", and the
|
|
22
|
+
// payload arrived afterwards with nobody left to receive it.
|
|
23
|
+
// * The fallback ran `powershell` (5.1) with an 8s timeout. Loading
|
|
24
|
+
// WinForms + Drawing there measured 3.8s idle and 11.5s on a busy machine —
|
|
25
|
+
// past the timeout, so `execSync` threw and the paste reported "unavailable"
|
|
26
|
+
// for a clipboard that was fine. `pwsh` does the same work in 1.9s.
|
|
27
|
+
// The warm-up also used to spawn a THROWAWAY shell (`pwsh -Command exit`) through
|
|
28
|
+
// `execSync` to decide which exe to use, blocking the TUI's startup path for ~1.9s
|
|
29
|
+
// before its first paint. The exe is discovered from the spawn's own `error` event
|
|
30
|
+
// now, so nothing blocks.
|
|
10
31
|
|
|
11
32
|
import cp from 'node:child_process';
|
|
12
33
|
import fs from 'node:fs';
|
|
@@ -16,8 +37,23 @@ import path from 'node:path';
|
|
|
16
37
|
const SENT = '@@HNCODE_CLIP@@';
|
|
17
38
|
let helper = null; // long-lived PowerShell child
|
|
18
39
|
let helperBuf = ''; // stdout accumulated since the last sentinel
|
|
19
|
-
let helperQueue = []; // pending read() resolvers
|
|
40
|
+
let helperQueue = []; // pending read() resolvers, ONE per sentinel written
|
|
20
41
|
let helperBusy = false; // one command in flight at a time
|
|
42
|
+
let helperReady = false; // the warm sequence has answered: the shell can take commands
|
|
43
|
+
|
|
44
|
+
/** How long a read will wait for the helper to finish warming. It is already coming
|
|
45
|
+
* up, and waiting for a ~10ms answer beats the multi-second cold fallback. */
|
|
46
|
+
const HELPER_READY_MS = 6000;
|
|
47
|
+
|
|
48
|
+
/** Deadline for ONE command on a warm shell. Only reached when the shell has already
|
|
49
|
+
* answered, so it is a hang detector, not a warm-up budget. */
|
|
50
|
+
const HELPER_CMD_MS = 3000;
|
|
51
|
+
|
|
52
|
+
/** Deadline for the cold one-shot fallback. Raised from 8s: loading WinForms and
|
|
53
|
+
* Drawing in powershell 5.1 measured 11.5s under load, and the old timeout turned
|
|
54
|
+
* that into a spurious "clipboard unavailable". This is the last resort, so it is
|
|
55
|
+
* allowed to be slow — failing here helps nobody. */
|
|
56
|
+
const ONESHOT_MS = 20000;
|
|
21
57
|
|
|
22
58
|
function win() { return process.platform === 'win32'; }
|
|
23
59
|
|
|
@@ -39,26 +75,95 @@ export function copyText(text) {
|
|
|
39
75
|
return true;
|
|
40
76
|
}
|
|
41
77
|
|
|
78
|
+
// Which PowerShell the helper runs. `pwsh` (7) starts far faster than the built-in
|
|
79
|
+
// `powershell` (5.1) — measured 1.9s vs 11.5s for the same image probe — and the image
|
|
80
|
+
// path needs the WinForms/Drawing assemblies. This is only the PREFERRED name: it is
|
|
81
|
+
// resolved from the spawn's own `error` event (see startHelper), so discovering that a
|
|
82
|
+
// box has no pwsh costs nothing instead of blocking the TUI's startup path with a
|
|
83
|
+
// throwaway shell.
|
|
84
|
+
let _psExe = 'pwsh';
|
|
85
|
+
|
|
86
|
+
/** One-shot PowerShell, preferring the exe the helper resolved. Returns stdout, or
|
|
87
|
+
* null when neither exe could answer. A machine without `pwsh` has to be able to
|
|
88
|
+
* fall back WITHOUT the startup-time probe that used to decide this. */
|
|
89
|
+
function runPowershell(args, opts) {
|
|
90
|
+
const tried = [];
|
|
91
|
+
for (const exe of [_psExe, 'powershell']) {
|
|
92
|
+
if (tried.includes(exe)) continue;
|
|
93
|
+
tried.push(exe);
|
|
94
|
+
try { return cp.execSync(`${exe} ${args}`, opts); } catch { /* try the next exe */ }
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
42
99
|
// ------------------------------------------------------- PowerShell helper
|
|
43
100
|
export function warmClipboard() {
|
|
44
101
|
if (!win() || helper) return;
|
|
102
|
+
startHelper(_psExe);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Spawn the long-lived shell. `exe` is tried first; if that exe does not exist,
|
|
106
|
+
* the spawn reports ENOENT on its `error` event and this retries with 5.1. */
|
|
107
|
+
function startHelper(exe) {
|
|
45
108
|
try {
|
|
46
|
-
helper = cp.spawn(
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
109
|
+
helper = cp.spawn(exe, ['-NoProfile', '-NoLogo', '-Command', '-'], { windowsHide: true, stdio: ['pipe', 'pipe', 'ignore'] });
|
|
110
|
+
} catch { helper = null; return; }
|
|
111
|
+
helperReady = false;
|
|
112
|
+
helper.on('error', () => {
|
|
113
|
+
const missing = helper;
|
|
114
|
+
dropHelper();
|
|
115
|
+
// `pwsh` absent (or unusable): fall back to the built-in shell once.
|
|
116
|
+
if (exe !== 'powershell' && missing) { _psExe = 'powershell'; warmClipboard(); }
|
|
117
|
+
});
|
|
118
|
+
helper.on('exit', () => dropHelper());
|
|
119
|
+
helper.stdout.on('data', (d) => {
|
|
120
|
+
helperBuf += d.toString('binary');
|
|
121
|
+
let i;
|
|
122
|
+
while ((i = helperBuf.indexOf(SENT)) >= 0) {
|
|
123
|
+
const chunk = helperBuf.slice(0, i);
|
|
124
|
+
helperBuf = helperBuf.slice(i + SENT.length);
|
|
125
|
+
const w = helperQueue.shift();
|
|
126
|
+
helperBusy = false;
|
|
127
|
+
if (w) w(chunk);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
helper.stdin.write('$ProgressPreference = "SilentlyContinue"\n');
|
|
131
|
+
// Pre-load the image assemblies ONCE, at warm time. Loading them lazily inside
|
|
132
|
+
// the probe cost ~3.6s on 5.1 — longer than the probe timeout — so the FIRST
|
|
133
|
+
// image paste always failed. Doing it up front moves that cost off the paste
|
|
134
|
+
// path entirely (the shell is warmed at TUI start, not on a keypress).
|
|
135
|
+
//
|
|
136
|
+
// This sentinel is QUEUED like every other command answer, and that is the whole
|
|
137
|
+
// point: written without a resolver it used to be claimed by whatever read pushed
|
|
138
|
+
// its resolver first, which handed that read THIS empty output and dropped the
|
|
139
|
+
// read's own answer — the first paste after startup reported "Clipboard empty or
|
|
140
|
+
// unavailable" for a clipboard that had content. The queued resolver here is what
|
|
141
|
+
// marks the shell as ready, so one sentinel still has exactly one owner.
|
|
142
|
+
helperQueue.push(() => { helperReady = true; });
|
|
143
|
+
helper.stdin.write("try { Add-Type -AssemblyName System.Windows.Forms; Add-Type -AssemblyName System.Drawing } catch {}\n'" + SENT + "'\n");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Tear down the helper and release every waiter. A read must never be left hanging on
|
|
147
|
+
* a shell that has died — it has a cold fallback to get to. */
|
|
148
|
+
function dropHelper() {
|
|
149
|
+
helper = null;
|
|
150
|
+
helperBusy = false;
|
|
151
|
+
helperReady = false;
|
|
152
|
+
helperBuf = '';
|
|
153
|
+
const waiting = helperQueue;
|
|
154
|
+
helperQueue = [];
|
|
155
|
+
for (const w of waiting) w(null);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Resolve once the shell can take commands, or false when it never got there. */
|
|
159
|
+
async function waitHelperReady(ms) {
|
|
160
|
+
const deadline = Date.now() + ms;
|
|
161
|
+
while (Date.now() < deadline) {
|
|
162
|
+
if (helperReady) return true;
|
|
163
|
+
if (!helper) return false;
|
|
164
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
165
|
+
}
|
|
166
|
+
return helperReady;
|
|
62
167
|
}
|
|
63
168
|
|
|
64
169
|
function helperRun(script, timeoutMs) {
|
|
@@ -78,10 +183,11 @@ function helperRun(script, timeoutMs) {
|
|
|
78
183
|
// text we read base64 ASCII and decode it ourselves.
|
|
79
184
|
function readOnce() {
|
|
80
185
|
if (win()) {
|
|
81
|
-
const b64 =
|
|
82
|
-
'
|
|
83
|
-
{ encoding: 'ascii', windowsHide: true, timeout:
|
|
186
|
+
const b64 = runPowershell(
|
|
187
|
+
'-NoProfile -Command "[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes((Get-Clipboard -Raw) + \'\'))"',
|
|
188
|
+
{ encoding: 'ascii', windowsHide: true, timeout: ONESHOT_MS },
|
|
84
189
|
);
|
|
190
|
+
if (b64 == null) throw new Error('no powershell');
|
|
85
191
|
const s = String(b64).replace(/[^A-Za-z0-9+/=]/g, '');
|
|
86
192
|
return Buffer.from(s, 'base64').toString('utf8');
|
|
87
193
|
}
|
|
@@ -91,15 +197,20 @@ function readOnce() {
|
|
|
91
197
|
|
|
92
198
|
// Async read: uses the warm helper when available. Returns { text, via } so the
|
|
93
199
|
// caller can tell the user which path answered.
|
|
94
|
-
export async function readText(timeoutMs =
|
|
95
|
-
if (win()
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
200
|
+
export async function readText(timeoutMs = HELPER_CMD_MS) {
|
|
201
|
+
if (win()) {
|
|
202
|
+
warmClipboard();
|
|
203
|
+
// Same readiness gate as the probe: a read sent to a shell that has not finished
|
|
204
|
+
// warming is answered by nothing, and the cold path is the expensive one.
|
|
205
|
+
if (helper && await waitHelperReady(HELPER_READY_MS)) {
|
|
206
|
+
helperBusy = true;
|
|
207
|
+
const out = await helperRun('[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes((Get-Clipboard -Raw) + ""))', timeoutMs);
|
|
208
|
+
if (out != null) {
|
|
209
|
+
const b64 = out.replace(/\r/g, '').split('\n').filter((l) => l.trim()).pop() || '';
|
|
210
|
+
try { return { text: Buffer.from(b64.trim(), 'base64').toString('utf8'), via: 'helper' }; } catch {}
|
|
211
|
+
}
|
|
212
|
+
helperBusy = false;
|
|
101
213
|
}
|
|
102
|
-
helperBusy = false;
|
|
103
214
|
}
|
|
104
215
|
try { return { text: readOnce(), via: 'oneshot' }; } catch { return { text: '', via: 'none' }; }
|
|
105
216
|
}
|
|
@@ -125,13 +236,21 @@ export async function readText(timeoutMs = 3000) {
|
|
|
125
236
|
// PowerShell expression (silently returned nothing), which made FILE pastes fall
|
|
126
237
|
// back to the slow cold spawn. Emitting one F:: line per file is simpler and
|
|
127
238
|
// also supports multi-file copies.
|
|
239
|
+
//
|
|
240
|
+
// The image branch uses the .NET WinForms clipboard, NOT `Get-Clipboard -Image`:
|
|
241
|
+
// that parameter only exists on some builds (and is absent even from the pwsh 7
|
|
242
|
+
// in use here), so it silently failed everywhere and image paste never worked.
|
|
243
|
+
// `[Windows.Forms.Clipboard]::GetImage()` exists on Windows PowerShell 5.1 AND
|
|
244
|
+
// pwsh 7 — verified on 5.1 — so it is the portable way to read a copied image.
|
|
128
245
|
const CLIP_PROBE = [
|
|
129
246
|
"$ErrorActionPreference='SilentlyContinue'",
|
|
130
247
|
"$f=Get-Clipboard -Format FileDropList",
|
|
131
248
|
"if($f -and $f.Count -gt 0){$f|ForEach-Object{'F::'+$_.FullName};return}",
|
|
132
249
|
"$t=Get-Clipboard -Raw",
|
|
133
250
|
"if($t){'T::'+[Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($t));return}",
|
|
134
|
-
|
|
251
|
+
// The assemblies are loaded once by warmClipboard(); no Add-Type here, so the
|
|
252
|
+
// probe stays fast even the first time an image is on the clipboard.
|
|
253
|
+
"$i=[System.Windows.Forms.Clipboard]::GetImage()",
|
|
135
254
|
"if($null -ne $i){$p=Join-Path $env:TEMP ('hncode-paste-'+[guid]::NewGuid().ToString('N')+'.png');$i.Save($p,[System.Drawing.Imaging.ImageFormat]::Png);'I::'+$p}",
|
|
136
255
|
].join('; ');
|
|
137
256
|
|
|
@@ -142,7 +261,7 @@ function parseClipResult(out) {
|
|
|
142
261
|
const fileLines = lines.filter((l) => l.startsWith('F::'));
|
|
143
262
|
if (fileLines.length) return { text: fileLines.map((l) => l.slice(3).trim()).join('\n'), via: 'files' };
|
|
144
263
|
const imgLine = lines.find((l) => l.startsWith('I::'));
|
|
145
|
-
if (imgLine)
|
|
264
|
+
if (imgLine) { const p = imgLine.slice(3).trim(); trackPastedImage(p); return { text: p, via: 'image' }; }
|
|
146
265
|
const txtLine = lines.find((l) => l.startsWith('T::'));
|
|
147
266
|
if (txtLine) {
|
|
148
267
|
try { return { text: Buffer.from(txtLine.slice(3).trim(), 'base64').toString('utf8'), via: 'text' }; }
|
|
@@ -151,18 +270,53 @@ function parseClipResult(out) {
|
|
|
151
270
|
return null;
|
|
152
271
|
}
|
|
153
272
|
|
|
273
|
+
// ---- pasted-image lifecycle ------------------------------------------------
|
|
274
|
+
// A pasted image is written to a temp .png (the model needs a PATH to hand to
|
|
275
|
+
// ReadMediaFile), so something has to remove it again or the temp dir fills up
|
|
276
|
+
// one screenshot at a time. Every path the probe reports is remembered here and
|
|
277
|
+
// the TUI deletes the whole set on exit — see cleanupPastedImages().
|
|
278
|
+
const pastedImages = new Set();
|
|
279
|
+
|
|
280
|
+
function trackPastedImage(p) {
|
|
281
|
+
if (p) pastedImages.add(p);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Delete every image this process wrote to a temp file. Safe to call from a
|
|
286
|
+
* `process.on('exit')` handler: it is fully synchronous and swallows errors (a
|
|
287
|
+
* locked file must not turn a clean exit into a failed one).
|
|
288
|
+
* Returns the number of files removed.
|
|
289
|
+
*/
|
|
290
|
+
export function cleanupPastedImages() {
|
|
291
|
+
let removed = 0;
|
|
292
|
+
for (const p of pastedImages) {
|
|
293
|
+
try { fs.rmSync(p, { force: true }); removed++; } catch { /* locked/missing: ignore */ }
|
|
294
|
+
}
|
|
295
|
+
pastedImages.clear();
|
|
296
|
+
return removed;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** How many pasted images are still on disk (diagnostics/tests). */
|
|
300
|
+
export function pastedImageCount() { return pastedImages.size; }
|
|
301
|
+
|
|
154
302
|
// Run the probe on the warm helper (fast path). Resolves null if it is not ready.
|
|
155
303
|
async function readViaHelper() {
|
|
156
304
|
warmClipboard();
|
|
157
305
|
if (!helper) return null;
|
|
158
|
-
// Wait
|
|
159
|
-
// so
|
|
306
|
+
// Wait for the shell to finish warming FIRST. The warm sequence is already in
|
|
307
|
+
// flight, so this waits for something that is going to happen anyway; sending the
|
|
308
|
+
// probe early meant it was answered by nobody, which cost the full command
|
|
309
|
+
// deadline and then fell through to the cold one-shot — several seconds for a
|
|
310
|
+
// clipboard the helper could have read in ~10ms.
|
|
311
|
+
if (!helperReady && !await waitHelperReady(HELPER_READY_MS)) return null;
|
|
312
|
+
// Then wait briefly for any in-flight command (e.g. a concurrent readText) to
|
|
313
|
+
// drain, so we still hit the warm shell instead of falling back to a cold spawn.
|
|
160
314
|
for (let i = 0; i < 20 && helperBusy; i++) {
|
|
161
315
|
await new Promise((r) => setTimeout(r, 25));
|
|
162
316
|
}
|
|
163
|
-
if (helperBusy) return null;
|
|
317
|
+
if (helperBusy || !helper) return null;
|
|
164
318
|
helperBusy = true;
|
|
165
|
-
const out = await helperRun(CLIP_PROBE,
|
|
319
|
+
const out = await helperRun(CLIP_PROBE, HELPER_CMD_MS);
|
|
166
320
|
return out == null ? null : parseClipResult(out);
|
|
167
321
|
}
|
|
168
322
|
|
|
@@ -171,16 +325,18 @@ export function readClipboardContent() {
|
|
|
171
325
|
try { return { text: readOnce(), via: 'oneshot' }; } catch { return { text: '', via: 'none' }; }
|
|
172
326
|
}
|
|
173
327
|
// Synchronous API is required by the caller, so use a one-shot PowerShell here
|
|
174
|
-
// and let the async path (readClipboardContentAsync) use the warm helper.
|
|
328
|
+
// and let the async path (readClipboardContentAsync) use the warm helper. The exe
|
|
329
|
+
// is the one the helper resolved — `powershell` 5.1 measured 11.5s on the same
|
|
330
|
+
// probe that `pwsh` did in 1.9s, and the old 8s deadline turned that into a
|
|
331
|
+
// spurious "clipboard unavailable".
|
|
175
332
|
const scriptFile = clipScriptPath();
|
|
176
333
|
if (!scriptFile) return { text: '', via: 'none' };
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
} catch { return { text: '', via: 'none' }; }
|
|
334
|
+
const out = runPowershell(
|
|
335
|
+
`-NoProfile -ExecutionPolicy Bypass -File "${scriptFile}"`,
|
|
336
|
+
{ encoding: 'utf8', windowsHide: true, timeout: ONESHOT_MS },
|
|
337
|
+
);
|
|
338
|
+
if (out == null) return { text: '', via: 'none' };
|
|
339
|
+
return parseClipResult(out) || { text: '', via: 'none' };
|
|
184
340
|
}
|
|
185
341
|
|
|
186
342
|
// Async version that prefers the warm helper (~10ms) over a cold spawn (~2.9s).
|
|
@@ -206,7 +362,10 @@ function clipScriptPath() {
|
|
|
206
362
|
'if ($files -and $files.Count -gt 0) { $files | ForEach-Object { "F::" + $_.FullName }; exit }',
|
|
207
363
|
'$txt = Get-Clipboard -Raw',
|
|
208
364
|
'if ($txt) { "T::" + [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($txt)); exit }',
|
|
209
|
-
|
|
365
|
+
// .NET clipboard, not `Get-Clipboard -Image` (see the CLIP_PROBE note).
|
|
366
|
+
'Add-Type -AssemblyName System.Windows.Forms',
|
|
367
|
+
'Add-Type -AssemblyName System.Drawing',
|
|
368
|
+
'$img = [System.Windows.Forms.Clipboard]::GetImage()',
|
|
210
369
|
'if ($null -ne $img) {',
|
|
211
370
|
' $path = Join-Path $env:TEMP ("hncode-paste-" + [guid]::NewGuid().ToString("N") + ".png")',
|
|
212
371
|
' $img.Save($path, [System.Drawing.Imaging.ImageFormat]::Png)',
|