nikcli-remote 1.0.7 → 1.0.9

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.
@@ -1661,292 +1661,649 @@ function getWebClient() {
1661
1661
  <html lang="en">
1662
1662
  <head>
1663
1663
  <meta charset="UTF-8">
1664
- <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
1664
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
1665
1665
  <meta name="apple-mobile-web-app-capable" content="yes">
1666
- <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
1666
+ <meta name="mobile-web-app-capable" content="yes">
1667
+ <meta name="theme-color" content="#0d1117">
1667
1668
  <title>NikCLI Remote</title>
1668
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
1669
1669
  <style>
1670
- * { box-sizing: border-box; margin: 0; padding: 0; }
1671
1670
  :root {
1672
- --bg-primary: #0d1117;
1671
+ --bg: #0d1117;
1673
1672
  --bg-secondary: #161b22;
1673
+ --fg: #e6edf3;
1674
+ --fg-muted: #8b949e;
1674
1675
  --accent: #58a6ff;
1675
1676
  --success: #3fb950;
1677
+ --warning: #d29922;
1678
+ --error: #f85149;
1676
1679
  --border: #30363d;
1680
+ --font-mono: 'SF Mono', 'Fira Code', 'Consolas', monospace;
1677
1681
  }
1678
- html, body { height: 100%; overflow: hidden; touch-action: manipulation; }
1679
- body {
1680
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1681
- background: var(--bg-primary);
1682
- color: #e6edf3;
1682
+
1683
+ * {
1684
+ box-sizing: border-box;
1685
+ margin: 0;
1686
+ padding: 0;
1687
+ -webkit-tap-highlight-color: transparent;
1688
+ }
1689
+
1690
+ html, body {
1691
+ height: 100%;
1692
+ background: var(--bg);
1693
+ color: var(--fg);
1694
+ font-family: var(--font-mono);
1695
+ font-size: 14px;
1696
+ overflow: hidden;
1697
+ touch-action: manipulation;
1698
+ }
1699
+
1700
+ #app {
1683
1701
  display: flex;
1684
1702
  flex-direction: column;
1703
+ height: 100%;
1704
+ height: 100dvh;
1685
1705
  }
1686
- #terminal { flex: 1; overflow: hidden; background: var(--bg-primary); padding: 8px; }
1687
- #input-area {
1688
- background: var(--bg-secondary);
1689
- border-top: 1px solid var(--border);
1706
+
1707
+ /* Header */
1708
+ #header {
1709
+ display: flex;
1710
+ align-items: center;
1711
+ justify-content: space-between;
1690
1712
  padding: 12px 16px;
1713
+ background: var(--bg-secondary);
1714
+ border-bottom: 1px solid var(--border);
1691
1715
  flex-shrink: 0;
1692
- padding-bottom: env(safe-area-inset-bottom, 12px);
1693
1716
  }
1694
- .input-row { display: flex; gap: 8px; align-items: center; }
1695
- .prompt { color: var(--success); font-family: 'SF Mono', Monaco, monospace; font-size: 14px; white-space: nowrap; }
1696
- #cmd-input {
1717
+
1718
+ #header h1 {
1719
+ font-size: 16px;
1720
+ font-weight: 600;
1721
+ color: var(--accent);
1722
+ display: flex;
1723
+ align-items: center;
1724
+ gap: 8px;
1725
+ }
1726
+
1727
+ #header h1::before {
1728
+ content: '';
1729
+ width: 10px;
1730
+ height: 10px;
1731
+ background: var(--accent);
1732
+ border-radius: 2px;
1733
+ }
1734
+
1735
+ #status {
1736
+ display: flex;
1737
+ align-items: center;
1738
+ gap: 6px;
1739
+ font-size: 12px;
1740
+ color: var(--fg-muted);
1741
+ }
1742
+
1743
+ #status-dot {
1744
+ width: 8px;
1745
+ height: 8px;
1746
+ border-radius: 50%;
1747
+ background: var(--error);
1748
+ transition: background 0.3s;
1749
+ }
1750
+
1751
+ #status-dot.connected {
1752
+ background: var(--success);
1753
+ }
1754
+
1755
+ #status-dot.connecting {
1756
+ background: var(--warning);
1757
+ animation: pulse 1s infinite;
1758
+ }
1759
+
1760
+ @keyframes pulse {
1761
+ 0%, 100% { opacity: 1; }
1762
+ 50% { opacity: 0.5; }
1763
+ }
1764
+
1765
+ /* Terminal */
1766
+ #terminal-container {
1697
1767
  flex: 1;
