green-screen-react 0.4.0 → 1.0.0

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/dist/index.js CHANGED
@@ -111,15 +111,40 @@ var RestAdapter = class {
111
111
  };
112
112
 
113
113
  // src/adapters/WebSocketAdapter.ts
114
- var WebSocketAdapter = class {
114
+ var import_meta = {};
115
+ var WebSocketAdapter = class _WebSocketAdapter {
115
116
  constructor(options = {}) {
116
117
  this.ws = null;
117
118
  this.screen = null;
118
119
  this.status = { connected: false, status: "disconnected" };
119
- this.pendingResolvers = /* @__PURE__ */ new Map();
120
+ this.pendingScreenResolver = null;
121
+ this.pendingConnectResolver = null;
122
+ this.connectingPromise = null;
120
123
  this.screenListeners = /* @__PURE__ */ new Set();
121
124
  this.statusListeners = /* @__PURE__ */ new Set();
122
- this.workerUrl = (options.workerUrl || "http://localhost:3001").replace(/\/+$/, "");
125
+ this._sessionId = null;
126
+ this.workerUrl = (options.workerUrl || _WebSocketAdapter.detectEnvUrl() || "http://localhost:3001").replace(/\/+$/, "");
127
+ }
128
+ static detectEnvUrl() {
129
+ try {
130
+ if (typeof import_meta !== "undefined" && import_meta.env) {
131
+ const env = import_meta.env;
132
+ return env.VITE_GREEN_SCREEN_URL || env.VITE_WORKER_URL || void 0;
133
+ }
134
+ } catch {
135
+ }
136
+ try {
137
+ const p = typeof globalThis !== "undefined" && globalThis.process;
138
+ if (p && p.env) {
139
+ return p.env.NEXT_PUBLIC_GREEN_SCREEN_URL || p.env.REACT_APP_GREEN_SCREEN_URL || void 0;
140
+ }
141
+ } catch {
142
+ }
143
+ return void 0;
144
+ }
145
+ /** The proxy-side session ID (available after connect or reattach) */
146
+ get sessionId() {
147
+ return this._sessionId;
123
148
  }
124
149
  /** Subscribe to real-time screen updates */
125
150
  onScreen(listener) {
@@ -150,24 +175,13 @@ var WebSocketAdapter = class {
150
175
  }
151
176
  return new Promise((resolve) => {
152
177
  const timeout = setTimeout(() => {
178
+ this.pendingConnectResolver = null;
153
179
  resolve({ success: false, error: "Connection timeout" });
154
180
  }, 3e4);
155
- const onMessage = (event) => {
156
- try {
157
- const msg = JSON.parse(event.data);
158
- if (msg.type === "connected") {
159
- clearTimeout(timeout);
160
- this.ws?.removeEventListener("message", onMessage);
161
- resolve({ success: true });
162
- } else if (msg.type === "error") {
163
- clearTimeout(timeout);
164
- this.ws?.removeEventListener("message", onMessage);
165
- resolve({ success: false, error: msg.message });
166
- }
167
- } catch {
168
- }
181
+ this.pendingConnectResolver = (result) => {
182
+ clearTimeout(timeout);
183
+ resolve(result);
169
184
  };
170
- this.ws?.addEventListener("message", onMessage);
171
185
  this.wsSend({
172
186
  type: "connect",
173
187
  host: config.host,
@@ -178,9 +192,29 @@ var WebSocketAdapter = class {
178
192
  });
179
193
  });
180
194
  }
195
+ /**
196
+ * Reattach to an existing proxy session (e.g. after page reload).
197
+ * The proxy keeps the TCP connection alive; this just reconnects the
198
+ * WebSocket and receives the current screen.
199
+ */
200
+ async reattach(sessionId) {
201
+ await this.ensureWebSocket();
202
+ return new Promise((resolve) => {
203
+ const timeout = setTimeout(() => {
204
+ this.pendingConnectResolver = null;
205
+ resolve({ success: false, error: "Reattach timeout" });
206
+ }, 1e4);
207
+ this.pendingConnectResolver = (result) => {
208
+ clearTimeout(timeout);
209
+ resolve(result);
210
+ };
211
+ this.wsSend({ type: "reattach", sessionId });
212
+ });
213
+ }
181
214
  async disconnect() {
182
215
  this.wsSend({ type: "disconnect" });
183
216
  this.status = { connected: false, status: "disconnected" };
217
+ this._sessionId = null;
184
218
  if (this.ws) {
185
219
  this.ws.close();
186
220
  this.ws = null;
@@ -190,7 +224,7 @@ var WebSocketAdapter = class {
190
224
  async reconnect() {
191
225
  return { success: false, error: "Use disconnect() then connect() instead" };
192
226
  }
193
- /** Close the WebSocket without sending disconnect */
227
+ /** Close the WebSocket without sending disconnect (session stays alive on proxy) */
194
228
  dispose() {
195
229
  if (this.ws) {
196
230
  this.ws.close();
@@ -199,7 +233,8 @@ var WebSocketAdapter = class {
199
233
  }
200
234
  async ensureWebSocket() {
201
235
  if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
202
- return new Promise((resolve, reject) => {
236
+ if (this.connectingPromise) return this.connectingPromise;
237
+ this.connectingPromise = new Promise((resolve, reject) => {
203
238
  const wsUrl = this.workerUrl.replace(/^http/, "ws") + "/ws";
204
239
  this.ws = new WebSocket(wsUrl);
205
240
  this.ws.onopen = () => resolve();
@@ -215,39 +250,61 @@ var WebSocketAdapter = class {
215
250
  this.status = { connected: false, status: "disconnected" };
216
251
  for (const listener of this.statusListeners) listener(this.status);
217
252
  };
253
+ }).finally(() => {
254
+ this.connectingPromise = null;
218
255
  });
256
+ return this.connectingPromise;
219
257
  }
220
258
  handleMessage(msg) {
221
259
  switch (msg.type) {
222
- case "screen":
260
+ case "screen": {
223
261
  this.screen = msg.data;
224
262
  for (const listener of this.screenListeners) listener(msg.data);
225
- const screenResolver = this.pendingResolvers.get("screen");
226
- if (screenResolver) {
227
- this.pendingResolvers.delete("screen");
228
- screenResolver(msg.data);
263
+ if (this.pendingScreenResolver) {
264
+ const resolver = this.pendingScreenResolver;
265
+ this.pendingScreenResolver = null;
266
+ resolver(msg.data);
229
267
  }
230
268
  break;
269
+ }
231
270
  case "status":
232
271
  this.status = msg.data;
233
272
  for (const listener of this.statusListeners) listener(msg.data);
234
273
  break;
235
- case "error":
236
- const errorResolver = this.pendingResolvers.get("screen");
237
- if (errorResolver) {
238
- this.pendingResolvers.delete("screen");
239
- errorResolver(null);
274
+ case "connected":
275
+ this._sessionId = msg.sessionId ?? null;
276
+ if (this.pendingConnectResolver) {
277
+ const resolver = this.pendingConnectResolver;
278
+ this.pendingConnectResolver = null;
279
+ resolver({ success: true });
280
+ }
281
+ break;
282
+ case "error": {
283
+ if (this.pendingConnectResolver) {
284
+ const resolver = this.pendingConnectResolver;
285
+ this.pendingConnectResolver = null;
286
+ resolver({ success: false, error: msg.message });
287
+ } else if (this.pendingScreenResolver) {
288
+ const resolver = this.pendingScreenResolver;
289
+ this.pendingScreenResolver = null;
290
+ resolver(null);
240
291
  }
241
292
  break;
293
+ }
242
294
  }
243
295
  }
244
296
  sendAndWaitForScreen(msg) {
245
297
  return new Promise((resolve) => {
298
+ if (this.pendingScreenResolver) {
299
+ const old = this.pendingScreenResolver;
300
+ this.pendingScreenResolver = null;
301
+ old(this.screen);
302
+ }
246
303
  const timeout = setTimeout(() => {
247
- this.pendingResolvers.delete("screen");
304
+ this.pendingScreenResolver = null;
248
305
  resolve({ success: true, ...this.screenToResult() });
249
306
  }, 5e3);
250
- this.pendingResolvers.set("screen", (screen) => {
307
+ this.pendingScreenResolver = (screen) => {
251
308
  clearTimeout(timeout);
252
309
  if (screen) {
253
310
  resolve({
@@ -260,7 +317,7 @@ var WebSocketAdapter = class {
260
317
  } else {
261
318
  resolve({ success: false, error: "No screen data received" });
262
319
  }
263
- });
320
+ };
264
321
  this.wsSend(msg);
265
322
  });
266
323
  }
@@ -891,20 +948,23 @@ var PROTOCOL_OPTIONS = [
891
948
  { value: "vt", label: "VT220" },
892
949
  { value: "hp6530", label: "HP 6530 (NonStop)" }
893
950
  ];
894
- function InlineSignIn({ defaultProtocol, loading, error, onConnect }) {
951
+ function InlineSignIn({ defaultProtocol, loading: externalLoading, error, onConnect }) {
895
952
  const [host, setHost] = (0, import_react4.useState)("");
896
953
  const [port, setPort] = (0, import_react4.useState)("");
897
954
  const [selectedProtocol, setSelectedProtocol] = (0, import_react4.useState)(defaultProtocol);
898
955
  const [username, setUsername] = (0, import_react4.useState)("");
899
956
  const [password, setPassword] = (0, import_react4.useState)("");
957
+ const [submitted, setSubmitted] = (0, import_react4.useState)(false);
958
+ const loading = externalLoading || submitted;
900
959
  const handleSubmit = (e) => {
901
960
  e.preventDefault();
961
+ setSubmitted(true);
902
962
  onConnect({
903
963
  host,
904
- port: port ? parseInt(port, 10) : void 0,
964
+ port: port ? parseInt(port, 10) : 23,
905
965
  protocol: selectedProtocol,
906
- username,
907
- password
966
+ ...username.trim() ? { username: username.trim() } : {},
967
+ ...password ? { password } : {}
908
968
  });
909
969
  };
910
970
  const inputStyle = {
@@ -927,6 +987,16 @@ function InlineSignIn({ defaultProtocol, loading, error, onConnect }) {
927
987
  color: "var(--gs-muted, #94a3b8)",
928
988
  fontFamily: "var(--gs-font)"
929
989
  };
990
+ if (loading) {
991
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "gs-signin", style: { display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: "12px" }, children: [
992
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(RefreshIcon, { size: 28, className: "gs-spin" }),
993
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { fontSize: "11px", letterSpacing: "0.15em", textTransform: "uppercase", color: "var(--gs-muted)", fontFamily: "var(--gs-font)" }, children: [
994
+ "Connecting to ",
995
+ host || "host",
996
+ "..."
997
+ ] })
998
+ ] });
999
+ }
930
1000
  return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("form", { onSubmit: handleSubmit, className: "gs-signin", children: [
931
1001
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { textAlign: "center", marginBottom: "16px" }, children: [
932
1002
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(TerminalIcon, { size: 28 }),
@@ -934,7 +1004,10 @@ function InlineSignIn({ defaultProtocol, loading, error, onConnect }) {
934
1004
  ] }),
935
1005
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "gs-signin-row", children: [
936
1006
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { flex: 1 }, children: [
937
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: labelStyle, children: "Host" }),
1007
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { style: labelStyle, children: [
1008
+ "Host ",
1009
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: "#ef4444" }, children: "*" })
1010
+ ] }),
938
1011
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { style: inputStyle, value: host, onChange: (e) => setHost(e.target.value), placeholder: "192.168.1.100", required: true, autoFocus: true })
939
1012
  ] }),
940
1013
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { width: "72px" }, children: [
@@ -943,22 +1016,25 @@ function InlineSignIn({ defaultProtocol, loading, error, onConnect }) {
943
1016
  ] })
944
1017
  ] }),
945
1018
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
946
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: labelStyle, children: "Protocol" }),
1019
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("label", { style: labelStyle, children: [
1020
+ "Protocol ",
1021
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { style: { color: "#ef4444" }, children: "*" })
1022
+ ] }),
947
1023
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("select", { style: { ...inputStyle, appearance: "none" }, value: selectedProtocol, onChange: (e) => setSelectedProtocol(e.target.value), children: PROTOCOL_OPTIONS.map((o) => /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("option", { value: o.value, children: o.label }, o.value)) })
948
1024
  ] }),
