ispbills-icli 8.4.10 → 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 +1 -1
- package/src/client.js +6 -0
- package/src/logger.js +23 -0
- package/src/tui/app.js +18 -77
- package/src/tui/run.js +0 -14
- package/src/tui/mouse.js +0 -68
package/package.json
CHANGED
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,43 +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 occupies row 1 (1-indexed y). Each tab is: 2-char prefix + label
|
|
135
|
-
// + marginRight=2. paddingX=1 on the outer Box shifts all tabs one column
|
|
136
|
-
// right, so column 2 is where the first tab starts.
|
|
137
|
-
const tabClickRanges = useMemo(() => {
|
|
138
|
-
let x = 2; // paddingX=1 → first content starts at column 2 (1-indexed)
|
|
139
|
-
return TABS.map((label) => {
|
|
140
|
-
const start = x;
|
|
141
|
-
const w = 2 + label.length; // ' ' or '❯ ' prefix + label chars
|
|
142
|
-
x += w + 2; // +2 for marginRight
|
|
143
|
-
return { start, end: start + w - 1 };
|
|
144
|
-
});
|
|
145
|
-
}, []);
|
|
146
|
-
|
|
147
|
-
useEffect(() => {
|
|
148
|
-
const TAB_ROW = 1; // y=1 in SGR coords (first row)
|
|
149
|
-
|
|
150
|
-
const onMouse = ({ btn, x, y, press }) => {
|
|
151
|
-
// ── Scroll wheel UP → scroll the terminal viewport to reveal history ──
|
|
152
|
-
if (btn === 64) {
|
|
153
|
-
try { process.stdout.write('\x1b[3S'); } catch {}
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
// ── Scroll wheel DOWN → swallowed (no drifting below live area) ────────
|
|
157
|
-
if (btn === 65) return;
|
|
158
|
-
|
|
159
|
-
// ── Left click (btn=0, press) on tab bar row ──────────────────────────
|
|
160
|
-
if (btn === 0 && press && y === TAB_ROW) {
|
|
161
|
-
const idx = tabClickRanges.findIndex((r) => x >= r.start && x <= r.end);
|
|
162
|
-
if (idx >= 0) setActiveTab(idx);
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
|
|
166
|
-
mouseEvents.on('event', onMouse);
|
|
167
|
-
return () => mouseEvents.off('event', onMouse);
|
|
168
|
-
}, [tabClickRanges]);
|
|
169
|
-
|
|
170
126
|
const setAutopilotMode = useCallback((next) => {
|
|
171
127
|
apRef.current = next;
|
|
172
128
|
setAutopilot(next);
|
|
@@ -619,52 +575,38 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, ve
|
|
|
619
575
|
};
|
|
620
576
|
|
|
621
577
|
const ctx = [model ? shortModel(model) : null, APP_CWD, cfg.user?.name, cfg.user?.role].filter(Boolean).join(midDot());
|
|
622
|
-
// Footer
|
|
623
|
-
|
|
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;
|
|
624
586
|
const compact = cols < 60;
|
|
625
587
|
|
|
626
588
|
// The banner is the first <Static> item so it prints once at the very top and
|
|
627
589
|
// scrolls away with history; completed transcript items follow it. Ink flushes
|
|
628
590
|
// each <Static> item to the terminal's normal buffer exactly once, leaving the
|
|
629
591
|
// native scrollback intact so earlier conversation can be scrolled back to.
|
|
630
|
-
// Scroll UP (mouse wheel) reveals this history; scroll DOWN is blocked.
|
|
631
592
|
const staticItems = [{ type: 'banner', id: `banner-${clearEpoch}` }, ...items];
|
|
632
593
|
|
|
633
|
-
// ── Main render ─────────────────────────────────────────────────────────────
|
|
634
|
-
// Layout (top to bottom):
|
|
635
|
-
// 1. Compact persistent header — "iCopilot" brand always visible
|
|
636
|
-
// 2. Tab bar — Session / Monitoring / Issues / Gists (mouse-clickable)
|
|
637
|
-
// 3. Session pane — Static history + live streaming area (scrollback via ↑)
|
|
638
|
-
// 4. Composer + footer — always pinned to the bottom
|
|
639
594
|
return html`
|
|
640
595
|
<${Box} flexDirection="column" width=${cols}>
|
|
641
|
-
|
|
642
|
-
${/* ── 1. Compact header: brand + active context ─────────────────── */null}
|
|
643
|
-
<${Box} paddingX=${1} paddingTop=${0}>
|
|
644
|
-
<${Text} color=${C.accent} bold>iCopilot<//>
|
|
645
|
-
<${Text} color=${C.gray}> ${dash()} <//>
|
|
646
|
-
<${Text} dimColor>${ctx}<//>
|
|
647
|
-
${latestVersion?.isNewer
|
|
648
|
-
? html`<${Box} flexGrow=${1} justifyContent="flex-end"><${Text} color=${C.yellow}> 🔔 v${latestVersion.latest} /update<//> <//>`
|
|
649
|
-
: null}
|
|
650
|
-
<//>
|
|
651
|
-
|
|
652
|
-
${/* ── 2. Tab bar (mouse-clickable + Ctrl+1–4) ──────────────────── */null}
|
|
596
|
+
${/* Tab bar — always pinned above content */null}
|
|
653
597
|
<${TabBar} tabs=${TABS} active=${activeTab} onSelect=${setActiveTab} width=${cols} />
|
|
654
598
|
|
|
655
|
-
${/* ── 3a. Session pane ────────────────────────────────────────────── */null}
|
|
656
599
|
${activeTab === 0
|
|
657
600
|
? html`<${Box} flexDirection="column">
|
|
658
601
|
<${Static} key=${clearEpoch} items=${staticItems}>
|
|
659
602
|
${(it) => it.type === 'banner'
|
|
660
|
-
? html`<${Box} key=${it.id} flexDirection="column" alignItems="center" marginBottom=${
|
|
603
|
+
? html`<${Box} key=${it.id} flexDirection="column" alignItems="center" marginBottom=${1}>
|
|
661
604
|
<${Banner} cfg=${cfg} model=${model} width=${cols} compact=${compact} version=${version} cwd=${APP_CWD} />
|
|
662
605
|
<//>`
|
|
663
606
|
: renderItem(it)}
|
|
664
607
|
<//>
|
|
665
|
-
${/* Gap between history and live area — gives breathing room */null}
|
|
666
608
|
${live
|
|
667
|
-
? html`<${Box} flexDirection="column"
|
|
609
|
+
? html`<${Box} flexDirection="column">
|
|
668
610
|
<${StatusLines} lines=${live.status} busy=${busy} />
|
|
669
611
|
${showReasoning && live.reasoning ? html`<${Reasoning} text=${live.reasoning} />` : null}
|
|
670
612
|
${live.reply?.steps ? html`<${Plan} reply=${live.reply} live=${true} />` : null}
|
|
@@ -681,24 +623,23 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, ve
|
|
|
681
623
|
: null}
|
|
682
624
|
<//>`
|
|
683
625
|
: null}
|
|
684
|
-
|
|
685
|
-
${/* ── 3b. Other tabs ──────────────────────────────────────────────── */null}
|
|
686
626
|
${activeTab === 1 ? html`<${MonitoringView} cfg=${cfg} />` : null}
|
|
687
627
|
${activeTab === 2 ? html`<${IssuesView} issues=${issuesRef.current} />` : null}
|
|
688
628
|
${activeTab === 3 ? html`<${GistsView} cfg=${cfg} />` : null}
|
|
689
629
|
|
|
690
|
-
${/* ── 4. Composer + footer — fixed at bottom ─────────────────────── */null}
|
|
691
630
|
<${Box} flexDirection="column" marginTop=${1}>
|
|
692
631
|
${activeTab === 0
|
|
693
632
|
? (choice
|
|
694
633
|
? html`<${Choice} ...${choice} />`
|
|
695
634
|
: html`<${Composer} onSubmit=${onSubmit} busy=${busy} width=${cols} prefill=${prefillText} onPrefillConsumed=${() => setPrefillText(null)} />`)
|
|
696
|
-
: html`<${Box} paddingX=${1}><${Text} dimColor>
|
|
635
|
+
: html`<${Box} paddingX=${1}><${Text} dimColor>Ctrl+1 for Session · Ctrl+2/3/4 for other tabs<//> <//>` }
|
|
697
636
|
<${Box} paddingX=${1} width=${cols}>
|
|
698
637
|
<${Box} flexGrow=${1}>
|
|
699
|
-
<${Text} dimColor wrap="truncate-end">${
|
|
700
|
-
|
|
701
|
-
|
|
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}<//>` }
|
|
702
643
|
<//>
|
|
703
644
|
<//>
|
|
704
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
|
-
}
|