1698
- background: #21262d;
1768
+ overflow: hidden;
1769
+ position: relative;
1770
+ }
1771
+
1772
+ #terminal {
1773
+ height: 100%;
1774
+ padding: 12px;
1775
+ overflow-y: auto;
1776
+ overflow-x: hidden;
1777
+ font-size: 13px;
1778
+ line-height: 1.5;
1779
+ white-space: pre-wrap;
1780
+ word-break: break-all;
1781
+ -webkit-overflow-scrolling: touch;
1782
+ }
1783
+
1784
+ #terminal::-webkit-scrollbar {
1785
+ width: 6px;
1786
+ }
1787
+
1788
+ #terminal::-webkit-scrollbar-track {
1789
+ background: var(--bg);
1790
+ }
1791
+
1792
+ #terminal::-webkit-scrollbar-thumb {
1793
+ background: var(--border);
1794
+ border-radius: 3px;
1795
+ }
1796
+
1797
+ .cursor {
1798
+ display: inline-block;
1799
+ width: 8px;
1800
+ height: 16px;
1801
+ background: var(--fg);
1802
+ animation: blink 1s step-end infinite;
1803
+ vertical-align: text-bottom;
1804
+ }
1805
+
1806
+ @keyframes blink {
1807
+ 50% { opacity: 0; }
1808
+ }
1809
+
1810
+ /* Notifications */
1811
+ #notifications {
1812
+ position: fixed;
1813
+ top: 60px;
1814
+ left: 12px;
1815
+ right: 12px;
1816
+ z-index: 1000;
1817
+ pointer-events: none;
1818
+ }
1819
+
1820
+ .notification {
1821
+ background: var(--bg-secondary);
1699
1822
  border: 1px solid var(--border);
1700
- border-radius: 12px;
1823
+ border-radius: 8px;
1701
1824
  padding: 12px 16px;
1702
- color: #e6edf3;
1825
+ margin-bottom: 8px;
1826
+ animation: slideIn 0.3s ease;
1827
+ pointer-events: auto;
1828
+ box-shadow: 0 4px 12px rgba(0,0,0,0.4);
1829
+ }
1830
+
1831
+ .notification.success { border-left: 3px solid var(--success); }
1832
+ .notification.error { border-left: 3px solid var(--error); }
1833
+ .notification.warning { border-left: 3px solid var(--warning); }
1834
+ .notification.info { border-left: 3px solid var(--accent); }
1835
+
1836
+ .notification h4 {
1837
+ font-size: 14px;
1838
+ font-weight: 600;
1839
+ margin-bottom: 4px;
1840
+ }
1841
+
1842
+ .notification p {
1843
+ font-size: 12px;
1844
+ color: var(--fg-muted);
1845
+ }
1846
+
1847
+ @keyframes slideIn {
1848
+ from { transform: translateY(-20px); opacity: 0; }
1849
+ to { transform: translateY(0); opacity: 1; }
1850
+ }
1851
+
1852
+ /* Quick Keys */
1853
+ #quickkeys {
1854
+ display: grid;
1855
+ grid-template-columns: repeat(6, 1fr);
1856
+ gap: 6px;
1857
+ padding: 8px 12px;
1858
+ background: var(--bg-secondary);
1859
+ border-top: 1px solid var(--border);
1860
+ flex-shrink: 0;
1861
+ }
1862
+
1863
+ .qkey {
1864
+ background: var(--bg);
1865
+ border: 1px solid var(--border);
1866
+ border-radius: 6px;
1867
+ padding: 10px 4px;
1868
+ color: var(--fg);
1869
+ font-size: 11px;
1870
+ font-family: var(--font-mono);
1871
+ text-align: center;
1872
+ cursor: pointer;
1873
+ user-select: none;
1874
+ transition: background 0.1s, transform 0.1s;
1875
+ }
1876
+
1877
+ .qkey:active {
1878
+ background: var(--border);
1879
+ transform: scale(0.95);
1880
+ }
1881
+
1882
+ .qkey.wide {
1883
+ grid-column: span 2;
1884
+ }
1885
+
1886
+ .qkey.accent {
1887
+ background: var(--accent);
1888
+ border-color: var(--accent);
1889
+ color: #fff;
1890
+ }
1891
+
1892
+ /* Input */
1893
+ #input-container {
1894
+ padding: 12px;
1895
+ background: var(--bg-secondary);
1896
+ border-top: 1px solid var(--border);
1897
+ flex-shrink: 0;
1898
+ }
1899
+
1900
+ #input-row {
1901
+ display: flex;
1902
+ gap: 8px;
1903
+ }
1904
+
1905
+ #input {
1906
+ flex: 1;
1907
+ background: var(--bg);
1908
+ border: 1px solid var(--border);
1909
+ border-radius: 8px;
1910
+ padding: 12px 14px;
1911
+ color: var(--fg);
1912
+ font-family: var(--font-mono);
1703
1913
  font-size: 16px;