949
1025
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
950
1026
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: labelStyle, children: "Username" }),
951
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { style: inputStyle, value: username, onChange: (e) => setUsername(e.target.value), required: true, autoComplete: "username" })
1027
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { style: inputStyle, value: username, onChange: (e) => setUsername(e.target.value), autoComplete: "username" })
952
1028
  ] }),
953
1029
  /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { children: [
954
1030
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("label", { style: labelStyle, children: "Password" }),
955
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { style: inputStyle, type: "password", value: password, onChange: (e) => setPassword(e.target.value), required: true, autoComplete: "current-password" })
1031
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("input", { style: inputStyle, type: "password", value: password, onChange: (e) => setPassword(e.target.value), autoComplete: "current-password" })
956
1032
  ] }),
957
1033
  error && /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { color: "#FF6B00", fontSize: "11px", fontFamily: "var(--gs-font)", display: "flex", alignItems: "center", gap: "6px" }, children: [
958
1034
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(AlertTriangleIcon, { size: 12 }),
959
1035
  /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { children: error })
960
1036
  ] }),
961
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", disabled: loading || !host || !username || !password, className: "gs-signin-btn", children: loading ? "Connecting..." : "Connect" })
1037
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("button", { type: "submit", disabled: !host, className: "gs-signin-btn", children: "Connect" })
962
1038
  ] });
