Skip to content

Commit a2dfdec

Browse files
fix: require explicit action for image paste and restore reliable trigger
- Remove clipboard badge ([📋]) from the prompt entirely - Keep image insertion explicit only (/img or Ctrl+V event when terminal forwards it) - Drop watcher-driven UI state/dismissal loop and related stale state paths - Make Ctrl+V (\x16) and empty bracketed paste trigger /img directly without waiting for watcher flag timing - Preserve multi-image support via repeated explicit insertions in one line Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4e31722 commit a2dfdec

2 files changed

Lines changed: 12 additions & 84 deletions

File tree

src/core/input.js

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -98,18 +98,11 @@ class RawInput extends EventEmitter {
9898
_onData(raw) {
9999
const data = raw;
100100

101-
// ── Ctrl+V raw byte (\x16) — only arrives when terminal cannot paste text
102-
// (e.g. clipboard holds an image, or non-WT terminal). When watcher
103-
// confirms image in clipboard → insert /img to trigger inline capture.
104-
// When no watcher or no image → fall through (WT handles text via
105-
// bracketed paste and we never receive \x16 for text anyway).
101+
// ── Ctrl+V raw byte (\x16) — explicit user action to paste image context.
102+
// We trigger /img immediately (no watcher gating) so capture is reliable
103+
// even if clipboard watcher state is momentarily stale.
106104
if (data === '\x16') {
107-
if (!this._paused) {
108-
if (this._hasImageFn && this._hasImageFn()) {
109-
this._insertImgCommand();
110-
}
111-
// else: terminal is handling paste via bracketed paste sequence
112-
}
105+
if (!this._paused) this._insertImgCommand();
113106
return;
114107
}
115108

@@ -167,10 +160,8 @@ class RawInput extends EventEmitter {
167160
this._insert(pasted);
168161
this._redraw();
169162
} else {
170-
// Empty text paste — clipboard had image only; trigger /img if confirmed
171-
if (!this._paused && this._hasImageFn && this._hasImageFn()) {
172-
this._insertImgCommand();
173-
}
163+
// Empty paste payload is treated as explicit image-paste intent.
164+
if (!this._paused) this._insertImgCommand();
174165
}
175166
return;
176167
}

src/core/repl.js

Lines changed: 6 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,10 @@ const BRAND = '⟩⟨';
1717
// Lazy-loaded to avoid startup cost
1818
function imgpaste() { return require('../utils/imgpaste'); }
1919

20-
// Dynamic prompt — mirrors PowerShell style, with optional clipboard indicator.
21-
// imgReady = true adds [📋] hint so user knows they can type /img.
22-
function makePrompt(imgReady) {
20+
// Dynamic prompt — mirrors PowerShell style.
21+
function makePrompt() {
2322
const cwd = process.cwd();
24-
const base = `\x1b[90mPS ${cwd}>\x1b[0m \x1b[1;96m${BRAND}\x1b[0m `;
25-
return imgReady
26-
? base + `\x1b[33m[📋]\x1b[0m `
27-
: base;
23+
return `\x1b[90mPS ${cwd}>\x1b[0m \x1b[1;96m${BRAND}\x1b[0m `;
2824
}
2925

3026
const HELP_TEXT = `
@@ -289,16 +285,12 @@ class SarmadRepl {
289285

290286
async start() {
291287
// ── Clipboard watcher ─────────────────────────────────────────────────────
292-
// Detects images in clipboard without relying on Ctrl+V (which Windows
293-
// Terminal intercepts). Background watcher stores clipboard sequence id
294-
// for image state, so we can dismiss stale images correctly.
288+
// Detect clipboard image presence for explicit image insert actions
289+
// (/img, Ctrl+V when forwarded by terminal, empty image paste fallback).
295290
const watcher = new ClipboardWatcher();
296291
watcher.start();
297292

298-
let _imgReady = false;
299-
let _lastSeq = null; // latest clipboard sequence with image
300-
let _dismissedSeq = null; // sequence user dismissed by deleting token
301-
const getPrompt = () => makePrompt(_imgReady);
293+
const getPrompt = () => makePrompt();
302294

303295
const rawInput = new RawInput({
304296
history: this.history
@@ -308,50 +300,8 @@ class SarmadRepl {
308300
hasImageFn: () => watcher.hasImage(),
309301
});
310302

311-
// ── Clipboard watcher interval ────────────────────────────────────────────
312-
// Uses clipboard sequence numbers to avoid stale state:
313-
// - No auto-capture while line is active (UX is explicit, not surprising)
314-
// - Deleting a token dismisses current sequence (hide [📋], no stale reinsert)
315-
// - Any clipboard change clears dismissal automatically
316-
const _watchInterval = setInterval(() => {
317-
if (rawInput._paused) return;
318-
const { hasImage, seq } = watcher.getState();
319-
320-
// Clipboard has no image: clear all image-related transient state
321-
if (!hasImage || !seq) {
322-
_lastSeq = null;
323-
_dismissedSeq = null;
324-
if (_imgReady) {
325-
_imgReady = false;
326-
rawInput.setPromptFn(getPrompt);
327-
rawInput._redraw();
328-
}
329-
return;
330-
}
331-
332-
// New clipboard content cancels any prior dismissal.
333-
if (_dismissedSeq !== null && seq !== _dismissedSeq) {
334-
_dismissedSeq = null;
335-
}
336-
337-
const isDismissed = (_dismissedSeq !== null && seq === _dismissedSeq);
338-
const isNewSeq = (_lastSeq === null || seq !== _lastSeq);
339-
340-
if (isNewSeq) _lastSeq = seq;
341-
342-
// Indicator shown only when current sequence is not dismissed.
343-
const wantIndicator = !isDismissed;
344-
if (wantIndicator !== _imgReady) {
345-
_imgReady = wantIndicator;
346-
rawInput.setPromptFn(getPrompt);
347-
rawInput._redraw();
348-
}
349-
}, 300); // 300ms — fast clear/update with low overhead
350-
351303
rawInput.on('line', async (line) => {
352304
rawInput.pause();
353-
_imgReady = false;
354-
rawInput.setPromptFn(getPrompt);
355305
try {
356306
await this.processInput(line);
357307
} catch (err) {
@@ -398,17 +348,6 @@ class SarmadRepl {
398348
rawInput.on('token-removed', (token) => {
399349
const m = token.match(/\[img:([A-Z0-9]{4})\]/);
400350
if (m) this._imgStore.delete(m[1]);
401-
// User explicitly removed an image token: dismiss current clipboard image
402-
// sequence so [📋] hides immediately and Enter won't re-capture it.
403-
const st = watcher.getState();
404-
if (st.hasImage && st.seq) {
405-
_dismissedSeq = st.seq;
406-
if (_imgReady) {
407-
_imgReady = false;
408-
rawInput.setPromptFn(getPrompt);
409-
rawInput._redraw();
410-
}
411-
}
412351
});
413352

414353
// ── Ctrl+C (single) — show hint ───────────────────────────────────────────
@@ -428,7 +367,6 @@ class SarmadRepl {
428367

429368
// ── Double Ctrl+C — exit ──────────────────────────────────────────────────
430369
rawInput.on('exit', () => {
431-
clearInterval(_watchInterval);
432370
watcher.stop();
433371
console.log(`\n${c.cyan}${BRAND} مع السلامة!${R}\n`);
434372
saveHistory(this.history);
@@ -437,7 +375,6 @@ class SarmadRepl {
437375

438376
// ── Ctrl+D ────────────────────────────────────────────────────────────────
439377
rawInput.on('close', () => {
440-
clearInterval(_watchInterval);
441378
watcher.stop();
442379
console.log(`\n${c.cyan}${BRAND} Session ended. Goodbye!${R}\n`);
443380
saveHistory(this.history);

0 commit comments

Comments
 (0)