1704
- font-family: 'SF Mono', Monaco, monospace;
1705
1914
  outline: none;
1706
- -webkit-appearance: none;
1915
+ transition: border-color 0.2s;
1916
+ }
1917
+
1918
+ #input:focus {
1919
+ border-color: var(--accent);
1920
+ }
1921
+
1922
+ #input::placeholder {
1923
+ color: var(--fg-muted);
1707
1924
  }
1708
- #cmd-input:focus { border-color: var(--accent); }
1709
- #send-btn {
1925
+
1926
+ #send {
1710
1927
  background: var(--accent);
1711
- color: white;
1928
+ color: #fff;
1712
1929
  border: none;
1713
- border-radius: 12px;
1714
- padding: 12px 24px;
1715
- font-size: 16px;
1930
+ border-radius: 8px;
1931
+ padding: 12px 20px;
1932
+ font-size: 14px;
1716
1933
  font-weight: 600;
1934
+ font-family: var(--font-mono);
1717
1935
  cursor: pointer;
1718
- -webkit-tap-highlight-color: transparent;
1936
+ transition: opacity 0.2s, transform 0.1s;
1719
1937
  }
1720
- #send-btn:active { opacity: 0.7; }
1721
- #status-bar {
1722
- background: var(--bg-secondary);
1723
- border-bottom: 1px solid var(--border);
1724
- padding: 8px 16px;
1938
+
1939
+ #send:active {
1940
+ opacity: 0.8;
1941
+ transform: scale(0.98);
1942
+ }
1943
+
1944
+ /* Auth Screen */
1945
+ #auth-screen {
1946
+ position: fixed;
1947
+ inset: 0;
1948
+ background: var(--bg);
1725
1949
  display: flex;
1726
- justify-content: space-between;
1950
+ flex-direction: column;
1727
1951
  align-items: center;
