ispbills-icli 8.7.2 → 8.7.4
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/tui/app.js +20 -15
- package/src/tui/components.js +150 -48
- package/src/tui/composer.js +5 -0
- package/src/tui/run.js +32 -13
package/package.json
CHANGED
package/src/tui/app.js
CHANGED
|
@@ -1106,16 +1106,18 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
1106
1106
|
// Show interactive model picker
|
|
1107
1107
|
const cur = model || modelRef.current || '';
|
|
1108
1108
|
const MODELS = [
|
|
1109
|
-
{ id: 'gpt-4o-mini',
|
|
1110
|
-
{ id: 'openai/gpt-4o',
|
|
1111
|
-
{ id: 'openai/gpt-4.1',
|
|
1112
|
-
{ id: 'openai/
|
|
1113
|
-
{ id: 'openai/o3
|
|
1114
|
-
{ id: '
|
|
1115
|
-
{ id: 'anthropic/claude-
|
|
1116
|
-
{ id: 'anthropic/claude-3-
|
|
1117
|
-
{ id: '
|
|
1118
|
-
{ id: '
|
|
1109
|
+
{ id: 'gpt-4o-mini', desc: 'Fast & affordable (default)' },
|
|
1110
|
+
{ id: 'openai/gpt-4o', desc: 'OpenAI GPT-4o — balanced' },
|
|
1111
|
+
{ id: 'openai/gpt-4.1', desc: 'OpenAI GPT-4.1 — latest' },
|
|
1112
|
+
{ id: 'openai/gpt-5.3-codex', desc: 'GPT-5.3 Codex — advanced coding' },
|
|
1113
|
+
{ id: 'openai/o3', desc: 'OpenAI o3 — reasoning' },
|
|
1114
|
+
{ id: 'openai/o3-mini', desc: 'OpenAI o3-mini — fast reasoning' },
|
|
1115
|
+
{ id: 'anthropic/claude-sonnet-4-6', desc: 'Claude Sonnet 4.6 — latest (recommended)' },
|
|
1116
|
+
{ id: 'anthropic/claude-3-5-sonnet', desc: 'Claude 3.5 Sonnet — strong' },
|
|
1117
|
+
{ id: 'anthropic/claude-3-opus', desc: 'Claude 3 Opus — advanced' },
|
|
1118
|
+
{ id: 'anthropic/claude-3-haiku', desc: 'Claude 3 Haiku — fast' },
|
|
1119
|
+
{ id: 'meta/llama-3.3-70b-instruct', desc: 'Llama 3.3 70B (open)' },
|
|
1120
|
+
{ id: 'mistral/mistral-large', desc: 'Mistral Large' },
|
|
1119
1121
|
];
|
|
1120
1122
|
choiceActionRef.current = (val) => {
|
|
1121
1123
|
if (val) {
|
|
@@ -2130,9 +2132,12 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
2130
2132
|
? `${glyphs.spinner} Working\u2026`
|
|
2131
2133
|
: hint
|
|
2132
2134
|
? hint
|
|
2133
|
-
:
|
|
2134
|
-
|
|
2135
|
-
|
|
2135
|
+
: activeTab !== 'session'
|
|
2136
|
+
? `Enter → Session ${midDot()} r refresh ${midDot()} Tab next tab ${midDot()} 1-9 insert command`
|
|
2137
|
+
: `? help ${midDot()} / commands ${midDot()} @ mention ${midDot()} \u2191\u2193 history ${midDot()} Tab mode`;
|
|
2138
|
+
const footerStatusColor = busy ? C.warning : hint ? C.accent : C.brand;
|
|
2139
|
+
const footerStatusRight = busy ? `Ctrl+C cancel` : mode === 'autopilot' ? 'AUTOPILOT' : mode === 'plan' ? 'PLAN' : '';
|
|
2140
|
+
const footerStatusRightColor = busy ? C.warning : mode === 'autopilot' ? C.warning : mode === 'plan' ? C.accent : C.muted;
|
|
2136
2141
|
const composerMode = shellMode ? 'shell' : mode;
|
|
2137
2142
|
|
|
2138
2143
|
// ── Viewport slicing ───────────────────────────────────────────────────────
|
|
@@ -2252,9 +2257,9 @@ export function App({ cfg, display = {}, onExternal, onExit, initialQuestion, sh
|
|
|
2252
2257
|
|
|
2253
2258
|
<${Box} paddingX=${1} width=${cols}>
|
|
2254
2259
|
<${Box} flexGrow=${1}>
|
|
2255
|
-
<${Text} color=${footerStatusColor} wrap="truncate-end">${footerStatusLeft}<//>
|
|
2260
|
+
<${Text} color=${footerStatusColor} bold=${busy} wrap="truncate-end">${footerStatusLeft}<//>
|
|
2256
2261
|
<//>
|
|
2257
|
-
<${Text} color=${
|
|
2262
|
+
<${Text} color=${footerStatusRightColor} bold wrap="truncate-end">${footerStatusRight}<//>
|
|
2258
2263
|
<//>
|
|
2259
2264
|
|
|
2260
2265
|
<${Box} paddingX=${1} width=${cols}>
|
package/src/tui/components.js
CHANGED
|
@@ -614,6 +614,20 @@ function statusCounts(list) {
|
|
|
614
614
|
return { on, off, other: list.length - on - off };
|
|
615
615
|
}
|
|
616
616
|
|
|
617
|
+
/** Format a timestamp as a relative "Xm ago" / "Xh ago" string. */
|
|
618
|
+
function fmtAgo(ts) {
|
|
619
|
+
if (!ts) return '';
|
|
620
|
+
const diff = Date.now() - new Date(ts).getTime();
|
|
621
|
+
if (isNaN(diff) || diff < 0) return '';
|
|
622
|
+
const s = Math.floor(diff / 1000);
|
|
623
|
+
if (s < 90) return `${s}s ago`;
|
|
624
|
+
const m = Math.floor(s / 60);
|
|
625
|
+
if (m < 90) return `${m}m ago`;
|
|
626
|
+
const h = Math.floor(m / 60);
|
|
627
|
+
if (h < 48) return `${h}h ago`;
|
|
628
|
+
return `${Math.floor(h / 24)}d ago`;
|
|
629
|
+
}
|
|
630
|
+
|
|
617
631
|
// ── DevicesPanel ─────────────────────────────────────────────────────────────
|
|
618
632
|
|
|
619
633
|
export function DevicesPanel({ data, loading, error, cols }) {
|
|
@@ -658,56 +672,95 @@ export function DevicesPanel({ data, loading, error, cols }) {
|
|
|
658
672
|
<//>
|
|
659
673
|
<//>`;
|
|
660
674
|
|
|
661
|
-
|
|
662
|
-
const
|
|
663
|
-
const
|
|
664
|
-
const
|
|
675
|
+
// Compute column widths based on terminal width
|
|
676
|
+
const nameW = Math.max(20, Math.floor(cols * 0.32));
|
|
677
|
+
const ipW = 17;
|
|
678
|
+
const statusW = 9; // ' ONLINE ' / ' OFFLINE ' / ' UNKNOWN '
|
|
679
|
+
const maxRowsPerGroup = Math.max(5, cols > 100 ? 14 : 10);
|
|
665
680
|
|
|
666
681
|
const ts = data.fetchedAt
|
|
667
682
|
? new Date(data.fetchedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
668
683
|
: '';
|
|
669
684
|
|
|
685
|
+
// Summary line: total counts across all device types
|
|
686
|
+
const totals = groups.reduce((acc, g) => {
|
|
687
|
+
const { on, off, other } = statusCounts(g.items);
|
|
688
|
+
acc.on += on; acc.off += off; acc.other += other; acc.total += g.items.length;
|
|
689
|
+
return acc;
|
|
690
|
+
}, { on: 0, off: 0, other: 0, total: 0 });
|
|
691
|
+
|
|
670
692
|
return html`
|
|
671
693
|
<${Box} flexDirection="column">
|
|
672
694
|
${header}
|
|
695
|
+
|
|
696
|
+
<!-- Fleet summary bar -->
|
|
697
|
+
<${Box} paddingX=${2} marginBottom=${1}>
|
|
698
|
+
<${Text} color=${C.muted}>Total <//>
|
|
699
|
+
<${Text} bold>${totals.total}<//>
|
|
700
|
+
<${Text} color=${C.muted}> devices <//>
|
|
701
|
+
<${Text} color=${C.success} bold>● ${totals.on} online<//>
|
|
702
|
+
<${Text} color=${C.muted}> <//>
|
|
703
|
+
<${Text} color=${C.error} bold>○ ${totals.off} offline<//>
|
|
704
|
+
${totals.other > 0 ? html`<${Text} color=${C.warning} bold> ◌ ${totals.other} unknown<//>` : null}
|
|
705
|
+
<//>
|
|
706
|
+
|
|
673
707
|
<${Box} flexDirection="column" paddingX=${2} paddingY=${0}>
|
|
674
708
|
${groups.map((g) => {
|
|
675
709
|
const { on, off, other } = statusCounts(g.items);
|
|
710
|
+
// Sort: online first, unknown second, offline last
|
|
676
711
|
const sorted = [...g.items].sort((a, b) => {
|
|
677
712
|
const order = { online: 0, unknown: 1, offline: 2 };
|
|
678
713
|
return (order[devStatus(a)] ?? 1) - (order[devStatus(b)] ?? 1);
|
|
679
714
|
});
|
|
680
715
|
const shown = sorted.slice(0, maxRowsPerGroup);
|
|
681
716
|
const hidden = sorted.length - shown.length;
|
|
717
|
+
|
|
682
718
|
return html`
|
|
683
719
|
<${Box} key=${g.label} flexDirection="column" marginTop=${1}>
|
|
720
|
+
<!-- Group header with status summary -->
|
|
684
721
|
<${Box}>
|
|
685
722
|
<${Text} color=${C.accent} bold>${g.icon} ${g.label}<//>
|
|
686
|
-
<${Text} color=${C.muted}> <//>
|
|
687
|
-
<${Text} color=${C.success}>${on}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
723
|
+
<${Text} color=${C.muted}> (${g.items.length}) <//>
|
|
724
|
+
<${Text} color=${C.success}>${on} online<//>
|
|
725
|
+
${off > 0 ? html`<${Text} color=${C.error}> ${off} offline<//>` : null}
|
|
726
|
+
${other > 0 ? html`<${Text} color=${C.warning}> ${other} unknown<//>` : null}
|
|
727
|
+
<//>
|
|
728
|
+
|
|
729
|
+
<!-- Column header -->
|
|
730
|
+
<${Box}>
|
|
731
|
+
<${Text} color=${C.muted}> ${'Name'.padEnd(nameW)}${'IP Address'.padEnd(ipW)}Status Checked<//>
|
|
691
732
|
<//>
|
|
733
|
+
|
|
692
734
|
${shown.map((d, i) => {
|
|
735
|
+
const st = devStatus(d);
|
|
693
736
|
const { dot, color } = statusDot(d);
|
|
694
737
|
const isLast = i === shown.length - 1 && hidden === 0;
|
|
695
738
|
const tree = isLast ? '└─' : '├─';
|
|
739
|
+
|
|
696
740
|
const name = (d.name || d.device_name || `#${d.id}`)
|
|
697
741
|
.slice(0, nameW - 1).padEnd(nameW);
|
|
698
742
|
const ip = (d.ip_address || '─').slice(0, ipW - 1).padEnd(ipW);
|
|
699
|
-
|
|
743
|
+
|
|
744
|
+
// Status badge
|
|
745
|
+
const badgeText = st === 'online' ? ' ONLINE ' : st === 'offline' ? ' OFFLINE' : ' UNKNOWN';
|
|
746
|
+
const badgeColor = st === 'online' ? C.success : st === 'offline' ? C.error : C.warning;
|
|
747
|
+
|
|
748
|
+
// Last-check time
|
|
749
|
+
const checkTs = d.last_check || d.last_seen || d.created_at;
|
|
750
|
+
const ago = fmtAgo(checkTs);
|
|
751
|
+
|
|
700
752
|
return html`
|
|
701
753
|
<${Box} key=${d.id}>
|
|
702
|
-
<${Text} color=${C.muted}>${tree}<//>
|
|
754
|
+
<${Text} color=${C.muted}>${tree} <//>
|
|
703
755
|
<${Text} color=${color}>${dot} <//>
|
|
704
756
|
<${Text}>${name}<//>
|
|
705
757
|
<${Text} color=${C.muted}>${ip}<//>
|
|
706
|
-
<${Text} color=${
|
|
758
|
+
<${Text} color=${badgeColor} bold>${badgeText}<//>
|
|
759
|
+
${ago ? html`<${Text} color=${C.muted} dimColor> ${ago}<//>` : null}
|
|
707
760
|
<//>`;
|
|
708
761
|
})}
|
|
709
762
|
${hidden > 0
|
|
710
|
-
? html`<${Box}><${Text} color=${C.muted} dimColor> └─ …and ${hidden} more<//> <//>`
|
|
763
|
+
? html`<${Box}><${Text} color=${C.muted} dimColor> └─ …and ${hidden} more (press r to refresh)<//> <//>`
|
|
711
764
|
: null}
|
|
712
765
|
<//>`;
|
|
713
766
|
})}
|
|
@@ -716,7 +769,7 @@ export function DevicesPanel({ data, loading, error, cols }) {
|
|
|
716
769
|
<${Text} color=${C.separator}>${'─'.repeat(Math.max(1, bw))}<//>
|
|
717
770
|
<//>
|
|
718
771
|
<${Box} paddingX=${2}>
|
|
719
|
-
<${Text} color=${C.muted} dimColor>r refresh${ts ? ' ' + midDot() + ' updated ' + ts : ''} ${midDot()} Enter → Session<//>
|
|
772
|
+
<${Text} color=${C.muted} dimColor>r refresh${ts ? ' ' + midDot() + ' updated ' + ts : ''} ${midDot()} ● online ○ offline ◌ unknown ${midDot()} Enter → Session<//>
|
|
720
773
|
<//>
|
|
721
774
|
<//>`;
|
|
722
775
|
}
|
|
@@ -751,81 +804,130 @@ export function MapsPanel({ data, loading, error, cols }) {
|
|
|
751
804
|
<//>`;
|
|
752
805
|
|
|
753
806
|
const tiers = [
|
|
754
|
-
{ label: 'Routers', items: data.routers, icon: '⬡', color: C.brand
|
|
755
|
-
{ label: 'OLTs', items: data.olts, icon: '◈', color: C.accent
|
|
756
|
-
{ label: 'Ubiquiti APs', items: data.ubiquiti, icon: '◉', color: C.success
|
|
757
|
-
{ label: 'Cambium APs', items: data.cambium, icon: '◉', color: C.warning
|
|
807
|
+
{ label: 'Routers', items: data.routers, icon: '⬡', color: C.brand },
|
|
808
|
+
{ label: 'OLTs', items: data.olts, icon: '◈', color: C.accent },
|
|
809
|
+
{ label: 'Ubiquiti APs', items: data.ubiquiti, icon: '◉', color: C.success },
|
|
810
|
+
{ label: 'Cambium APs', items: data.cambium, icon: '◉', color: C.warning },
|
|
758
811
|
].filter((t) => t.items.length > 0);
|
|
759
812
|
|
|
760
|
-
const maxPerTier =
|
|
761
|
-
const
|
|
813
|
+
const maxPerTier = cols > 100 ? 12 : 8;
|
|
814
|
+
const nameW = Math.max(18, Math.floor(cols * 0.28));
|
|
815
|
+
const ipW = 17;
|
|
816
|
+
|
|
817
|
+
// Build the total fleet summary
|
|
818
|
+
const fleet = tiers.reduce((acc, t) => {
|
|
819
|
+
const { on, off, other } = statusCounts(t.items);
|
|
820
|
+
acc.on += on; acc.off += off; acc.other += other; acc.total += t.items.length;
|
|
821
|
+
return acc;
|
|
822
|
+
}, { on: 0, off: 0, other: 0, total: 0 });
|
|
823
|
+
|
|
824
|
+
const ts = data.fetchedAt
|
|
825
|
+
? new Date(data.fetchedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
|
826
|
+
: '';
|
|
762
827
|
|
|
763
828
|
return html`
|
|
764
829
|
<${Box} flexDirection="column">
|
|
765
830
|
${header}
|
|
831
|
+
|
|
766
832
|
<${Box} flexDirection="column" paddingX=${2} paddingY=${0}>
|
|
767
833
|
|
|
768
|
-
|
|
769
|
-
|
|
834
|
+
<!-- Fleet summary -->
|
|
835
|
+
<${Box} marginTop=${1} marginBottom=${1}>
|
|
836
|
+
<${Text} color=${C.muted}>Fleet: <//>
|
|
837
|
+
<${Text} bold>${fleet.total} devices<//>
|
|
838
|
+
<${Text} color=${C.muted}> <//>
|
|
839
|
+
<${Text} color=${C.success} bold>● ${fleet.on} up<//>
|
|
840
|
+
<${Text} color=${C.muted}> <//>
|
|
841
|
+
<${Text} color=${C.error} bold>○ ${fleet.off} down<//>
|
|
842
|
+
${fleet.other > 0 ? html`<${Text} color=${C.warning} bold> ◌ ${fleet.other} unknown<//>` : null}
|
|
843
|
+
<//>
|
|
844
|
+
|
|
845
|
+
<!-- Internet cloud node -->
|
|
846
|
+
<${Box}>
|
|
847
|
+
<${Text} color=${C.muted}> ╔══════════════╗<//>
|
|
770
848
|
<//>
|
|
771
849
|
<${Box}>
|
|
772
|
-
<${Text} color=${C.muted}
|
|
773
|
-
<${Text} color=${C.brand} bold>
|
|
774
|
-
<${Text} color=${C.muted}
|
|
850
|
+
<${Text} color=${C.muted}> ║ <//>
|
|
851
|
+
<${Text} color=${C.brand} bold> INTERNET <//>
|
|
852
|
+
<${Text} color=${C.muted}> ║<//>
|
|
775
853
|
<//>
|
|
776
|
-
<${Box}
|
|
777
|
-
<${Text} color=${C.muted}
|
|
854
|
+
<${Box}>
|
|
855
|
+
<${Text} color=${C.muted}> ╚══════╤═══════╝<//>
|
|
778
856
|
<//>
|
|
779
857
|
|
|
858
|
+
<!-- Each tier rendered as a branch under the topology tree -->
|
|
780
859
|
${tiers.map((tier, ti) => {
|
|
781
|
-
const { on, off } = statusCounts(tier.items);
|
|
782
|
-
|
|
783
|
-
const
|
|
860
|
+
const { on, off, other } = statusCounts(tier.items);
|
|
861
|
+
// Sort: online first, offline last
|
|
862
|
+
const sorted = [...tier.items].sort((a, b) => {
|
|
863
|
+
const order = { online: 0, unknown: 1, offline: 2 };
|
|
864
|
+
return (order[devStatus(a)] ?? 1) - (order[devStatus(b)] ?? 1);
|
|
865
|
+
});
|
|
866
|
+
const shown = sorted.slice(0, maxPerTier);
|
|
867
|
+
const hidden = sorted.length - shown.length;
|
|
784
868
|
const isLast = ti === tiers.length - 1;
|
|
785
|
-
const nameW = Math.max(18, Math.floor(cols * 0.3));
|
|
786
869
|
|
|
787
870
|
return html`
|
|
788
871
|
<${Box} key=${tier.label} flexDirection="column">
|
|
872
|
+
|
|
873
|
+
<!-- Vertical connector to this tier -->
|
|
874
|
+
<${Box}>
|
|
875
|
+
<${Text} color=${C.muted}> │<//>
|
|
876
|
+
<//>
|
|
877
|
+
|
|
878
|
+
<!-- Tier header box -->
|
|
879
|
+
<${Box}>
|
|
880
|
+
<${Text} color=${C.muted}> ┌──────┴──────────────────────────────<//>
|
|
881
|
+
<//>
|
|
789
882
|
<${Box}>
|
|
790
|
-
<${Text} color=${C.muted}
|
|
883
|
+
<${Text} color=${C.muted}> │ <//>
|
|
884
|
+
<${Text} color=${tier.color} bold>${tier.icon} ${tier.label.padEnd(14)}<//>
|
|
885
|
+
<${Text} color=${C.success}> ● ${on} up<//>
|
|
886
|
+
${off > 0 ? html`<${Text} color=${C.error}> ○ ${off} down<//>` : null}
|
|
887
|
+
${other > 0 ? html`<${Text} color=${C.warning}> ◌ ${other} unk<//>` : null}
|
|
791
888
|
<//>
|
|
792
889
|
<${Box}>
|
|
793
|
-
<${Text} color=${C.muted}
|
|
794
|
-
<${Text} color=${tier.color} bold> ${tier.icon} ${tier.label} <//>
|
|
795
|
-
<${Text} color=${C.success}>${on}↑ <//>
|
|
796
|
-
<${Text} color=${C.error}>${off}↓<//>
|
|
890
|
+
<${Text} color=${C.muted}> └─┬────────────────────────────────<//>
|
|
797
891
|
<//>
|
|
892
|
+
|
|
893
|
+
<!-- Device rows -->
|
|
798
894
|
${shown.map((d, i) => {
|
|
895
|
+
const st = devStatus(d);
|
|
799
896
|
const { dot, color } = statusDot(d);
|
|
800
897
|
const isLastItem = i === shown.length - 1 && hidden === 0;
|
|
801
|
-
const
|
|
802
|
-
const name
|
|
803
|
-
const ip
|
|
804
|
-
const
|
|
898
|
+
const branch = isLastItem ? ' └─' : ' ├─';
|
|
899
|
+
const name = (d.name || d.device_name || `#${d.id}`).slice(0, nameW - 1).padEnd(nameW);
|
|
900
|
+
const ip = (d.ip_address || '─').slice(0, ipW - 1).padEnd(ipW);
|
|
901
|
+
const ago = fmtAgo(d.last_check || d.last_seen);
|
|
902
|
+
const badge = st === 'online' ? 'UP ' : st === 'offline' ? 'DOWN' : 'UNKN';
|
|
903
|
+
const badgeColor = st === 'online' ? C.success : st === 'offline' ? C.error : C.warning;
|
|
805
904
|
return html`
|
|
806
905
|
<${Box} key=${d.id}>
|
|
807
|
-
<${Text} color=${C.muted}
|
|
906
|
+
<${Text} color=${C.muted}> ${branch} <//>
|
|
808
907
|
<${Text} color=${color}>${dot} <//>
|
|
809
|
-
<${Text}
|
|
908
|
+
<${Text}>${name}<//>
|
|
810
909
|
<${Text} color=${C.muted}>${ip}<//>
|
|
811
|
-
<${Text} color=${
|
|
910
|
+
<${Text} color=${badgeColor} bold>[${badge}]<//>
|
|
911
|
+
${ago ? html`<${Text} color=${C.muted} dimColor> ${ago}<//>` : null}
|
|
812
912
|
<//>`;
|
|
813
913
|
})}
|
|
814
914
|
${hidden > 0
|
|
815
|
-
? html`<${Box}><${Text} color=${C.muted} dimColor
|
|
915
|
+
? html`<${Box}><${Text} color=${C.muted} dimColor> └─ …+${hidden} more<//> <//>`
|
|
816
916
|
: null}
|
|
817
|
-
${!isLast
|
|
818
|
-
? null
|
|
819
|
-
: html`<${Box}><${Text} color=${C.muted}>${indent} │ (end)<//> <//>` }
|
|
820
917
|
<//>`;
|
|
821
918
|
})}
|
|
822
919
|
|
|
920
|
+
<!-- End cap -->
|
|
921
|
+
<${Box} marginTop=${1}>
|
|
922
|
+
<${Text} color=${C.muted} dimColor> (topology end)<//>
|
|
923
|
+
<//>
|
|
823
924
|
<//>
|
|
925
|
+
|
|
824
926
|
<${Box} paddingX=${2} marginTop=${1}>
|
|
825
927
|
<${Text} color=${C.separator}>${'─'.repeat(Math.max(1, bw))}<//>
|
|
826
928
|
<//>
|
|
827
929
|
<${Box} paddingX=${2}>
|
|
828
|
-
<${Text} color=${C.muted} dimColor>r refresh ${midDot()}
|
|
930
|
+
<${Text} color=${C.muted} dimColor>r refresh${ts ? ' ' + midDot() + ' updated ' + ts : ''} ${midDot()} ● up ○ down ◌ unknown ${midDot()} /topology for AI analysis<//>
|
|
829
931
|
<//>
|
|
830
932
|
<//>`;
|
|
831
933
|
}
|
package/src/tui/composer.js
CHANGED
|
@@ -248,6 +248,11 @@ export function Composer({
|
|
|
248
248
|
};
|
|
249
249
|
|
|
250
250
|
useInput((input, key) => {
|
|
251
|
+
// Drop any mouse escape sequences that leaked through the stdin filter.
|
|
252
|
+
// SGR: \x1b[<N;N;N[Mm] X10: \x1b[M... URXVT: \x1b[N;N;NM
|
|
253
|
+
if (input && input[0] === '\x1b') {
|
|
254
|
+
if (/^\x1b\[(?:<\d|M|\d+;\d+;\d+M)/.test(input)) return;
|
|
255
|
+
}
|
|
251
256
|
const enter = key.return || input === '\r' || input === '\n';
|
|
252
257
|
const ctrlEnter = busy && key.ctrl && enter;
|
|
253
258
|
if (ctrlEnter || (busy && key.ctrl && input === 'q')) {
|
package/src/tui/run.js
CHANGED
|
@@ -26,20 +26,39 @@ class MouseFilterTransform extends Transform {
|
|
|
26
26
|
let out = '';
|
|
27
27
|
let i = 0;
|
|
28
28
|
while (i < this._buf.length) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
const c0 = this._buf[i];
|
|
30
|
+
const c1 = this._buf[i + 1]; // may be undefined at end of buffer
|
|
31
|
+
const c2 = this._buf[i + 2]; // may be undefined
|
|
32
|
+
|
|
33
|
+
if (c0 === '\x1b' && c1 === '[') {
|
|
34
|
+
// SGR mouse: \x1b[<N;N;N[Mm] (e.g. \x1b[<0;5;3M \x1b[<64;10;5M)
|
|
35
|
+
if (c2 === '<') {
|
|
36
|
+
const rest = this._buf.slice(i);
|
|
37
|
+
const m = rest.match(/^\x1b\[<\d+;\d+;\d+[Mm]/);
|
|
38
|
+
if (m) { i += m[0].length; continue; }
|
|
39
|
+
if (rest.length < 40) break; // wait for more bytes (max SGR seq ~20 chars)
|
|
40
|
+
out += this._buf[i++]; // give up on this ESC, pass through
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
// X10 mouse: \x1b[M + 3 raw bytes (total 6 bytes)
|
|
44
|
+
if (c2 === 'M') {
|
|
45
|
+
if (i + 6 <= this._buf.length) { i += 6; continue; }
|
|
46
|
+
break; // incomplete X10 sequence — wait
|
|
47
|
+
}
|
|
48
|
+
// URXVT extended mouse: \x1b[N;N;NM (digits without '<', ends with M)
|
|
49
|
+
if (c2 !== undefined && c2 >= '0' && c2 <= '9') {
|
|
50
|
+
const rest = this._buf.slice(i);
|
|
51
|
+
const mu = rest.match(/^\x1b\[\d+;\d+;\d+M/);
|
|
52
|
+
if (mu) { i += mu[0].length; continue; }
|
|
53
|
+
// partial — if short, wait for more data
|
|
54
|
+
if (rest.length < 20) break;
|
|
55
|
+
// Not a mouse seq — fall through to pass through ESC
|
|
56
|
+
}
|
|
57
|
+
// Focus-gained/lost: \x1b[I \x1b[O — these look like keys in some
|
|
58
|
+
// terminals but are focus events; drop them silently.
|
|
59
|
+
if (c2 === 'I' || c2 === 'O') { i += 3; continue; }
|
|
42
60
|
}
|
|
61
|
+
|
|
43
62
|
out += this._buf[i++];
|
|
44
63
|
}
|
|
45
64
|
this._buf = this._buf.slice(i);
|