ispbills-icli 8.7.0 → 8.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ispbills-icli",
3
- "version": "8.7.0",
3
+ "version": "8.7.1",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
package/src/tui/app.js CHANGED
@@ -434,7 +434,7 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
434
434
  const [diffState, setDiffState] = useState(null);
435
435
  const chipsRows = (!busy && chips.length && !choice && !search && !sessionPicker && !diffState) ? 1 : 0;
436
436
  const composerRows = 2;
437
- const chromeRows = 2 + composerRows + 1 + chipsRows + 1;
437
+ const chromeRows = 2 + composerRows + 1 + chipsRows + 2; // +2 for two footer rows
438
438
  const viewportRows = Math.max(3, rows - chromeRows);
439
439
  const scrollPage = Math.max(4, viewportRows - 2);
440
440
 
@@ -2090,11 +2090,15 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
2090
2090
  const cwdMaxLen = Math.max(12, cols - footerRightEstLen - (branch ? branch.length + 6 : 0) - 4);
2091
2091
  const cwd = shortenPath(process.cwd(), cwdMaxLen);
2092
2092
  const footerLeft = [cwd, branch ? `${glyphs.gitBranch}${branch}` : null].filter(Boolean).join(midDot());
2093
- const footerRight = hint
2093
+ const footerInfoRight = streamerMode ? '' : [modelLabel, tokenLabel].filter(Boolean).join(midDot());
2094
+ // Status row: working indicator when busy, key hints when idle, hint message when set
2095
+ const footerStatusLeft = busy
2096
+ ? `${glyphs.spinner} Working\u2026`
2097
+ : hint
2094
2098
  ? hint
2095
- : streamerMode
2096
- ? ''
2097
- : [modelLabel, tokenLabel].filter(Boolean).join(midDot());
2099
+ : `? help ${midDot()} / commands ${midDot()} \u2191\u2193 history ${midDot()} Tab mode`;
2100
+ const footerStatusColor = busy ? C.warning : hint ? C.warning : C.muted;
2101
+ const footerStatusRight = busy ? `Ctrl+C to cancel` : '';
2098
2102
  const composerMode = shellMode ? 'shell' : mode;
2099
2103
 
2100
2104
  // ── Viewport slicing ───────────────────────────────────────────────────────
@@ -2210,9 +2214,16 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
2210
2214
 
2211
2215
  <${Box} paddingX=${1} width=${cols}>
2212
2216
  <${Box} flexGrow=${1}>
2213
- <${Text} color=${C.muted} wrap="truncate-end">${footerLeft}<//>
2217
+ <${Text} color=${footerStatusColor} wrap="truncate-end">${footerStatusLeft}<//>
2214
2218
  <//>
2215
- <${Text} color=${hint ? C.warning : C.muted} wrap="truncate-end">${footerRight}<//>
2219
+ <${Text} color=${footerStatusColor} dimColor wrap="truncate-end">${footerStatusRight}<//>
2220
+ <//>
2221
+
2222
+ <${Box} paddingX=${1} width=${cols}>
2223
+ <${Box} flexGrow=${1}>
2224
+ <${Text} color=${C.muted} dimColor wrap="truncate-end">${footerLeft}<//>
2225
+ <//>
2226
+ <${Text} color=${C.muted} dimColor wrap="truncate-end">${footerInfoRight}<//>
2216
2227
  <//>
2217
2228
  <//>
2218
2229
  <//>`;
package/src/tui/mouse.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import { useEffect, useRef } from './dom.js';
2
- import { useStdin } from 'ink';
3
2
 
4
3
  function parseMouseData(chunk, emit) {
5
4
  let i = 0;
@@ -28,15 +27,16 @@ function parseMouseData(chunk, emit) {
28
27
  return '';
29
28
  }
30
29
 
30
+ // Read directly from process.stdin (before Ink's filtered stream) so we see
31
+ // the raw mouse escape sequences that the filter strips out in run.js.
31
32
  export function useMouse(handler, enabled = true) {
32
- const { stdin } = useStdin();
33
33
  const handlerRef = useRef(handler);
34
34
  const bufferRef = useRef('');
35
35
 
36
36
  handlerRef.current = handler;
37
37
 
38
38
  useEffect(() => {
39
- if (!enabled || !stdin) return;
39
+ if (!enabled) return;
40
40
 
41
41
  try { process.stdout.write('\x1b[?1000h\x1b[?1006h'); } catch {}
42
42
 
@@ -47,11 +47,11 @@ export function useMouse(handler, enabled = true) {
47
47
  });
48
48
  };
49
49
 
50
- stdin.on('data', onData);
50
+ process.stdin.on('data', onData);
51
51
  return () => {
52
- stdin.off('data', onData);
52
+ process.stdin.off('data', onData);
53
53
  bufferRef.current = '';
54
54
  try { process.stdout.write('\x1b[?1000l\x1b[?1006l'); } catch {}
55
55
  };
56
- }, [enabled, stdin]);
56
+ }, [enabled]);
57
57
  }