1728
- font-size: 12px;
1729
- padding-top: env(safe-area-inset-top, 8px);
1730
- }
1731
- .status-row { display: flex; align-items: center; gap: 8px; }
1732
- .status-dot { width: 8px; height: 8px; border-radius: 50%; background: #8b949e; }
1733
- .status-dot.connected { background: var(--success); box-shadow: 0 0 8px var(--success); }
1734
- .status-dot.connecting { background: var(--accent); animation: pulse 1s infinite; }
1735
- .status-dot.disconnected { background: #f85149; }
1736
- @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
1737
- #auth-overlay {
1738
- position: fixed; inset: 0; background: var(--bg-primary);
1739
- display: flex; flex-direction: column; align-items: center; justify-content: center;
1740
- padding: 20px; z-index: 100;
1741
- }
1742
- #auth-overlay.hidden { display: none; }
1743
- .auth-title { font-size: 28px; font-weight: 700; margin-bottom: 8px; }
1744
- .auth-subtitle { color: #8b949e; font-size: 16px; margin-bottom: 24px; }
1745
- .auth-msg {
1746
- background: var(--bg-secondary); padding: 20px 28px;
1747
- border-radius: 16px; border: 1px solid var(--border); text-align: center;
1748
- }
1749
- .auth-msg.error { color: #f85149; border-color: #f85149; }
1750
- .quick-btns {
1751
- display: flex; gap: 8px; margin-top: 20px; flex-wrap: wrap; justify-content: center;
1752
- }
1753
- .quick-btn {
1754
- background: #21262d; border: 1px solid var(--border); color: #e6edf3;
1755
- padding: 10px 16px; border-radius: 8px; font-size: 14px;
1756
- font-family: 'SF Mono', Monaco, monospace; cursor: pointer;
1757
- }
1758
- .quick-btn:active { background: #30363d; }
1759
- .hint { font-size: 12px; color: #8b949e; margin-top: 12px; }
1760
- @media (max-width: 600px) {
1761
- #input-area { padding: 10px 12px; }
1762
- .quick-btn { padding: 8px 12px; font-size: 12px; }
1952
+ justify-content: center;
1953
+ gap: 20px;
1954
+ z-index: 2000;
1955
+ }
1956
+
1957
+ #auth-screen.hidden {
1958
+ display: none;
1959
+ }
1960
+
1961
+ .spinner {
1962
+ width: 40px;
1963
+ height: 40px;
1964
+ border: 3px solid var(--border);
1965
+ border-top-color: var(--accent);
1966
+ border-radius: 50%;
1967
+ animation: spin 1s linear infinite;
1968
+ }
1969
+
1970
+ @keyframes spin {
1971
+ to { transform: rotate(360deg); }
1972
+ }
1973
+
1974
+ #auth-screen p {
1975
+ color: var(--fg-muted);
1976
+ font-size: 14px;
1977
+ }
1978
+
1979
+ #auth-screen .error {
1980
+ color: var(--error);
1981
+ }
1982
+
1983
+ /* Safe area padding for notched devices */
1984
+ @supports (padding: env(safe-area-inset-bottom)) {
1985
+ #input-container {
1986
+ padding-bottom: calc(12px + env(safe-area-inset-bottom));
1987
+ }
1988
+ }
1989
+
1990
+ /* Landscape adjustments */
1991
+ @media (max-height: 500px) {
1992
+ #quickkeys {
1993
+ grid-template-columns: repeat(12, 1fr);
1994
+ padding: 6px 8px;
1995
+ }
1996
+ .qkey {
1997
+ padding: 8px 2px;
1998
+ font-size: 10px;
1999
+ }
2000
+ #terminal {
2001
+ font-size: 12px;
2002
+ }
1763
2003
  }
1764
2004
  </style>
1765
2005
  </head>
1766
2006
  <body>
1767
- <div id="auth-overlay">
1768
- <div class="auth-title">\u{1F4F1} NikCLI Remote</div>
1769
- <div class="auth-subtitle">Full terminal emulation</div>
1770
- <div id="auth-msg" class="auth-msg">
1771
- <div id="auth-text">Connecting...</div>
1772
- </div>
1773
- <div class="quick-btns">
1774
- <button class="quick-btn" onclick="send('help')">/help</button>
1775
- <button class="quick-btn" onclick="send('ls -la')">ls -la</button>
1776
- <button class="quick-btn" onclick="send('pwd')">pwd</button>
1777
- <button class="quick-btn" onclick="send('whoami')">whoami</button>
1778
- <button class="quick-btn" onclick="send('clear')">clear</button>
2007
+ <div id="app">
2008
+ <div id="auth-screen">
2009
+ <div class="spinner"></div>
2010
+ <p id="auth-status">Connecting to NikCLI...</p>
1779
2011
  </div>
1780
- <div class="hint">Mobile keyboard to type commands</div>
1781
- </div>
1782
2012
 
1783
- <div id="status-bar">
1784
- <div class="status-row">
1785
- <span class="status-dot" id="status-dot"></span>
1786
- <span id="status-text">Disconnected</span>
2013
+ <header id="header">
2014
+ <h1>NikCLI Remote</h1>
2015
+ <div id="status">
2016
+ <span id="status-dot" class="connecting"></span>
2017
+ <span id="status-text">Connecting</span>
2018
+ </div>
2019
+ </header>
2020
+
2021
+ <div id="terminal-container">
2022
+ <div id="terminal"></div>
1787
2023
  </div>
1788
- <span id="session-id" style="color: #8b949e;"></span>
1789
- </div>
1790
2024
 
