ispbills-icli 8.4.11 → 8.4.13

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.
@@ -20,7 +20,7 @@ const MENTIONS = [
20
20
  { tag: '@mac', desc: 'A MAC address' },
21
21
  ];
22
22
 
23
- export function Composer({ onSubmit, busy, width = 80, prefill = null, onPrefillConsumed = null }) {
23
+ export function Composer({ onSubmit, busy, width = 80 }) {
24
24
  const [value, setValue] = useState('');
25
25
  const [cursor, setCursor] = useState(0);
26
26
  const [history, setHistory] = useState([]);
@@ -40,18 +40,6 @@ export function Composer({ onSubmit, busy, width = 80, prefill = null, onPrefill
40
40
  lastMut.current = kind;
41
41
  };
42
42
 
43
- // When app.js injects a /paste prefill, load it into the input box once.
44
- useEffect(() => {
45
- if (!prefill) return;
46
- setValue(prefill);
47
- setCursor(prefill.length);
48
- undoRef.current = []; redoRef.current = []; lastMut.current = '';
49
- onPrefillConsumed?.();
50
- }, [prefill]); // eslint-disable-line react-hooks/exhaustive-deps
51
-
52
- // Ctrl+S stash: save the current prompt to restore later (GitHub Copilot CLI parity).
53
- const stashRef = useRef('');
54
-
55
43
  // Bracketed-paste assembly. A right-click / Cmd+V paste is wrapped by the
56
44
  // terminal in \x1b[200~ … \x1b[201~ and can arrive split across several
57
45
  // reads, so we buffer until the end marker before inserting.
@@ -155,18 +143,6 @@ export function Composer({ onSubmit, busy, width = 80, prefill = null, onPrefill
155
143
  return;
156
144
  }
157
145
 
158
- // ── Ctrl+S: stash / pop prompt (GitHub Copilot CLI parity) ──
159
- if (key.ctrl && input === 's') {
160
- if (value) {
161
- stashRef.current = value;
162
- setValue(''); setCursor(0);
163
- } else if (stashRef.current) {
164
- const v = stashRef.current; stashRef.current = '';
165
- setText(v, v.length);
166
- }
167
- return;
168
- }
169
-
170
146
  // ── Menu / history navigation ──
171
147
  if (key.upArrow) {
172
148
  if (menuOpen) { setSel((i) => Math.max(0, i - 1)); return; }
@@ -239,15 +215,6 @@ export function Composer({ onSubmit, busy, width = 80, prefill = null, onPrefill
239
215
  const sugTok = menuOpen && matches[sel] ? (slashMode ? matches[sel].cmd : matches[sel].tag) : '';
240
216
  const ghost = sugTok && sugTok.startsWith(token) && cursor >= value.length ? sugTok.slice(token.length) : '';
241
217
 
242
- // ── Shell mode: prefix '!' triggers yellow $ prompt + orange border ──
243
- const shellMode = value.startsWith('!') && !menuOpen;
244
- const borderColor = busy ? C.gray : shellMode ? C.orange : C.accent;
245
- const promptGlyph = shellMode ? '$ ' : glyphs.prompt + ' ';
246
- const promptColor = busy ? C.gray : shellMode ? C.orange : C.accent;
247
-
248
- // Number of lines in the buffer (for the line-count badge).
249
- const lineCount = value.split('\n').length;
250
-
251
218
  return html`
252
219
  <${Box} flexDirection="column" width=${width}>
253
220
  ${menuOpen
@@ -273,18 +240,16 @@ export function Composer({ onSubmit, busy, width = 80, prefill = null, onPrefill
273
240
  <//>`
274
241
  : null}
275
242
 
276
- <${Box} width=${width} borderStyle=${borderStyle()} borderColor=${borderColor} paddingX=${1}>
277
- <${Text} color=${promptColor}>${promptGlyph}<//>
243
+ <${Box} width=${width} borderStyle=${borderStyle()} borderColor=${busy ? C.gray : C.accent} paddingX=${1}>
244
+ <${Text} color=${busy ? C.gray : C.accent}>${glyphs.prompt} <//>
278
245
  ${empty
279
246
  ? html`<${Box} flexGrow=${1}>
280
247
  <${Text} color=${C.gray} inverse> <//>
281
- <${Text} color=${C.gray}>${busy ? 'Working…' : 'How can I help with your network? · /help · ! shell'}<//>
248
+ <${Text} color=${C.gray}>${busy ? 'Working…' : 'Ask about your network, or type / for commands'}<//>
282
249
  <//>`
283
250
  : html`<${Box} flexGrow=${1}>
284
251
  <${Text}>${before}<//><${Text} inverse>${at}<//><${Text}>${after}<//><${Text} color=${C.gray}>${ghost}<//>
285
252
  <//>`}
286
- ${lineCount > 1
287
- ? html`<${Text} dimColor> ${lineCount}L<//>` : null}
288
253
  <//>
289
254
  <//>`;
290
255
  }
@@ -90,30 +90,14 @@ function renderTable(header, rows) {
90
90
  export function Markdown({ content = '' }) {
91
91
  const rows = [];
92
92
  let inCode = false;
93
- let codeLines = [];
94
93
  const lines = String(content).replace(/\s+$/, '').split('\n');
95
94
 
96
95
  for (let i = 0; i < lines.length; i++) {
97
96
  const line = lines[i].replace(/\t/g, ' ');
98
97
 
99
- // Fenced code block — collect all lines then render as a left-bordered block.
100
- if (line.trim().startsWith('```')) {
101
- if (!inCode) {
102
- inCode = true;
103
- codeLines = [];
104
- } else {
105
- inCode = false;
106
- const captured = codeLines;
107
- rows.push(html`
108
- <${Box} key=${k()} flexDirection="column" marginY=${1}
109
- borderStyle=${asciiSafe() ? 'single' : 'single'} borderColor=${C.gray}
110
- borderTop=${false} borderRight=${false} borderBottom=${false} paddingLeft=${1}>
111
- ${captured.map((cl, ci) => html`<${Text} key=${k()} color=${C.green}>${cl}<//>` )}
112
- <//>`);
113
- }
114
- continue;
115
- }
116
- if (inCode) { codeLines.push(line); continue; }
98
+ // Fenced code block.
99
+ if (line.trim().startsWith('```')) { inCode = !inCode; continue; }
100
+ if (inCode) { rows.push(html`<${Text} key=${k()} color=${C.green}> ${line}<//>`); continue; }
117
101
 
118
102
  // GFM table: header row + separator + body rows.
119
103
  if (isTableRow(line) && i + 1 < lines.length && isTableSep(lines[i + 1])) {
package/src/tui/run.js CHANGED
@@ -3,32 +3,19 @@
3
3
  // once at the top and scrolls away, and the terminal's native scrollback keeps
4
4
  // the full conversation history. Completed transcript items are flushed to the
5
5
  // scrollback by Ink's <Static>; only the live area + input re-render in place.
6
- //
7
- // Mouse support: SGR mouse tracking is enabled so that:
8
- // • Left-clicks on the tab bar switch tabs.
9
- // • Scroll-wheel UP scrolls the terminal's native scrollback (history).
10
- // • Scroll-wheel DOWN is swallowed — no drifting below the live area.
11
6
  import { html } from './dom.js';
12
7
  import { render } from 'ink';
13
8
  import { App } from './app.js';
14
- import { patchStdinMouse, enableMouse, disableMouse } from './mouse.js';
15
9
 
16
10
  export async function runTui(cfg, opts = {}) {
17
- const { display = {}, onExternal, initialQuestion, version = '', resumeSession = null } = opts;
11
+ const { display = {}, onExternal, initialQuestion } = opts;
18
12
  let externalAction;
19
13
 
20
- // Patch stdin.read() to strip mouse sequences before Ink sees them,
21
- // then enable SGR mouse tracking so the terminal sends us those events.
22
- const unpatchMouse = patchStdinMouse(process.stdin);
23
- enableMouse();
24
-
25
14
  const instance = render(
26
15
  html`<${App}
27
16
  cfg=${cfg}
28
17
  display=${display}
29
18
  initialQuestion=${initialQuestion}
30
- version=${version}
31
- resumeSession=${resumeSession}
32
19
  onExternal=${(a) => { externalAction = a; onExternal?.(a); }}
33
20
  />`,
34
21
  { exitOnCtrlC: false },
@@ -36,8 +23,5 @@ export async function runTui(cfg, opts = {}) {
36
23
 
37
24
  await instance.waitUntilExit();
38
25
 
39
- disableMouse();
40
- unpatchMouse();
41
-
42
26
  return externalAction;
43
27
  }
package/src/tui/theme.js CHANGED
@@ -5,36 +5,18 @@ import { glyphs, asciiSafe } from '../ui.js';
5
5
 
6
6
  export { glyphs, asciiSafe };
7
7
 
8
- // Soft-blue brand accent (approx. xterm-256 colour 111) — matches Copilot CLI.
8
+ // Soft-blue brand accent (approx. xterm-256 colour 111).
9
9
  export const ACCENT = '#87afff';
10
-
11
- // Semantic color palette. Where possible mirror Copilot CLI conventions:
12
- // - accent/cyan for interactive / in-flight elements
13
- // - green for success / completed
14
- // - red for errors / destructive
15
- // - yellow for warnings / caution / autopilot
16
- // - magenta for reasoning/thinking
17
- // - gray / dimColor for secondary / completed-dim text
18
10
  export const C = {
19
- accent: ACCENT,
20
- gray: 'gray',
21
- white: 'white',
22
- cyan: 'cyan',
23
- green: 'green',
24
- yellow: 'yellow',
25
- red: 'red',
11
+ accent: ACCENT,
12
+ gray: 'gray',
13
+ white: 'white',
14
+ cyan: 'cyan',
15
+ green: 'green',
16
+ yellow: 'yellow',
17
+ red: 'red',
26
18
  magenta: 'magenta',
27
- blue: 'blue',
28
- orange: '#ffb86c', // shell mode / caution
29
- purple: '#d097ff', // sub-agent / agent role
30
- };
31
-
32
- // Copilot-style task status glyphs (Unicode only; fall back handled per use-site).
33
- // ● filled = step completed ◐ half = step in-progress ○ empty = pending
34
- export const SG = {
35
- filled: '●', // U+25CF completed
36
- half: '◐', // U+25D0 in-progress (used alongside spinner)
37
- empty: '○', // U+25CB pending
19
+ blue: 'blue',
38
20
  };
39
21
 
40
22
  // ink-spinner uses cli-spinners; 'line' is pure ASCII (|/-\), 'dots' is braille.
@@ -48,9 +30,9 @@ export function borderStyle() {
48
30
  return asciiSafe() ? 'single' : 'round';
49
31
  }
50
32
 
51
- // Middle-dot separator. Copilot CLI uses a single space each side.
33
+ // Middle-dot separator, ASCII-safe on classic Windows consoles.
52
34
  export function midDot() {
53
- return asciiSafe() ? ' - ' : ' · ';
35
+ return asciiSafe() ? ' - ' : ' · ';
54
36
  }
55
37
 
56
38
  // Em-dash, ASCII-safe on classic Windows consoles.
package/src/resume.js DELETED
@@ -1,30 +0,0 @@
1
- /**
2
- * Temporary session persistence for the /update hot-reload flow.
3
- *
4
- * Before updating, icli serialises the live conversation to a JSON file in
5
- * /tmp so the new binary can resume right where the user left off.
6
- */
7
- import { writeFileSync, readFileSync, existsSync, unlinkSync } from 'fs';
8
- import { tmpdir } from 'os';
9
- import { join } from 'path';
10
-
11
- const PREFIX = 'icli-resume-';
12
-
13
- /** Write a resume file and return its path. */
14
- export function saveResume(convo, plain, items = []) {
15
- const path = join(tmpdir(), `${PREFIX}${Date.now()}.json`);
16
- writeFileSync(path, JSON.stringify({ convo, plain, items, savedAt: new Date().toISOString() }), 'utf8');
17
- return path;
18
- }
19
-
20
- /** Load and delete a resume file. Returns { convo, plain, items } or null. */
21
- export function loadResume(path) {
22
- if (!path || !existsSync(path)) return null;
23
- try {
24
- const data = JSON.parse(readFileSync(path, 'utf8'));
25
- try { unlinkSync(path); } catch { /* best-effort delete */ }
26
- return { convo: data.convo || [], plain: data.plain || [], items: data.items || [] };
27
- } catch {
28
- return null;
29
- }
30
- }
package/src/shell.js DELETED
@@ -1,18 +0,0 @@
1
- import { exec } from 'child_process';
2
-
3
- /**
4
- * Run a shell command and capture combined stdout/stderr.
5
- * Returns { ok, stdout, stderr, exitCode }.
6
- */
7
- export function runShell(cmd, timeout = 30_000) {
8
- return new Promise((resolve) => {
9
- exec(cmd, { timeout, maxBuffer: 512 * 1024, shell: true }, (err, stdout, stderr) => {
10
- resolve({
11
- ok: !err || err.code === 0,
12
- stdout: (stdout || '').trim(),
13
- stderr: (stderr || '').trim(),
14
- exitCode: err?.code ?? 0,
15
- });
16
- });
17
- });
18
- }
package/src/tui/mouse.js DELETED
@@ -1,68 +0,0 @@
1
- // Mouse event support for the iCli TUI.
2
- // Patches process.stdin.read() to intercept SGR mouse escape sequences before
3
- // Ink's input parser sees them, routing them to a shared EventEmitter instead.
4
- // This prevents mouse sequences from appearing as garbage in the composer.
5
- //
6
- // SGR mouse format: \x1b[<{btn};{x};{y}M (button press)
7
- // \x1b[<{btn};{x};{y}m (button release)
8
- //
9
- // Button codes of interest:
10
- // 0 = left click press/release
11
- // 64 = scroll wheel up
12
- // 65 = scroll wheel down
13
- import { EventEmitter } from 'events';
14
-
15
- const MOUSE_SGR = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
16
-
17
- // Shared event emitter — app.js subscribes to 'event'.
18
- export const mouseEvents = new EventEmitter();
19
- mouseEvents.setMaxListeners(20);
20
-
21
- /**
22
- * Patch stdin.read() to intercept and strip mouse sequences.
23
- * Returns an unpatch function to restore the original.
24
- */
25
- export function patchStdinMouse(stdin) {
26
- if (!stdin || typeof stdin.read !== 'function') return () => {};
27
- const origRead = stdin.read.bind(stdin);
28
-
29
- stdin.read = function (size) {
30
- const data = origRead(size);
31
- if (!data) return data;
32
- const str = data.toString('utf8');
33
-
34
- // Emit each mouse event found in the chunk.
35
- MOUSE_SGR.lastIndex = 0;
36
- let m;
37
- while ((m = MOUSE_SGR.exec(str)) !== null) {
38
- mouseEvents.emit('event', {
39
- btn: parseInt(m[1], 10),
40
- x: parseInt(m[2], 10),
41
- y: parseInt(m[3], 10),
42
- press: m[4] === 'M',
43
- });
44
- }
45
-
46
- // Strip ALL mouse sequences so Ink never sees them.
47
- const filtered = str.replace(/\x1b\[<\d+;\d+;\d+[Mm]/g, '');
48
- return filtered ? Buffer.from(filtered, 'utf8') : null;
49
- };
50
-
51
- return function unpatch() {
52
- stdin.read = origRead;
53
- };
54
- }
55
-
56
- /** Enable SGR mouse button + scroll-wheel tracking. */
57
- export function enableMouse() {
58
- try {
59
- process.stdout.write('\x1b[?1000h\x1b[?1006h');
60
- } catch {}
61
- }
62
-
63
- /** Disable SGR mouse tracking (restore normal terminal mouse behaviour). */
64
- export function disableMouse() {
65
- try {
66
- process.stdout.write('\x1b[?1000l\x1b[?1006l');
67
- } catch {}
68
- }
@@ -1,43 +0,0 @@
1
- /**
2
- * Background version check against npm registry.
3
- * Caches the result in the session so we only hit npm once per run.
4
- * Resolves to { latest, isNewer } or null on any error (network, etc.).
5
- */
6
-
7
- const PKG_NAME = 'ispbills-icli';
8
- const REGISTRY = `https://registry.npmjs.org/${PKG_NAME}/latest`;
9
-
10
- /** Parse a semver string into [major, minor, patch] ints. */
11
- function parse(v) {
12
- const parts = String(v || '').replace(/[^0-9.]/g, '').split('.').map(Number);
13
- return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
14
- }
15
-
16
- /** Returns true when `remote` is strictly newer than `current`. */
17
- function isNewer(current, remote) {
18
- const [cMaj, cMin, cPat] = parse(current);
19
- const [rMaj, rMin, rPat] = parse(remote);
20
- if (rMaj !== cMaj) return rMaj > cMaj;
21
- if (rMin !== cMin) return rMin > cMin;
22
- return rPat > cPat;
23
- }
24
-
25
- /**
26
- * Check npm for a newer version of ispbills-icli.
27
- * Resolves to { latest: string, isNewer: boolean } or null.
28
- */
29
- export async function checkLatestVersion(currentVersion) {
30
- try {
31
- const ctrl = new AbortController();
32
- const timer = setTimeout(() => ctrl.abort(), 5000);
33
- const res = await fetch(REGISTRY, { signal: ctrl.signal });
34
- clearTimeout(timer);
35
- if (!res.ok) return null;
36
- const data = await res.json();
37
- const latest = data.version;
38
- if (!latest) return null;
39
- return { latest, isNewer: isNewer(currentVersion, latest) };
40
- } catch {
41
- return null;
42
- }
43
- }