ispbills-icli 8.3.0 → 8.3.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 CHANGED
@@ -188,7 +188,7 @@ suggestion (accept with **Tab** or **→**), and runs the highlighted command on
188
188
  | **@** | Mention a device/entity (e.g. `@olt:`, `@onu:`); **Tab** / **→** completes the tag |
189
189
  | **↑/↓** (empty input) | Navigate input history |
190
190
  | **Ctrl+Z** / **Ctrl+Y** | Undo / redo the input line |
191
- | **Paste** | Multi-line pastes are inserted as one line (never auto-submitted) |
191
+ | **Paste** | Multi-line pastes (incl. mouse right-click) keep their line breaks and are never auto-submitted — press **Enter** to send |
192
192
  | **Shift+Tab** | Toggle autopilot mode |
193
193
  | **Ctrl+T** | Toggle reasoning display |
194
194
  | **Ctrl+L** | Clear the transcript |
@@ -225,7 +225,7 @@ iCli connects to the same AI engine as the in-browser terminal:
225
225
  - **Bot memory** — `/remember` saves standing operator notes that are injected into every answer (`/memory`, `/forget` to manage)
226
226
  - **Backups in git** — `/backup` archives config to a per-user git repo; run `/git status|log|commit|push` from the prompt
227
227
  - **Share as a gist** — `/gist` publishes the last answer or transcript to a secret GitHub gist
228
- - **Copy & paste** — `/copy` puts the last answer on the clipboard; multi-line pastes and **Ctrl+Z / Ctrl+Y** undo/redo work in the input
228
+ - **Copy & paste** — `/copy` puts the last answer on the clipboard; multi-line pastes (mouse right-click or Cmd/Ctrl+V) keep their line breaks, and **Ctrl+Z / Ctrl+Y** undo/redo work in the input
229
229
  - **Full-screen terminal UI** — a full-height alternate-screen TUI (built on [Ink](https://github.com/vadimdemedes/ink)) with the banner pinned to the top, a bottom-pinned bordered input, a selectable slash-command menu, input history, and a bottom-anchored transcript — the same layout as GitHub Copilot CLI / Gemini CLI. Piped/non-interactive use falls back to plain streaming output
230
230
  - **Terminal-aware rendering** — the layout reflows to your terminal width and resizes live; on classic Windows consoles (`cmd.exe` / legacy PowerShell) it auto-switches to ASCII-safe glyphs and borders so nothing shows as broken boxes
231
231
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ispbills-icli",
3
- "version": "8.3.0",
3
+ "version": "8.3.1",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
package/src/tui/app.js CHANGED
@@ -100,8 +100,9 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion })
100
100
  const lastAnswerRef = useRef(''); // most recent assistant answer (for /copy, /gist)
101
101
  const choiceActionRef = useRef(null); // action to run when a non-AI Choice is picked
102
102
 
