ispbills-icli 8.4.11 → 8.4.12

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.4.11",
3
+ "version": "8.4.12",
4
4
  "description": "iCli — IspBills AI network engineer in your terminal",
5
5
  "keywords": [
6
6
  "ispbills",
package/src/client.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { refreshToken } from './auth.js';
2
+ import { logStream } from './logger.js';
2
3
 
3
4
  /**
4
5
  * Incrementally extract the answer text from the raw JSON the backend streams
@@ -160,5 +161,10 @@ export async function streamChat(cfg, messages, options = {}) {
160
161
  // committed answer below what the operator already saw stream by.
161
162
  const finalText = reply.text || '';
162
163
  const text = finalText.length >= answerText.length ? finalText : answerText;
164
+
165
+ // Log prompt + reply to ~/.config/ispbills-icli/logs/<user>/stream.log
166
+ const prompt = [...messages].reverse().find((m) => m.role === 'user')?.content ?? '';
167
+ logStream(cfg, prompt, text);
168
+
163
169
  return { reply, text, meta };
164
170
  }
package/src/logger.js ADDED
@@ -0,0 +1,23 @@
1
+ // Append-only log of every prompt + reply that passes through the icli stream.
2
+ // Writes to: ~/.config/ispbills-icli/logs/<userKey>/stream.log
3
+ import { appendFileSync, mkdirSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { userDir } from './userdir.js';
6
+
7
+ /**
8
+ * Append a prompt/reply pair to the stream log for this user.
9
+ * Call once per turn: logStream(cfg, prompt, reply).
10
+ * Silently no-ops on any I/O error.
11
+ */
12
+ export function logStream(cfg, prompt, reply) {
13
+ try {
14
+ const dir = userDir(cfg, 'logs');
15
+ const file = join(dir, 'stream.log');
16
+ const ts = new Date().toISOString();
17
+ const sep = '─'.repeat(60);
18
+ const entry = `\n${sep}\n[${ts}]\nPROMPT: ${String(prompt).trim()}\nREPLY: ${String(reply).trim()}\n`;
19
+ appendFileSync(file, entry, 'utf8');
20
+ } catch {
21
+ // never crash the app over logging
22
+ }
23
+ }
package/src/tui/app.js CHANGED
@@ -3,16 +3,9 @@
3
3
  // items are flushed to the terminal's native scrollback via Ink <Static> (so
4
4
  // you can scroll back through history), and only the live/streaming area plus
5
5
  // the input composer are re-rendered in place at the bottom.
6
- //
7
- // Mouse support (via src/tui/mouse.js + run.js):
8
- // • Scroll UP → sends \x1b[3S to scroll the terminal viewport upward
9
- // so the native scrollback buffer shows conversation history.
10
- // • Scroll DOWN → swallowed; prevents drifting below the live/composer area.
11
- // • Left click on tab bar row 1 → switches tabs by x-coordinate range.
12
- import { html, useState, useEffect, useRef, useCallback, useMemo } from './dom.js';
6
+ import { html, useState, useEffect, useRef, useCallback } from './dom.js';
13
7
  import { Box, Text, Static, useStdout, useApp, useInput } from 'ink';
14
8
  import { C, glyphs, midDot, dash } from './theme.js';
15
- import { mouseEvents } from './mouse.js';
16
9
  import {
17
10
  Banner, UserMessage, AssistantMessage, StatusLines, Reasoning,
18
11
  Plan, Connect, Notice, Thinking, Choice, ShellInput, ShellOut,
@@ -130,62 +123,6 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, ve
130
123
  if (input === '4') { setActiveTab(3); return; }
131
124
  });
132
125
 