1791
- <div id="terminal"></div>
2025
+ <div id="notifications"></div>
2026
+
2027
+ <div id="quickkeys">
2028
+ <button class="qkey" data-key="\\t">Tab</button>
2029
+ <button class="qkey" data-key="\\x1b[A">\u2191</button>
2030
+ <button class="qkey" data-key="\\x1b[B">\u2193</button>
2031
+ <button class="qkey" data-key="\\x1b[D">\u2190</button>
2032
+ <button class="qkey" data-key="\\x1b[C">\u2192</button>
2033
+ <button class="qkey" data-key="\\x1b">Esc</button>
2034
+ <button class="qkey" data-key="\\x03">^C</button>
2035
+ <button class="qkey" data-key="\\x04">^D</button>
2036
+ <button class="qkey" data-key="\\x1a">^Z</button>
2037
+ <button class="qkey" data-key="\\x0c">^L</button>
2038
+ <button class="qkey wide accent" data-key="\\r">Enter \u23CE</button>
2039
+ </div>
1792
2040
 
1793
- <div id="input-area">
1794
- <form class="input-row" onsubmit="return handleSubmit(event)">
1795
- <span class="prompt">$</span>
1796
- <input type="text" id="cmd-input" placeholder="Type command..." autocomplete="off" enterkeyhint="send" inputmode="text">
1797
- <button type="submit" id="send-btn">Send</button>
1798
- </form>
2041
+ <div id="input-container">
2042
+ <div id="input-row">
2043
+ <input type="text" id="input" placeholder="Type command..." autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
2044
+ <button id="send">Send</button>
2045
+ </div>
2046
+ </div>
1799
2047
  </div>
1800
2048
 
1801
- <script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
1802
- <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
1803
2049
  <script>