963
1039
  }
964
1040
 
@@ -977,6 +1053,7 @@ var noopAdapter = {
977
1053
  function GreenScreenTerminal({
978
1054
  adapter: externalAdapter,
979
1055
  baseUrl,
1056
+ workerUrl,
980
1057
  protocol,
981
1058
  protocolProfile: customProfile,
982
1059
  screenData: externalScreenData,
@@ -992,6 +1069,8 @@ function GreenScreenTerminal({
992
1069
  inlineSignIn = true,
993
1070
  defaultProtocol: signInDefaultProtocol,
994
1071
  onSignIn,
1072
+ autoSignedIn,
1073
+ autoFocusDisabled = false,
995
1074
  bootLoader,
996
1075
  headerRight,
997
1076
  overlay,
@@ -1007,11 +1086,16 @@ function GreenScreenTerminal({
1007
1086
  () => baseUrl ? new RestAdapter({ baseUrl }) : null,
1008
1087
  [baseUrl]
1009
1088
  );
1089
+ const workerUrlAdapter = (0, import_react5.useMemo)(
1090
+ () => workerUrl ? new WebSocketAdapter({ workerUrl }) : null,
1091
+ [workerUrl]
1092
+ );
1010
1093
  const defaultWsAdapter = (0, import_react5.useMemo)(
1011
- () => !externalAdapter && !baseUrl ? new WebSocketAdapter() : null,
1012
- [externalAdapter, baseUrl]
1094
+ () => !externalAdapter && !baseUrl && !workerUrl ? new WebSocketAdapter() : null,
1095
+ [externalAdapter, baseUrl, workerUrl]
1013
1096
  );
1014
- const adapter = externalAdapter ?? baseUrlAdapter ?? internalAdapter ?? defaultWsAdapter ?? noopAdapter;
1097
+ const adapter = externalAdapter ?? baseUrlAdapter ?? workerUrlAdapter ?? internalAdapter ?? defaultWsAdapter ?? noopAdapter;
1098
+ const isUsingDefaultAdapter = adapter === defaultWsAdapter;
1015
1099
  const shouldPoll = pollInterval > 0 && !externalScreenData;
1016
1100
  const { data: polledScreenData, error: screenError } = useTerminalScreen(adapter, pollInterval, shouldPoll);
1017
1101
  const { sendText: _sendText, sendKey: _sendKey } = useTerminalInput(adapter);
@@ -1038,6 +1122,12 @@ function GreenScreenTerminal({
1038
1122
  const sendKey = (0, import_react5.useCallback)(async (key) => _sendKey(key), [_sendKey]);
1039
1123
  const [inputText, setInputText] = (0, import_react5.useState)("");
1040
1124
  const [isFocused, setIsFocused] = (0, import_react5.useState)(false);
1125
+ const [showSignInHint, setShowSignInHint] = (0, import_react5.useState)(false);
1126
+ const prevAutoSignedIn = (0, import_react5.useRef)(false);
1127
+ (0, import_react5.useEffect)(() => {
1128
+ if (autoSignedIn && !prevAutoSignedIn.current) setShowSignInHint(true);
1129
+ prevAutoSignedIn.current = !!autoSignedIn;
1130
+ }, [autoSignedIn]);
1041
1131
  const terminalRef = (0, import_react5.useRef)(null);
1042
1132
  const inputRef = (0, import_react5.useRef)(null);
1043
1133
  const [syncedCursor, setSyncedCursor] = (0, import_react5.useState)(null);
@@ -1047,6 +1137,7 @@ function GreenScreenTerminal({
1047
1137
  if (prevRawContentRef.current && newContent && newContent !== prevRawContentRef.current) {
1048
1138
  setSyncedCursor(null);
1049
1139
  setInputText("");
1140
+ if (showSignInHint) setShowSignInHint(false);
1050
1141
  }
1051
1142
  prevRawContentRef.current = newContent;
1052
1143
  }, [rawScreenData?.content]);
@@ -1094,17 +1185,34 @@ function GreenScreenTerminal({
1094
1185
  if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
1095
1186
  };
1096
1187
  }, [connStatus?.connected, autoReconnectAttempt, isAutoReconnecting, reconnecting, reconnect, autoReconnectEnabled, maxAttempts, onNotification]);
1188
+ const [connecting, setConnecting] = (0, import_react5.useState)(false);
1189
+ const [signInError, setSignInError] = (0, import_react5.useState)(null);
1097
1190
  const handleSignIn = (0, import_react5.useCallback)(async (config) => {
1098
- onSignIn?.(config);
1099
- if (!externalAdapter && !baseUrlAdapter) {
1100
- const port = config.port ? `:${config.port}` : "";
1101
- const newAdapter = new RestAdapter({ baseUrl: `http://${config.host}${port}` });
1102
- setInternalAdapter(newAdapter);
1103
- await newAdapter.connect(config);
1191
+ if (onSignIn) {
1192
+ onSignIn(config);
1104
1193
  return;
1105
1194
  }
1106
- await connect(config);
1195
+ setConnecting(true);
1196
+ setSignInError(null);
1197
+ try {
1198
+ if (!externalAdapter && !baseUrlAdapter) {
1199
+ const port = config.port ? `:${config.port}` : "";
1200
+ const newAdapter = new RestAdapter({ baseUrl: `http://${config.host}${port}` });
1201
+ setInternalAdapter(newAdapter);
1202
+ await newAdapter.connect(config);
1203
+ if (config.username && config.password) setShowSignInHint(true);
1204
+ return;
1205
+ }
1206
+ await connect(config);
1207
+ if (config.username && config.password) setShowSignInHint(true);
1208
+ } catch (err) {
1209
+ setSignInError(err instanceof Error ? err.message : String(err));
1210
+ setConnecting(false);
1211
+ }
1107
1212
  }, [connect, onSignIn, externalAdapter, baseUrlAdapter]);
1213
+ (0, import_react5.useEffect)(() => {
1214
+ if (connecting && screenData?.content) setConnecting(false);
1215
+ }, [connecting, screenData?.content]);
1108
1216
  const [showBootLoader, setShowBootLoader] = (0, import_react5.useState)(bootLoader !== false);
1109
1217
  const [bootFadingOut, setBootFadingOut] = (0, import_react5.useState)(false);
1110
1218
  (0, import_react5.useEffect)(() => {
@@ -1115,6 +1223,35 @@ function GreenScreenTerminal({
1115
1223
  return () => clearTimeout(timer);
1116
1224
  }
1117
1225
  }, [screenData?.content, showBootLoader]);
1226
+ const FOCUS_STORAGE_KEY = "gs-terminal-focused";
1227
+ (0, import_react5.useEffect)(() => {
1228
+ if (!autoFocusDisabled && !readOnly) {
1229
+ try {
1230
+ if (localStorage.getItem(FOCUS_STORAGE_KEY) === "true") {
1231
+ setIsFocused(true);
1232
+ }
1233
+ } catch {
1234
+ }
1235
+ }
1236
+ }, []);
1237
+ (0, import_react5.useEffect)(() => {
1238
+ if (autoFocusDisabled) return;
1239
+ try {
1240
+ localStorage.setItem(FOCUS_STORAGE_KEY, String(isFocused));
1241
+ } catch {
1242
+ }
1243
+ }, [isFocused, autoFocusDisabled]);
1244
+ (0, import_react5.useEffect)(() => {
1245
+ if (isFocused) inputRef.current?.focus();
1246
+ }, [isFocused]);
1247
+ const hadScreenData = (0, import_react5.useRef)(false);
1248
+ (0, import_react5.useEffect)(() => {
1249
+ if (screenData?.content && !hadScreenData.current && !autoFocusDisabled && !readOnly) {
1250
+ hadScreenData.current = true;
1251
+ setIsFocused(true);
1252
+ }
1253
+ if (!screenData?.content) hadScreenData.current = false;
1254
+ }, [screenData?.content, autoFocusDisabled, readOnly]);
1118
1255
  (0, import_react5.useEffect)(() => {
1119
1256
  const handleClickOutside = (event) => {
1120
1257
  if (terminalRef.current && !terminalRef.current.contains(event.target)) setIsFocused(false);
@@ -1265,7 +1402,7 @@ function GreenScreenTerminal({
1265
1402
  const highlightedFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.is_highlighted);
1266
1403
  const reverseFields = fields.filter((f) => f.row === rowIndex && f.is_protected && f.is_reverse);
1267
1404
  const allRowFields = [...inputFields, ...highlightedFields, ...reverseFields];
1268
- if (allRowFields.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: renderTextWithUnderlines(line, `r${rowIndex}`) });
1405
+ if (allRowFields.length === 0) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: line });
1269
1406
  const sorted = [...allRowFields].sort((a, b) => a.col - b.col);
1270
1407
  const segs = [];
1271
1408
  let lastEnd = 0;
@@ -1273,21 +1410,24 @@ function GreenScreenTerminal({
1273
1410
  sorted.forEach((field, idx) => {
1274
1411
  const fs = field.col;
1275
1412
  const fe = Math.min(field.col + field.length, cols);
1276
- if (fs > lastEnd) segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: renderTextWithUnderlines(line.substring(lastEnd, fs), `r${rowIndex}p${idx}`) }, `t${idx}`));
1413
+ if (fs > lastEnd) segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: line.substring(lastEnd, fs) }, `t${idx}`));
1277
1414
  const fc = line.substring(fs, fe);
1278
1415
  if (field.is_input) {
1279
- const w = field.length >= 30 ? Math.max(field.length, 40) : field.length;
1280
- segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-input-field", style: { borderBottom: "2px solid var(--gs-green, #10b981)", display: "inline-block", minWidth: `${w}ch` }, children: fc }, `f${idx}`));
1416
+ const fieldWidth = Math.min(field.length, cols - fs);
1417
+ const fieldClass = showSignInHint ? "gs-confirmed-field" : field.is_underscored ? "gs-input-field" : void 0;
1418
+ segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: fieldClass || void 0, style: { display: "inline-block", width: `${fieldWidth}ch`, overflow: "hidden" }, children: fc }, `f${idx}`));
1281
1419
  } else if (field.is_reverse) {
1282
1420
  segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: "#ef4444", fontWeight: "bold" }, children: fc }, `v${idx}`));
1283
- } else {
1421
+ } else if (field.is_highlighted) {
1284
1422
  segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: { color: "var(--gs-white, #FFFFFF)" }, children: fc }, `h${idx}`));