133
- // ── Mouse event handling ────────────────────────────────────────────────────
134
- // Tab bar x-ranges (1-indexed). Layout: paddingX=1 on the TabBar outer Box
135
- // so text starts at column 2. Each tab = 2-char prefix (' ' or '❯ ') +
136
- // label text, followed by marginRight=2 before the next tab.
137
- const tabClickRanges = useMemo(() => {
138
- let x = 2; // paddingX=1 → first tab starts at column 2
139
- return TABS.map((label) => {
140
- const start = x;
141
- const w = 2 + label.length; // prefix (2 chars) + label
142
- x += w + 2; // advance past tab + marginRight=2
143
- return { start, end: start + w - 1 };
144
- });
145
- }, []);
146
-
147
- useEffect(() => {
148
- const onMouse = ({ btn, x, y, press }) => {
149
- // ── Scroll wheel UP → scroll viewport to reveal history ──────────────
150
- if (btn === 64) {
151
- try { process.stdout.write('\x1b[3S'); } catch {}
152
- return;
153
- }
154
- // ── Scroll wheel DOWN → swallowed ─────────────────────────────────────
155
- if (btn === 65) return;
156
-
157
- // ── Left click → tab bar ─────────────────────────────────────────────
158
- // Ink renders INLINE at the bottom of the terminal — NOT at y=1.
159
- // SGR y coordinates are 1-indexed from the terminal top.
160
- //
161
- // Layout rows at idle (bottom-to-top):
162
- // rows = footer hint
163
- // rows-1 = composer bottom border
164
- // rows-2 = composer input
165
- // rows-3 = composer top border
166
- // rows-4 = blank (marginTop=1 before composer)
167
- // rows-5 = tab separator
168
- // rows-6 = tab labels ← TAB_ROW at idle
169
- // rows-7 = compact header
170
- //
171
- // When there is live content of height h between tab separator and
172
- // composer, each row shifts up by h: TAB_ROW = rows - 6 - liveH.
173
- if (btn === 0 && press) {
174
- const liveH = live
175
- ? 1 + Math.min((live.status?.length || 0) + (live.text ? 2 : 0) + (!live.text && !live.reply?.steps ? 1 : 0), 12)
176
- : 0;
177
- const tabRowY = rows - 6 - liveH;
178
- if (Math.abs(y - tabRowY) <= 1) { // ±1 row tolerance
179
- const idx = tabClickRanges.findIndex((r) => x >= r.start && x <= r.end);
180
- if (idx >= 0) setActiveTab(idx);
181
- }
182
- }
183
- };
184
-
185
- mouseEvents.on('event', onMouse);
186
- return () => mouseEvents.off('event', onMouse);
187
- }, [tabClickRanges, rows, live]);
188
-
189
126
  const setAutopilotMode = useCallback((next) => {
190
127
  apRef.current = next;
191
128
  setAutopilot(next);
@@ -638,52 +575,38 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, ve
638
575
  };
639
576
 
640
577
  const ctx = [model ? shortModel(model) : null, APP_CWD, cfg.user?.name, cfg.user?.role].filter(Boolean).join(midDot());
641
- // Footer shows a concise key-hints bar. Autopilot badge is a right-aligned label.
642
- const baseHint = hint || `/help${midDot()}Shift+Tab autopilot${midDot()}↑ scroll history${midDot()}Ctrl+C exit`;
578
+ // Footer: left = session context (dimmed), right = hint or key-hints.
579
+ // Autopilot mode turns the right side yellow.
580
+ const footerHint = hint
581
+ ? hint
582
+ : autopilot
583
+ ? `${glyphs.bolt} autopilot ON${midDot()}/help${midDot()}Shift+Tab off${midDot()}Ctrl+C exit`
584
+ : `/help${midDot()}Shift+Tab autopilot${midDot()}Ctrl+C exit`;
585
+ const footerColor = hint ? C.yellow : autopilot ? C.yellow : void 0;
643
586
  const compact = cols < 60;
644
587
 
645
588
  // The banner is the first <Static> item so it prints once at the very top and
646
589
  // scrolls away with history; completed transcript items follow it. Ink flushes
647
590
  // each <Static> item to the terminal's normal buffer exactly once, leaving the
648
591
  // native scrollback intact so earlier conversation can be scrolled back to.
649
- // Scroll UP (mouse wheel) reveals this history; scroll DOWN is blocked.
650
592
  const staticItems = [{ type: 'banner', id: `banner-${clearEpoch}` }, ...items];
651
593
 
652
- // ── Main render ─────────────────────────────────────────────────────────────
653
- // Layout (top to bottom):
654
- // 1. Compact persistent header — "iCopilot" brand always visible
655
- // 2. Tab bar — Session / Monitoring / Issues / Gists (mouse-clickable)
656
- // 3. Session pane — Static history + live streaming area (scrollback via ↑)
657
- // 4. Composer + footer — always pinned to the bottom
658
594
  return html`
659
595
  <${Box} flexDirection="column" width=${cols}>
660
-
661
- ${/* ── 1. Compact header: brand + active context ─────────────────── */null}
662
- <${Box} paddingX=${1} paddingTop=${0}>
663
- <${Text} color=${C.accent} bold>iCopilot<//>
664
- <${Text} color=${C.gray}> ${dash()} <//>
665
- <${Text} dimColor>${ctx}<//>
666
- ${latestVersion?.isNewer
667
- ? html`<${Box} flexGrow=${1} justifyContent="flex-end"><${Text} color=${C.yellow}> 🔔 v${latestVersion.latest} /update<//> <//>`
668
- : null}
669
- <//>
670
-
671
- ${/* ── 2. Tab bar (mouse-clickable + Ctrl+1–4) ──────────────────── */null}
596
+ ${/* Tab bar — always pinned above content */null}
672
597
  <${TabBar} tabs=${TABS} active=${activeTab} onSelect=${setActiveTab} width=${cols} />