103
- // Enable terminal bracketed-paste so multi-line pastes arrive as one chunk
104
- // (the Composer flattens them) instead of submitting on the first newline.
103
+ // Enable terminal bracketed-paste so multi-line pastes (mouse right-click or
104
+ // Cmd/Ctrl+V) arrive wrapped in markers as one block the Composer inserts
105
+ // them with newlines preserved instead of submitting on the first newline.
105
106
  useEffect(() => {
106
107
  try { process.stdout.write('\x1b[?2004h'); } catch {}
107
108
  return () => { try { process.stdout.write('\x1b[?2004l'); } catch {} };
@@ -40,6 +40,26 @@ export function Composer({ onSubmit, busy, width = 80 }) {
40
40
  lastMut.current = kind;
41
41
  };
42
42
 
43
+ // Bracketed-paste assembly. A right-click / Cmd+V paste is wrapped by the
44
+ // terminal in \x1b[200~ … \x1b[201~ and can arrive split across several
45
+ // reads, so we buffer until the end marker before inserting.
46
+ const pasteRef = useRef({ active: false, buf: '' });
47
+
48
+ // Insert pasted text at the cursor, PRESERVING newlines so multi-line pastes
49
+ // (configs, logs) keep their structure. Tabs → 2 spaces; other control chars
50
+ // and the bracketed-paste markers are dropped.
51
+ const insertPaste = (raw) => {
52
+ const text = raw
53
+ .replace(/\x1b?\[20[01]~/g, '')
54
+ .replace(/\r\n?/g, '\n')
55
+ .replace(/\t/g, ' ')
56
+ .replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '');
57
+ if (!text) return;
58
+ snapshot('paste');
59
+ setValue((v) => v.slice(0, cursor) + text + v.slice(cursor));
60
+ setCursor((c) => c + text.length);
61
+ };
62
+
43
63
  // Determine the token being typed at the cursor to drive the palette.
44
64
  const upto = value.slice(0, cursor);
45
65
  const tokStart = Math.max(upto.lastIndexOf(' '), upto.lastIndexOf('\n')) + 1;
@@ -79,6 +99,25 @@ export function Composer({ onSubmit, busy, width = 80 }) {
79
99
  useInput((input, key) => {
80
100
  if (busy) return; // ignore edits while a turn streams (App handles Ctrl+C/Esc)
81
101
 
102
+ // ── Bracketed paste (mouse right-click / Cmd+V), possibly split reads ──
103
+ // Ink strips the leading ESC, so the terminal's \x1b[200~ … \x1b[201~
104
+ // wrappers arrive as the marker text "[200~" / "[201~" (sometimes as their
105
+ // own useInput calls). Buffer everything between them so embedded newlines
106
+ // are never treated as Enter, then insert the whole block at once.
107
+ const START = '[200~';
108
+ const END = '[201~';
109
+ if (pasteRef.current.active || input.includes(START)) {
110
+ pasteRef.current.active = true;
111
+ pasteRef.current.buf += input;
112
+ const e = pasteRef.current.buf.indexOf(END);
113
+ if (e === -1) return; // still receiving the paste
114
+ const s = pasteRef.current.buf.indexOf(START);
115
+ const raw = pasteRef.current.buf.slice(s + START.length, e);
116
+ pasteRef.current = { active: false, buf: '' };
117
+ insertPaste(raw);
118
+ return;
119
+ }
120
+
82
121
  // ── Submit / complete ──
83
122
  // Some terminals/PTYs deliver Enter as input="\r" without key.return, so
84
123
  // treat a lone CR/LF as Enter too (a paste is a longer chunk, never "\r").
@@ -155,20 +194,23 @@ export function Composer({ onSubmit, busy, width = 80 }) {
155
194
 
156
195
  // ── Printable input & paste ──
157
196
  if (input && !key.ctrl && !key.meta) {
158
- // Strip bracketed-paste markers, then flatten newlines/tabs from a paste.
159
- let text = input.replace(/\x1b\[20[01]~/g, '');
160
- const isPaste = text.length > 1 || /[\r\n\t]/.test(text);
161
- text = text.replace(/[\r\n\t]+/g, ' ').replace(/[\x00-\x1f\x7f]/g, '');
197
+ // A multi-char chunk or one containing newlines/tabs is a non-bracketed
198
+ // paste (terminal without bracketed-paste support): keep its newlines.
199
+ if (input.length > 1 || /[\r\n\t]/.test(input)) { insertPaste(input); return; }
200
+ const text = input.replace(/[\x00-\x1f\x7f]/g, '');
162
201
  if (!text) return;
163
- snapshot(isPaste ? 'paste' : 'type');
202
+ snapshot('type');
164
203
  setValue((v) => v.slice(0, cursor) + text + v.slice(cursor));
165
204
  setCursor((c) => c + text.length);
166
205
  }
167
206
  });
168
207
 
169
208
  const before = value.slice(0, cursor);
170
- const at = value[cursor] ?? ' ';
171
- const after = value.slice(cursor + 1);
209
+ const rawAt = value[cursor];
210
+ // When the cursor sits on a newline, show an inverse space and keep the line
211
+ // break in `after` so the buffer still renders across multiple lines.
212
+ const at = (rawAt === undefined || rawAt === '\n') ? ' ' : rawAt;
213
+ const after = rawAt === '\n' ? '\n' + value.slice(cursor + 1) : value.slice(cursor + 1);
172
214
  const empty = value.length === 0;
173
215
  const sugTok = menuOpen && matches[sel] ? (slashMode ? matches[sel].cmd : matches[sel].tag) : '';
174
216
  const ghost = sugTok && sugTok.startsWith(token) && cursor >= value.length ? sugTok.slice(token.length) : '';