1804
- let ws = null, term = null, fitAddon = null, connected = false, reconnectAttempts = 0;
1805
- const token = new URLSearchParams(location.search).get('t') || '';
1806
- const sessionId = new URLSearchParams(location.search).get('s') || '';
1807
-
1808
- const authOverlay = document.getElementById('auth-overlay');
1809
- const authMsg = document.getElementById('auth-msg');
1810
- const authText = document.getElementById('auth-text');
1811
- const statusDot = document.getElementById('status-dot');
1812
- const statusText = document.getElementById('status-text');
1813
- const sessionSpan = document.getElementById('session-id');
1814
- const cmdInput = document.getElementById('cmd-input');
1815
-
1816
- // Initialize xterm.js
1817
- term = new Terminal({
1818
- cursorBlink: true,
1819
- fontSize: 14,
1820
- fontFamily: '"SF Mono", Monaco, Consolas, monospace',
1821
- theme: {
1822
- background: '#0d1117',
1823
- foreground: '#e6edf3',
1824
- cursor: '#3fb950',
1825
- selectionBackground: '#264f78',
1826
- black: '#484f58',
1827
- red: '#f85149',
1828
- green: '#3fb950',
1829
- yellow: '#d29922',
1830
- blue: '#58a6ff',
1831
- magenta: '#a371f7',
1832
- cyan: '#39c5cf',
1833
- white: '#e6edf3',
1834
- brightBlack: '#6e7681',
1835
- brightRed: '#ffa198',
1836
- brightGreen: '#7ee787',
1837
- brightYellow: '#f0883e',
1838
- brightBlue: '#79c0ff',
1839
- brightMagenta: '#d2a8ff',
1840
- brightCyan: '#56d4db',
1841
- brightWhite: '#f0f6fc'
1842
- },
1843
- convertEol: true
1844
- });
2050
+ (function() {
2051
+ 'use strict';
1845
2052
 
1846
- fitAddon = new FitAddon.FitAddon();
1847
- term.loadAddon(fitAddon);
1848
- term.open(document.getElementById('terminal'));
1849
- fitAddon.fit();
1850
- term.writeln('Initializing NikCLI Remote...);
1851
- term.writeln('Type commands below');
1852
-
1853
- // Resize handler
1854
- window.addEventListener('resize', () => {
1855
- clearTimeout(window.resizeTimer);
1856
- window.resizeTimer = setTimeout(() => fitAddon.fit(), 100);
1857
- });
2053
+ // Parse URL params
2054
+ const params = new URLSearchParams(location.search);
2055
+ const token = params.get('t');
2056
+ const sessionId = params.get('s');
1858
2057
 
1859
- // Visual viewport for mobile keyboard
1860
- if (window.visualViewport) {
1861
- window.visualViewport.addEventListener('resize', () => {
1862
- clearTimeout(window.resizeTimer);
1863
- window.resizeTimer = setTimeout(() => fitAddon.fit(), 100);
1864
- });
1865
- }
2058
+ // DOM elements
2059
+ const terminal = document.getElementById('terminal');
2060
+ const input = document.getElementById('input');
2061
+ const sendBtn = document.getElementById('send');
2062
+ const statusDot = document.getElementById('status-dot');
2063
+ const statusText = document.getElementById('status-text');
2064
+ const authScreen = document.getElementById('auth-screen');
2065
+ const authStatus = document.getElementById('auth-status');
2066
+ const notifications = document.getElementById('notifications');
1866
2067
 
1867
- function connect() {
1868
- const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
1869
- ws = new WebSocket(protocol + '//' + location.host + '/ws');
2068
+ // State
2069
+ let ws = null;
2070
+ let reconnectAttempts = 0;
2071
+ const maxReconnectAttempts = 5;
2072
+ let terminalEnabled = true;
1870
2073
 
1871
- ws.onopen = () => {
1872
- setStatus('connecting', 'Authenticating...');
1873
- ws.send(JSON.stringify({ type: 'auth', token }));
1874
- reconnectAttempts = 0;
1875
- };
2074
+ // Connect to WebSocket
2075
+ function connect() {
2076
+ const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
2077
+ ws = new WebSocket(protocol + '//' + location.host);
1876
2078
 
1877
- ws.onmessage = (e) => {
1878
- try { handleMessage(JSON.parse(e.data)); } catch (err) { console.error(err); }
1879
- };
2079
+ ws.onopen = function() {
2080
+ setStatus('connecting', 'Authenticating...');
2081
+ ws.send(JSON.stringify({ type: 'auth', token: token }));
2082
+ };
1880
2083
 
1881
- ws.onclose = () => {
1882
- setStatus('disconnected', 'Disconnected');
1883
- connected = false;
1884
- reconnectAttempts++;
1885
- setTimeout(connect, Math.min(3000, reconnectAttempts * 500));
1886
- };
2084
+ ws.onmessage = function(event) {
2085
+ try {
2086
+ const msg = JSON.parse(event.data);
2087
+ handleMessage(msg);
2088
+ } catch (e) {
2089
+ console.error('Parse error:', e);
2090
+ }
2091
+ };
2092
+
2093
+ ws.onclose = function() {
2094
+ setStatus('disconnected', 'Disconnected');
2095
+ if (reconnectAttempts < maxReconnectAttempts) {
2096
+ reconnectAttempts++;
2097
+ const delay = Math.min(2000 * reconnectAttempts, 10000);
2098
+ setTimeout(connect, delay);
2099
+ } else {
2100
+ authStatus.textContent = 'Connection failed. Refresh to retry.';
2101
+ authStatus.classList.add('error');
2102
+ authScreen.classList.remove('hidden');
2103
+ }
2104
+ };
1887
2105
 
1888
- ws.onerror = () => setStatus('disconnected', 'Connection error');
1889
- }
1890
-
1891
- function handleMessage(msg) {
1892
- switch (msg.type) {
1893
- case 'auth:required':
1894
- ws.send(JSON.stringify({ type: 'auth', token }));
1895
- break;
1896
- case 'auth:success':
1897
- connected = true;
1898
- authOverlay.classList.add('hidden');
1899
- setStatus('connected', 'Connected');
1900
- sessionSpan.textContent = sessionId ? 'Session: ' + sessionId : '';
1901
- term.writeln('\u2713 Connected to NikCLI');
1902
- break;
1903
- case 'auth:failed':
1904
- authMsg.classList.add('error');
1905
- authText.textContent = 'Authentication failed';
1906
- break;
1907
- case 'terminal:output':
1908
- if (msg.payload?.data) term.write(msg.payload.data);
1909
- break;
1910
- case 'terminal:exit':
1911
- term.writeln('Process exited: ' + (msg.payload?.code || 0) + '');
1912
- break;
2106
+ ws.onerror = function() {
2107
+ console.error('WebSocket error');
2108
+ };
1913
2109
  }
1914
- }
1915
2110
 
1916
- function setStatus(state, text) {
1917
- statusDot.className = 'status-dot ' + state;
1918
- statusText.textContent = text;
1919
- }
2111
+ // Handle incoming message
2112
+ function handleMessage(msg) {
2113
+ switch (msg.type) {
2114
+ case 'auth:required':
2115
+ // Already sent auth on open
2116
+ break;
1920
2117
 
1921
- function handleSubmit(e) {
1922
- if (e) e.preventDefault();
1923
- const value = cmdInput.value.trim();
1924
- if (!value || !connected) return;
1925
- cmdInput.value = '';
1926
- term.write('$ ' + value );
1927
- ws.send(JSON.stringify({ type: 'terminal:input', data: value + '\r' }));
1928
- setTimeout(() => cmdInput.focus(), 50);
1929
- return false;
1930
- }
2118
+ case 'auth:success':
2119
+ authScreen.classList.add('hidden');
2120
+ setStatus('connected', 'Connected');
2121
+ reconnectAttempts = 0;
2122
+ terminalEnabled = msg.payload?.terminalEnabled !== false;
2123
+ if (terminalEnabled) {
2124
+ appendOutput('\\x1b[32mConnected to NikCLI\\x1b[0m\\n\\n');
2125
+ }
2126
+ break;
1931
2127
 
1932
- function send(cmd) {
1933
- if (!connected) return;
1934
- term.write('$ ' + cmd + );
1935
- ws.send(JSON.stringify({ type: 'terminal:input', data: cmd + '\r' }));
1936
- }
2128
+ case 'auth:failed':
2129
+ authStatus.textContent = 'Authentication failed';
2130
+ authStatus.classList.add('error');
2131
+ break;
2132
+
2133
+ case 'terminal:output':
2134
+ if (msg.payload?.data) {
2135
+ appendOutput(msg.payload.data);
2136
+ }
2137
+ break;
2138
+
2139
+ case 'terminal:exit':
2140
+ appendOutput('\\n\\x1b[33m[Process exited with code ' + (msg.payload?.code || 0) + ']\\x1b[0m\\n');
2141
+ break;
2142
+
2143
+ case 'notification':
2144
+ showNotification(msg.payload);
2145
+ break;
2146
+
2147
+ case 'session:end':
2148
+ appendOutput('\\n\\x1b[31m[Session ended]\\x1b[0m\\n');
2149
+ setStatus('disconnected', 'Session ended');
2150
+ break;
1937
2151
 
1938
- cmdInput.addEventListener('keydown', (e) => {
1939
- if (e.key === 'Enter' && !e.shiftKey) {
1940
- e.preventDefault();
1941
- handleSubmit();
2152
+ case 'pong':
2153
+ // Heartbeat response
2154
+ break;
2155
+
2156
+ default:
2157
+ console.log('Unknown message:', msg.type);
2158
+ }
1942
2159
  }
1943
- });
1944
2160
 
1945
- document.getElementById('terminal')?.addEventListener('click', () => {
1946
- if (connected) cmdInput.focus();
1947
- });
2161
+ // Append text to terminal with ANSI support
2162
+ function appendOutput(text) {
2163
+ // Simple ANSI to HTML conversion
2164
+ const html = ansiToHtml(text);
2165
+ terminal.innerHTML += html;
2166
+ terminal.scrollTop = terminal.scrollHeight;
2167
+ }
2168
+
2169
+ // Basic ANSI to HTML
2170
+ function ansiToHtml(text) {
2171
+ const ansiColors = {
2172
+ '30': '#6e7681', '31': '#f85149', '32': '#3fb950', '33': '#d29922',
2173
+ '34': '#58a6ff', '35': '#bc8cff', '36': '#76e3ea', '37': '#e6edf3',
2174
+ '90': '#6e7681', '91': '#f85149', '92': '#3fb950', '93': '#d29922',
2175
+ '94': '#58a6ff', '95': '#bc8cff', '96': '#76e3ea', '97': '#ffffff'
2176
+ };
2177
+
2178
+ let result = '';
2179
+ let currentStyle = '';
2180
+
2181
+ const parts = text.split(/\\x1b\\[([0-9;]+)m/);
2182
+ for (let i = 0; i < parts.length; i++) {
2183
+ if (i % 2 === 0) {
2184
+ // Text content
2185
+ result += escapeHtml(parts[i]);
2186
+ } else {
2187
+ // ANSI code
2188
+ const codes = parts[i].split(';');
2189
+ for (const code of codes) {
2190
+ if (code === '0') {
2191
+ if (currentStyle) {
2192
+ result += '</span>';
2193
+ currentStyle = '';
2194
+ }
2195
+ } else if (code === '1') {
2196
+ currentStyle = 'font-weight:bold;';
2197
+ result += '<span style="' + currentStyle + '">';
2198
+ } else if (ansiColors[code]) {
2199
+ if (currentStyle) result += '</span>';
2200
+ currentStyle = 'color:' + ansiColors[code] + ';';
2201
+ result += '<span style="' + currentStyle + '">';
2202
+ }
2203
+ }
2204
+ }
2205
+ }
2206
+
2207
+ if (currentStyle) result += '</span>';
2208
+ return result;
2209
+ }
2210
+
2211
+ // Escape HTML
2212
+ function escapeHtml(text) {
2213
+ return text
2214
+ .replace(/&/g, '&amp;')
2215
+ .replace(/</g, '&lt;')
2216
+ .replace(/>/g, '&gt;')
2217
+ .replace(/"/g, '&quot;')
2218
+ .replace(/\\n/g, '<br>')
2219
+ .replace(/ /g, '&nbsp;');
2220
+ }
2221
+
2222
+ // Set connection status
2223
+ function setStatus(state, text) {
2224
+ statusDot.className = state === 'connected' ? 'connected' :
2225
+ state === 'connecting' ? 'connecting' : '';
2226
+ statusText.textContent = text;
2227
+ }
1948
2228
 
1949
- connect();
2229
+ // Send data to terminal
2230
+ function send(data) {
2231
+ if (ws && ws.readyState === WebSocket.OPEN) {
2232
+ ws.send(JSON.stringify({ type: 'terminal:input', data: data }));
2233
+ }
2234
+ }
2235
+
2236
+ // Show notification
2237
+ function showNotification(n) {
2238
+ if (!n) return;
2239
+ const el = document.createElement('div');
2240
+ el.className = 'notification ' + (n.type || 'info');
2241
+ el.innerHTML = '<h4>' + escapeHtml(n.title || 'Notification') + '</h4>' +
2242
+ '<p>' + escapeHtml(n.body || '') + '</p>';
2243
+ notifications.appendChild(el);
2244
+ setTimeout(function() { el.remove(); }, 5000);
2245
+ }
2246
+
2247
+ // Event: Send button
2248
+ sendBtn.onclick = function() {
2249
+ if (input.value) {
2250
+ send(input.value + '\\r');
2251
+ input.value = '';
2252
+ }
2253
+ input.focus();
2254
+ };
2255
+
2256
+ // Event: Enter key in input
2257
+ input.onkeydown = function(e) {
2258
+ if (e.key === 'Enter') {
2259
+ e.preventDefault();
2260
+ sendBtn.click();
2261
+ }
2262
+ };
2263
+
2264
+ // Event: Quick keys
2265
+ document.querySelectorAll('.qkey').forEach(function(btn) {
2266
+ btn.onclick = function() {
2267
+ const key = btn.dataset.key;
2268
+ const decoded = key
2269
+ .replace(/\\\\x([0-9a-f]{2})/gi, function(_, hex) {
2270
+ return String.fromCharCode(parseInt(hex, 16));
2271
+ })
2272
+ .replace(/\\\\t/g, '\\t')
2273
+ .replace(/\\\\r/g, '\\r')
2274
+ .replace(/\\\\n/g, '\\n');
2275
+ send(decoded);
2276
+ input.focus();
2277
+ };
2278
+ });
2279
+
2280
+ // Heartbeat
2281
+ setInterval(function() {
2282
+ if (ws && ws.readyState === WebSocket.OPEN) {
2283
+ ws.send(JSON.stringify({ type: 'ping' }));
2284
+ }
2285
+ }, 25000);
2286
+
2287
+ // Handle resize
2288
+ function sendResize() {
2289
+ if (ws && ws.readyState === WebSocket.OPEN) {
2290
+ const cols = Math.floor(terminal.clientWidth / 8);
2291
+ const rows = Math.floor(terminal.clientHeight / 18);
2292
+ ws.send(JSON.stringify({ type: 'terminal:resize', cols: cols, rows: rows }));
2293
+ }
2294
+ }
2295
+
2296
+ window.addEventListener('resize', sendResize);
2297
+ setTimeout(sendResize, 1000);
2298
+
2299
+ // Start connection
2300
+ if (token) {
2301
+ connect();
2302
+ } else {
2303
+ authStatus.textContent = 'Invalid session URL';
2304
+ authStatus.classList.add('error');
2305
+ }
2306
+ })();
1950
2307
  </script>
1951
2308
  </body>
1952
2309
  </html>`;