package/src/tui/run.js CHANGED
@@ -1,9 +1,58 @@
1
1
  // Ink render entrypoint. Runs the TUI in a full-screen ALT-SCREEN viewport.
2
2
  import { html } from './dom.js';
3
3
  import { render } from 'ink';
4
+ import { Transform } from 'stream';
4
5
  import { App } from './app.js';
5
6
  import { saveConfig } from '../config.js';
6
7
 
8
+ // ── Mouse sequence filter ────────────────────────────────────────────────────
9
+ // Strips SGR (\x1b[<N;N;N[Mm]) and X10 (\x1b[M + 3 bytes) mouse escape
10
+ // sequences from the stdin stream before Ink sees it. Without this, unrecognised
11
+ // CSI sequences leak into the composer as raw characters when clicking/scrolling.
12
+ // mouse.js reads from process.stdin directly (before this filter) to get events.
13
+ class MouseFilterTransform extends Transform {
14
+ constructor() {
15
+ super();
16
+ this.isTTY = process.stdin.isTTY;
17
+ this.setRawMode = process.stdin.setRawMode
18
+ ? (mode) => process.stdin.setRawMode(mode)
19
+ : undefined;
20
+ this._buf = '';
21
+ }
22
+
23
+ _transform(chunk, _enc, cb) {
24
+ const text = Buffer.isBuffer(chunk) ? chunk.toString('latin1') : String(chunk);
25
+ this._buf += text;
26
+ let out = '';
27
+ let i = 0;
28
+ while (i < this._buf.length) {
29
+ // SGR mouse: \x1b[<N;N;N[Mm]
30
+ if (this._buf[i] === '\x1b' && this._buf[i + 1] === '[' && this._buf[i + 2] === '<') {
31
+ const rest = this._buf.slice(i);
32
+ const m = rest.match(/^\x1b\[<\d+;\d+;\d+[Mm]/);
33
+ if (m) { i += m[0].length; continue; }
34
+ if (rest.length < 32) break; // incomplete – wait for more bytes
35
+ out += this._buf[i++]; // give up, pass through
36
+ continue;
37
+ }
38
+ // X10 mouse: \x1b[M + 3 bytes
39
+ if (this._buf[i] === '\x1b' && this._buf[i + 1] === '[' && this._buf[i + 2] === 'M') {
40
+ if (i + 6 <= this._buf.length) { i += 6; continue; }
41
+ break; // incomplete
42
+ }
43
+ out += this._buf[i++];
44
+ }
45
+ this._buf = this._buf.slice(i);
46
+ if (out) this.push(out);
47
+ cb();
48
+ }
49
+
50
+ _flush(cb) {
51
+ if (this._buf) { this.push(this._buf); this._buf = ''; }
52
+ cb();
53
+ }
54
+ }
55
+
7
56
  // ── Alt-screen management ────────────────────────────────────────────────────
8
57
  // Switch to the alternate terminal buffer so the TUI owns the full viewport and
9
58
  // nothing leaks into the user's native scrollback. Must run BEFORE Ink renders.
@@ -124,6 +173,11 @@ export async function runTui(cfg, opts = {}) {
124
173
  try { saveConfig(cfg); } catch {}
125
174
  }
126
175
 
176
+ // Pipe raw stdin through the mouse filter so Ink never sees mouse escape
177
+ // sequences. mouse.js reads from process.stdin directly to capture events.
178
+ const mouseFilter = new MouseFilterTransform();
179
+ process.stdin.pipe(mouseFilter);
180
+
127
181
  const instance = render(
128
182
  html`<${App}
129
183
  cfg=${cfg}
@@ -134,7 +188,7 @@ export async function runTui(cfg, opts = {}) {
134
188
  resumeMode=${resumeMode}
135
189
  onExternal=${(a) => { externalAction = a; onExternal?.(a); }}
136
190
  />`,
137
- { exitOnCtrlC: false },
191
+ { exitOnCtrlC: false, stdin: mouseFilter },
138
192
  );
139
193
 
140
194
  try {