673
598
 
674
- ${/* ── 3a. Session pane ────────────────────────────────────────────── */null}
675
599
  ${activeTab === 0
676
600
  ? html`<${Box} flexDirection="column">
677
601
  <${Static} key=${clearEpoch} items=${staticItems}>
678
602
  ${(it) => it.type === 'banner'
679
- ? html`<${Box} key=${it.id} flexDirection="column" alignItems="center" marginBottom=${2}>
603
+ ? html`<${Box} key=${it.id} flexDirection="column" alignItems="center" marginBottom=${1}>
680
604
  <${Banner} cfg=${cfg} model=${model} width=${cols} compact=${compact} version=${version} cwd=${APP_CWD} />
681
605
  <//>`
682
606
  : renderItem(it)}
683
607
  <//>
684
- ${/* Gap between history and live area — gives breathing room */null}
685
608
  ${live
686
- ? html`<${Box} flexDirection="column" marginTop=${1}>
609
+ ? html`<${Box} flexDirection="column">
687
610
  <${StatusLines} lines=${live.status} busy=${busy} />
688
611
  ${showReasoning && live.reasoning ? html`<${Reasoning} text=${live.reasoning} />` : null}
689
612
  ${live.reply?.steps ? html`<${Plan} reply=${live.reply} live=${true} />` : null}
@@ -700,24 +623,23 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, ve
700
623
  : null}
701
624
  <//>`
702
625
  : null}
703
-
704
- ${/* ── 3b. Other tabs ──────────────────────────────────────────────── */null}
705
626
  ${activeTab === 1 ? html`<${MonitoringView} cfg=${cfg} />` : null}
706
627
  ${activeTab === 2 ? html`<${IssuesView} issues=${issuesRef.current} />` : null}
707
628
  ${activeTab === 3 ? html`<${GistsView} cfg=${cfg} />` : null}
708
629
 
709
- ${/* ── 4. Composer + footer — fixed at bottom ─────────────────────── */null}
710
630
  <${Box} flexDirection="column" marginTop=${1}>
711
631
  ${activeTab === 0
712
632
  ? (choice
713
633
  ? html`<${Choice} ...${choice} />`
714
634
  : html`<${Composer} onSubmit=${onSubmit} busy=${busy} width=${cols} prefill=${prefillText} onPrefillConsumed=${() => setPrefillText(null)} />`)
715
- : html`<${Box} paddingX=${1}><${Text} dimColor>Click a tab or Ctrl+1–4 to switch · Ctrl+1 for Session<//> <//>` }
635
+ : html`<${Box} paddingX=${1}><${Text} dimColor>Ctrl+1 for Session · Ctrl+2/3/4 for other tabs<//> <//>` }
716
636
  <${Box} paddingX=${1} width=${cols}>
717
637
  <${Box} flexGrow=${1}>
718
- <${Text} dimColor wrap="truncate-end">${hint ? hint : baseHint}<//><//>
719
- ${autopilot
720
- ? html`<${Text} color=${C.yellow} bold wrap="truncate-end"> ${glyphs.bolt} autopilot ON<//>` : null}
638
+ <${Text} dimColor wrap="truncate-end">${ctx}<//>
639
+ <//>
640
+ ${latestVersion?.isNewer
641
+ ? html`<${Text} color=${C.yellow} wrap="truncate-end">🔔 v${latestVersion.latest} available — /update<//>`
642
+ : html`<${Text} color=${footerColor} dimColor=${!footerColor} wrap="truncate-end">${footerHint}<//>` }
721
643
  <//>
722
644
  <//>
723
645
  <//>`;
package/src/tui/run.js CHANGED
@@ -3,25 +3,14 @@
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
11
  const { display = {}, onExternal, initialQuestion, version = '', resumeSession = null } = 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}
@@ -36,8 +25,5 @@ export async function runTui(cfg, opts = {}) {
36
25
 
37
26
  await instance.waitUntilExit();
38
27
 
39
- disableMouse();
40
- unpatchMouse();
41
-
42
28
  return externalAction;
43
29
  }
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
- }