1423
+ } else {
1424
+ segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: fc }, `h${idx}`));
1285
1425
  }
1286
1426
  lastEnd = fe;
1287
1427
  });
1288
- if (lastEnd < line.length) segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: renderTextWithUnderlines(line.substring(lastEnd), `r${rowIndex}e`) }, "te"));
1428
+ if (lastEnd < line.length) segs.push(/* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { children: line.substring(lastEnd) }, "te"));
1289
1429
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: segs });
1290
- }, [renderTextWithUnderlines, screenData?.cols, profile.defaultCols]);
1430
+ }, [renderTextWithUnderlines, screenData?.cols, profile.defaultCols, showSignInHint]);
1291
1431
  const renderScreen = () => {
1292
1432
  if (showBootLoader && !screenData?.content) {
1293
1433
  if (bootLoader === false) return null;
@@ -1297,11 +1437,15 @@ function GreenScreenTerminal({
1297
1437
  if (bootFadingOut && screenData?.content) return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "gs-fade-in", children: renderScreenContent() });
1298
1438
  if (!screenData?.content) {
1299
1439
  if (inlineSignIn && !connStatus?.connected) {
1300
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { width: `${screenData?.cols || profile.defaultCols}ch`, height: `${(screenData?.rows || profile.defaultRows) * 21}px`, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(InlineSignIn, { defaultProtocol: signInDefaultProtocol || protocol || "tn5250", loading: reconnecting, error: connectError, onConnect: handleSignIn }) });
1440
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { width: "100%", height: `${(screenData?.rows || profile.defaultRows) * 21}px`, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(InlineSignIn, { defaultProtocol: signInDefaultProtocol || protocol || "tn5250", loading: connecting || reconnecting, error: signInError || connectError, onConnect: handleSignIn }) });
1301
1441
  }
1302
1442
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { width: `${screenData?.cols || profile.defaultCols}ch`, height: `${(screenData?.rows || profile.defaultRows) * 21}px`, display: "flex", alignItems: "center", justifyContent: "center" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { textAlign: "center" }, children: [
1303
1443
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { color: "#808080", marginBottom: "12px" }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(TerminalIcon, { size: 40 }) }),
1304
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: { fontFamily: "var(--gs-font)", fontSize: "12px", color: "#808080" }, children: connStatus?.connected ? "Waiting for screen data..." : "Not connected" })
1444
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { style: { fontFamily: "var(--gs-font)", fontSize: "12px", color: "#808080" }, children: connStatus?.connected ? "Waiting for screen data..." : "Not connected" }),
1445
+ !connStatus?.connected && isUsingDefaultAdapter && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("p", { style: { fontFamily: "var(--gs-font)", fontSize: "11px", color: "#606060", marginTop: "8px" }, children: [
1446
+ "Start the proxy: ",
1447
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("code", { style: { color: "#10b981" }, children: "npx green-screen-proxy --mock" })
1448
+ ] })
1305
1449
  ] }) });
1306
1450
  }
1307
1451
  return renderScreenContent();
@@ -1316,8 +1460,7 @@ function GreenScreenTerminal({
1316
1460
  const fields = screenData.fields || [];
1317
1461
  const ROW_HEIGHT = 21;
1318
1462
  const cursor = getCursorPos();
1319
- const hasInputFields = fields.some((f) => f.is_input);
1320
- const hasCursor = screenData.cursor_row !== void 0 && screenData.cursor_col !== void 0 && (hasInputFields || screenData.cursor_row !== 0 || screenData.cursor_col !== 0);
1463
+ const hasCursor = screenData.cursor_row !== void 0 && screenData.cursor_col !== void 0;
1321
1464
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { style: { fontFamily: "var(--gs-font)", fontSize: "13px", position: "relative", width: `${cols}ch` }, children: [
1322
1465
  rows.map((line, index) => {
1323
1466
  let displayLine = line;
@@ -1328,9 +1471,10 @@ function GreenScreenTerminal({
1328
1471
  const headerSegments = index === 0 ? profile.colors.parseHeaderRow(displayLine) : null;
1329
1472
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: headerSegments ? "" : profile.colors.getRowColorClass(index, displayLine, termRows), style: { height: `${ROW_HEIGHT}px`, lineHeight: `${ROW_HEIGHT}px`, whiteSpace: "pre", position: "relative" }, children: [
1330
1473
  headerSegments ? headerSegments.map((seg, i) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: seg.colorClass, children: seg.text }, i)) : renderRowWithFields(displayLine, index, fields),
1331
- hasCursor && index === cursor.row && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-cursor", style: { position: "absolute", left: `${cursor.col}ch`, width: "1ch", height: `${ROW_HEIGHT}px`, top: 0, pointerEvents: "none" } })
1474
+ hasCursor && !showSignInHint && index === cursor.row && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "gs-cursor", style: { position: "absolute", left: `${cursor.col}ch`, width: "1ch", height: `${ROW_HEIGHT}px`, top: 0, pointerEvents: "none" } })
1332
1475
  ] }, index);
1333
1476
  }),
1477
+ showSignInHint && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "gs-signin-hint", children: "Signed in \u2014 press Enter to continue" }),
1334
1478
  screenData.cursor_row !== void 0 && screenData.cursor_col !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: { position: "absolute", bottom: 0, right: 0, fontFamily: "var(--gs-font)", fontSize: "10px", color: "var(--gs-green, #10b981)", pointerEvents: "none", opacity: 0.6 }, children: [
1335
1479
  String(screenData.cursor_row + 1).padStart(2, "0"),
1336
1480
  "/",