jh-web-gateway 2.2.0 → 2.3.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.
@@ -12,14 +12,14 @@ import {
12
12
  startServer,
13
13
  updateConfig,
14
14
  validateConfig
15
- } from "./chunk-Y2NMKJOG.js";
15
+ } from "./chunk-E6JMUHPA.js";
16
16
 
17
17
  // src/tui/index.ts
18
18
  import React10 from "react";
19
19
  import { render } from "ink";
20
20
 
21
21
  // src/tui/App.tsx
22
- import { useState as useState9, useRef as useRef2, useEffect as useEffect5 } from "react";
22
+ import { useState as useState9, useRef as useRef2, useEffect as useEffect6 } from "react";
23
23
  import { Box as Box11, useInput as useInput9 } from "ink";
24
24
 
25
25
  // src/tui/AppContext.tsx
@@ -42,7 +42,9 @@ var initialState = {
42
42
  config: defaultConfig,
43
43
  serverHandle: null,
44
44
  chromeState: null,
45
- tokenRefresher: null
45
+ tokenRefresher: null,
46
+ requestTracker: null,
47
+ requestQueue: null
46
48
  };
47
49
  var AppContext = createContext(null);
48
50
  function AppProvider({ children }) {
@@ -81,6 +83,12 @@ function AppProvider({ children }) {
81
83
  const setConfig = (config) => {
82
84
  setState((prev) => ({ ...prev, config }));
83
85
  };
86
+ const setRequestTracker = (requestTracker) => {
87
+ setState((prev) => ({ ...prev, requestTracker }));
88
+ };
89
+ const setRequestQueue = (requestQueue) => {
90
+ setState((prev) => ({ ...prev, requestQueue }));
91
+ };
84
92
  const value = {
85
93
  state,
86
94
  navigate,
@@ -90,7 +98,9 @@ function AppProvider({ children }) {
90
98
  setServerHandle,
91
99
  setChromeState,
92
100
  setTokenRefresher,
93
- setConfig
101
+ setConfig,
102
+ setRequestTracker,
103
+ setRequestQueue
94
104
  };
95
105
  return /* @__PURE__ */ jsx(AppContext.Provider, { value, children });
96
106
  }
@@ -137,6 +147,100 @@ function FooterBar({ shortcuts }) {
137
147
  // src/tui/components/QuitDialog.tsx
138
148
  import { Box as Box3, Text as Text3, useInput } from "ink";
139
149
 
150
+ // src/core/request-activity-tracker.ts
151
+ var RequestActivityTracker = class {
152
+ entries = /* @__PURE__ */ new Map();
153
+ orderedIds = [];
154
+ listeners = /* @__PURE__ */ new Set();
155
+ maxCompleted;
156
+ constructor(maxCompleted = 50) {
157
+ this.maxCompleted = maxCompleted;
158
+ }
159
+ /** Called by middleware when a request arrives. */
160
+ start(id, method, path) {
161
+ if (this.entries.has(id)) return;
162
+ const entry = {
163
+ id,
164
+ method,
165
+ path,
166
+ status: "active",
167
+ statusCode: null,
168
+ startTime: Date.now(),
169
+ endTime: null,
170
+ elapsedMs: null
171
+ };
172
+ this.entries.set(id, entry);
173
+ this.orderedIds.unshift(id);
174
+ this.notify();
175
+ }
176
+ /** Called by middleware when a response is sent. */
177
+ end(id, statusCode) {
178
+ const entry = this.entries.get(id);
179
+ if (!entry) return;
180
+ const endTime = Date.now();
181
+ entry.status = statusCode >= 200 && statusCode < 300 ? "completed" : "error";
182
+ entry.statusCode = statusCode;
183
+ entry.endTime = endTime;
184
+ entry.elapsedMs = endTime - entry.startTime;
185
+ this.prune();
186
+ this.notify();
187
+ }
188
+ /** Subscribe to state changes. Returns unsubscribe function. */
189
+ subscribe(listener) {
190
+ this.listeners.add(listener);
191
+ return () => {
192
+ this.listeners.delete(listener);
193
+ };
194
+ }
195
+ /** Get current snapshot of entries (most-recent-first). */
196
+ getEntries() {
197
+ return this.orderedIds.map((id) => this.entries.get(id)).filter(Boolean);
198
+ }
199
+ /** Get a single entry by ID. */
200
+ getEntry(id) {
201
+ return this.entries.get(id);
202
+ }
203
+ /** Clear all entries. */
204
+ clear() {
205
+ this.entries.clear();
206
+ this.orderedIds = [];
207
+ this.notify();
208
+ }
209
+ /** Notify all listeners with current entries snapshot. */
210
+ notify() {
211
+ const snapshot = this.getEntries();
212
+ for (const listener of this.listeners) {
213
+ try {
214
+ listener(snapshot);
215
+ } catch (err) {
216
+ console.error("[RequestActivityTracker] subscriber error:", err);
217
+ }
218
+ }
219
+ }
220
+ /** Remove oldest completed entries beyond maxCompleted. */
221
+ prune() {
222
+ const completed = this.orderedIds.filter((id) => {
223
+ const e = this.entries.get(id);
224
+ return e && e.status !== "active";
225
+ });
226
+ while (completed.length > this.maxCompleted) {
227
+ const oldestId = completed.pop();
228
+ this.entries.delete(oldestId);
229
+ const idx = this.orderedIds.indexOf(oldestId);
230
+ if (idx !== -1) this.orderedIds.splice(idx, 1);
231
+ }
232
+ }
233
+ };
234
+ function formatElapsed(ms) {
235
+ const seconds = Math.round(ms / 100) / 10;
236
+ return `${seconds.toFixed(1)}s`;
237
+ }
238
+ function formatQueueDepth(n, online) {
239
+ if (!online) return "Queue: offline";
240
+ if (n === 0) return "Queue: idle";
241
+ return `Queue: ${n} pending`;
242
+ }
243
+
140
244
  // src/tui/services/gateway-lifecycle.ts
141
245
  async function startGatewayForTui(config, options, callbacks) {
142
246
  const maxPages = options.pages ?? 1;
@@ -201,20 +305,24 @@ async function startGatewayForTui(config, options, callbacks) {
201
305
  maxWaitMs: config.maxQueueWaitMs
202
306
  });
203
307
  await pool.init(state.browser, seedPage);
308
+ const { queue: requestQueue, release } = await pool.acquire();
309
+ release();
310
+ const tracker = new RequestActivityTracker();
204
311
  const credentialHolder = new CredentialHolder();
205
312
  if (config.credentials) {
206
313
  credentialHolder.set(config.credentials);
207
314
  }
208
315
  const serverHandle = await startServer(config, {
209
316
  getPool: () => pool,
210
- getCredentials: () => credentialHolder.get()
317
+ getCredentials: () => credentialHolder.get(),
318
+ requestTracker: tracker
211
319
  });
212
320
  const tokenRefresher = new TokenRefresher(credentialHolder, config.cdpUrl);
213
321
  tokenRefresher.start();
214
322
  const baseUrl = `http://127.0.0.1:${config.port}`;
215
323
  const apiKey = config.auth.token ?? null;
216
324
  callbacks.onSuccess({ baseUrl, apiKey });
217
- return { serverHandle, chromeState: state, tokenRefresher };
325
+ return { serverHandle, chromeState: state, tokenRefresher, tracker, requestQueue };
218
326
  } catch (err) {
219
327
  const error = err instanceof Error ? err : new Error(String(err));
220
328
  callbacks.onError(error);
@@ -222,8 +330,9 @@ async function startGatewayForTui(config, options, callbacks) {
222
330
  throw error;
223
331
  }
224
332
  }
225
- async function stopGateway(serverHandle, chromeState, tokenRefresher) {
333
+ async function stopGateway(serverHandle, chromeState, tokenRefresher, tracker) {
226
334
  tokenRefresher.stop();
335
+ if (tracker) tracker.clear();
227
336
  await serverHandle.close();
228
337
  const chromeManager = new ChromeManager();
229
338
  await chromeManager.disconnect(chromeState);
@@ -271,15 +380,10 @@ var PANEL_SHORTCUTS = {
271
380
  { key: "b", label: "Back" },
272
381
  { key: "q", label: "Quit" }
273
382
  ],
274
- model: [
275
- { key: "\u2191\u2193", label: "Navigate" },
276
- { key: "Enter", label: "Select" },
277
- { key: "b", label: "Back" },
278
- { key: "q", label: "Quit" }
279
- ],
280
383
  chat: [
281
384
  { key: "Enter", label: "Send" },
282
- { key: "b", label: "Back" },
385
+ { key: "\u2191\u2193", label: "Model" },
386
+ { key: "Esc", label: "Back" },
283
387
  { key: "q", label: "Quit" }
284
388
  ],
285
389
  info: [
@@ -433,7 +537,7 @@ function SplashScreen({ onComplete }) {
433
537
  /* @__PURE__ */ jsx5(Starfield, { rows: termRows, cols: termCols, tick }),
434
538
  /* @__PURE__ */ jsx5(Box4, { width: "100%", justifyContent: "flex-end", paddingRight: 2, children: /* @__PURE__ */ jsxs4(Text4, { dimColor: true, color: "cyan", children: [
435
539
  "v",
436
- "2.2.0"
540
+ "2.3.0"
437
541
  ] }) }),
438
542
  /* @__PURE__ */ jsxs4(Box4, { flexDirection: "column", alignItems: "flex-start", paddingLeft: 2, children: [
439
543
  JHU_LOGO_LINES.map((line, i) => {
@@ -466,7 +570,6 @@ import { Box as Box5, Text as Text5, useInput as useInput3 } from "ink";
466
570
  // src/tui/types.ts
467
571
  var MENU_ITEMS = [
468
572
  { id: "gateway", label: "Start Gateway", description: "Launch Chrome, authenticate, and start the HTTP server" },
469
- { id: "model", label: "Model", description: "Select the active AI model" },
470
573
  { id: "chat", label: "Chat", description: "Send a test message to the running gateway" },
471
574
  { id: "info", label: "Server Info", description: "View and copy the server URL and API key" },
472
575
  { id: "settings", label: "Settings", description: "View and edit gateway configuration" },
@@ -543,7 +646,7 @@ function phaseColor(status) {
543
646
  }
544
647
  }
545
648
  function GatewayPanel() {
546
- const { state, navigate, setGatewayStatus, setGatewayError, setServerHandle, setChromeState, setTokenRefresher } = useAppContext();
649
+ const { state, navigate, setGatewayStatus, setGatewayError, setServerHandle, setChromeState, setTokenRefresher, setRequestTracker, setRequestQueue } = useAppContext();
547
650
  const { gatewayStatus, gatewayError, config, serverHandle, chromeState, tokenRefresher } = state;
548
651
  const [phases, setPhases] = useState4([]);
549
652
  const [starting, setStarting] = useState4(false);
@@ -596,6 +699,8 @@ function GatewayPanel() {
596
699
  setServerHandle(result.serverHandle);
597
700
  setChromeState(result.chromeState);
598
701
  setTokenRefresher(result.tokenRefresher);
702
+ setRequestTracker(result.tracker);
703
+ setRequestQueue(result.requestQueue);
599
704
  setGatewayStatus("running");
600
705
  } catch (err) {
601
706
  const message = err instanceof Error ? err.message : String(err);
@@ -604,22 +709,24 @@ function GatewayPanel() {
604
709
  } finally {
605
710
  setStarting(false);
606
711
  }
607
- }, [starting, stopping, config, setGatewayStatus, setGatewayError, setServerHandle, setChromeState, setTokenRefresher]);
712
+ }, [starting, stopping, config, setGatewayStatus, setGatewayError, setServerHandle, setChromeState, setTokenRefresher, setRequestTracker, setRequestQueue]);
608
713
  const handleStop = useCallback2(async () => {
609
714
  if (stopping || starting || !serverHandle || !chromeState || !tokenRefresher) return;
610
715
  setStopping(true);
611
716
  try {
612
- await stopGateway(serverHandle, chromeState, tokenRefresher);
717
+ await stopGateway(serverHandle, chromeState, tokenRefresher, state.requestTracker ?? void 0);
613
718
  setServerHandle(null);
614
719
  setChromeState(null);
615
720
  setTokenRefresher(null);
721
+ setRequestTracker(null);
722
+ setRequestQueue(null);
616
723
  setGatewayStatus("stopped");
617
724
  setGatewayError(null);
618
725
  setPhases([]);
619
726
  } finally {
620
727
  setStopping(false);
621
728
  }
622
- }, [stopping, starting, serverHandle, chromeState, tokenRefresher, setGatewayStatus, setGatewayError, setServerHandle, setChromeState, setTokenRefresher]);
729
+ }, [stopping, starting, serverHandle, chromeState, tokenRefresher, state.requestTracker, setGatewayStatus, setGatewayError, setServerHandle, setChromeState, setTokenRefresher, setRequestTracker, setRequestQueue]);
623
730
  useInput4((_input, key) => {
624
731
  if (key.return) {
625
732
  if (gatewayStatus === "running") {
@@ -671,69 +778,42 @@ function GatewayPanel() {
671
778
  ] });
672
779
  }
673
780
 
674
- // src/tui/panels/ModelSelector.tsx
781
+ // src/tui/panels/ChatPanel.tsx
675
782
  import { useState as useState5, useEffect as useEffect3 } from "react";
676
783
  import { Box as Box7, Text as Text7, useInput as useInput5 } from "ink";
677
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
784
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
678
785
  var MODELS = Object.keys(MODEL_ENDPOINT_MAP);
679
- function ModelSelector({ activeModel, onSelect, onBack }) {
680
- const [focusedIndex, setFocusedIndex] = useState5(() => {
786
+ function ChatPanel() {
787
+ const { state, navigate, setActiveModel } = useAppContext();
788
+ const { gatewayStatus, config, activeModel } = state;
789
+ const gatewayRunning = gatewayStatus === "running";
790
+ const [input, setInput] = useState5("");
791
+ const [loading, setLoading] = useState5(false);
792
+ const [lastMessage, setLastMessage] = useState5(null);
793
+ const [lastUserInput, setLastUserInput] = useState5(null);
794
+ const [showModelPicker, setShowModelPicker] = useState5(false);
795
+ const [modelFocusIndex, setModelFocusIndex] = useState5(() => {
681
796
  const idx = MODELS.indexOf(activeModel);
682
797
  return idx >= 0 ? idx : 0;
683
798
  });
684
799
  const [confirmationModel, setConfirmationModel] = useState5(null);
800
+ const [cursorVisible, setCursorVisible] = useState5(true);
801
+ useEffect3(() => {
802
+ const id = setInterval(() => setCursorVisible((v) => !v), 530);
803
+ return () => clearInterval(id);
804
+ }, []);
685
805
  useEffect3(() => {
686
806
  if (confirmationModel !== null) {
687
807
  const timer = setTimeout(() => setConfirmationModel(null), 1500);
688
808
  return () => clearTimeout(timer);
689
809
  }
690
810
  }, [confirmationModel]);
691
- useInput5((input, key) => {
692
- if (key.downArrow) {
693
- setFocusedIndex((i) => wrapIndex(i, 1, MODELS.length));
694
- } else if (key.upArrow) {
695
- setFocusedIndex((i) => wrapIndex(i, -1, MODELS.length));
696
- } else if (key.return) {
697
- const selected = MODELS[focusedIndex];
698
- updateConfig({ defaultModel: selected }).catch(() => {
699
- });
700
- onSelect(selected);
701
- setConfirmationModel(selected);
702
- } else if (input === "b" || key.escape) {
703
- onBack();
811
+ useEffect3(() => {
812
+ if (showModelPicker) {
813
+ const idx = MODELS.indexOf(activeModel);
814
+ setModelFocusIndex(idx >= 0 ? idx : 0);
704
815
  }
705
- });
706
- return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", padding: 1, children: [
707
- /* @__PURE__ */ jsx8(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx8(Text7, { bold: true, children: "Select Model" }) }),
708
- /* @__PURE__ */ jsx8(Box7, { flexDirection: "column", children: MODELS.map((model, index) => {
709
- const isFocused = index === focusedIndex;
710
- const isActive = model === activeModel;
711
- return /* @__PURE__ */ jsx8(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { color: isFocused ? "cyan" : void 0, bold: isFocused, children: [
712
- isFocused ? "> " : " ",
713
- /* @__PURE__ */ jsx8(Text7, { color: isActive ? "green" : "gray", children: isActive ? "\u25CF" : "\u25CB" }),
714
- " ",
715
- model
716
- ] }) }, model);
717
- }) }),
718
- confirmationModel !== null && /* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
719
- "\u2713 Selected: ",
720
- confirmationModel
721
- ] }) })
722
- ] });
723
- }
724
-
725
- // src/tui/panels/ChatPanel.tsx
726
- import { useState as useState6 } from "react";
727
- import { Box as Box8, Text as Text8, useInput as useInput6 } from "ink";
728
- import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
729
- function ChatPanel() {
730
- const { state, navigate } = useAppContext();
731
- const { gatewayStatus, config, activeModel } = state;
732
- const gatewayRunning = gatewayStatus === "running";
733
- const [input, setInput] = useState6("");
734
- const [loading, setLoading] = useState6(false);
735
- const [lastMessage, setLastMessage] = useState6(null);
736
- const [lastUserInput, setLastUserInput] = useState6(null);
816
+ }, [showModelPicker, activeModel]);
737
817
  const handleSubmit = async (value) => {
738
818
  if (!gatewayRunning) {
739
819
  navigate("gateway");
@@ -791,7 +871,24 @@ function ChatPanel() {
791
871
  setLoading(false);
792
872
  }
793
873
  };
794
- useInput6((_input, key) => {
874
+ useInput5((_input, key) => {
875
+ if (showModelPicker) {
876
+ if (key.downArrow) {
877
+ setModelFocusIndex((i) => wrapIndex(i, 1, MODELS.length));
878
+ } else if (key.upArrow) {
879
+ setModelFocusIndex((i) => wrapIndex(i, -1, MODELS.length));
880
+ } else if (key.return) {
881
+ const selected = MODELS[modelFocusIndex];
882
+ updateConfig({ defaultModel: selected }).catch(() => {
883
+ });
884
+ setActiveModel(selected);
885
+ setConfirmationModel(selected);
886
+ setShowModelPicker(false);
887
+ } else if (key.escape) {
888
+ setShowModelPicker(false);
889
+ }
890
+ return;
891
+ }
795
892
  if (!gatewayRunning) {
796
893
  if (key.return) navigate("gateway");
797
894
  else if (_input === "b" || key.escape) navigate("menu");
@@ -801,6 +898,15 @@ function ChatPanel() {
801
898
  if (!loading) navigate("menu");
802
899
  return;
803
900
  }
901
+ if ((key.upArrow || key.downArrow) && !loading) {
902
+ setShowModelPicker(true);
903
+ if (key.upArrow) {
904
+ setModelFocusIndex((i) => wrapIndex(i, -1, MODELS.length));
905
+ } else {
906
+ setModelFocusIndex((i) => wrapIndex(i, 1, MODELS.length));
907
+ }
908
+ return;
909
+ }
804
910
  if (key.return) {
805
911
  void handleSubmit(input);
806
912
  return;
@@ -815,42 +921,63 @@ function ChatPanel() {
815
921
  }
816
922
  });
817
923
  if (!gatewayRunning) {
818
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", padding: 1, children: [
819
- /* @__PURE__ */ jsx9(Box8, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text8, { bold: true, children: "Chat" }) }),
820
- /* @__PURE__ */ jsx9(Box8, { marginBottom: 1, children: /* @__PURE__ */ jsx9(Text8, { color: "yellow", children: "Gateway is not running. Press Enter to start it." }) }),
821
- /* @__PURE__ */ jsx9(Box8, { children: /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: "[b/Esc] Back" }) })
924
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", padding: 1, children: [
925
+ /* @__PURE__ */ jsx8(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx8(Text7, { bold: true, children: "Chat" }) }),
926
+ /* @__PURE__ */ jsx8(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsx8(Text7, { color: "yellow", children: "Gateway is not running. Press Enter to start it." }) }),
927
+ /* @__PURE__ */ jsx8(Box7, { children: /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: "[b/Esc] Back" }) })
822
928
  ] });
823
929
  }
824
- return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", padding: 1, children: [
825
- /* @__PURE__ */ jsxs8(Box8, { marginBottom: 1, children: [
826
- /* @__PURE__ */ jsx9(Text8, { bold: true, children: "Chat" }),
827
- /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
828
- " Model: ",
829
- activeModel
830
- ] })
930
+ return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", padding: 1, children: [
931
+ /* @__PURE__ */ jsxs7(Box7, { marginBottom: 1, children: [
932
+ /* @__PURE__ */ jsx8(Text7, { bold: true, children: "Chat" }),
933
+ /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: " Model: " }),
934
+ /* @__PURE__ */ jsx8(Text7, { color: "green", children: activeModel }),
935
+ confirmationModel !== null && /* @__PURE__ */ jsx8(Text7, { color: "green", children: " \u2713 Switched!" }),
936
+ !showModelPicker && /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: " [\u2191\u2193] change" })
831
937
  ] }),
832
- /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", marginBottom: 1, minHeight: 6, children: [
833
- lastUserInput && /* @__PURE__ */ jsx9(Box8, { marginBottom: 1, children: /* @__PURE__ */ jsxs8(Text8, { color: "cyan", children: [
938
+ showModelPicker && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [
939
+ /* @__PURE__ */ jsxs7(Box7, { marginBottom: 1, children: [
940
+ /* @__PURE__ */ jsx8(Text7, { bold: true, color: "cyan", children: "Select Model" }),
941
+ /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: " [\u2191\u2193] Navigate [Enter] Select [Esc] Cancel" })
942
+ ] }),
943
+ MODELS.map((model, index) => {
944
+ const isFocused = index === modelFocusIndex;
945
+ const isActive = model === activeModel;
946
+ return /* @__PURE__ */ jsx8(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { color: isFocused ? "cyan" : void 0, bold: isFocused, children: [
947
+ isFocused ? "> " : " ",
948
+ /* @__PURE__ */ jsx8(Text7, { color: isActive ? "green" : "gray", children: isActive ? "\u25CF" : "\u25CB" }),
949
+ " ",
950
+ model
951
+ ] }) }, model);
952
+ })
953
+ ] }),
954
+ !showModelPicker && /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", marginBottom: 1, minHeight: 6, children: [
955
+ lastUserInput && /* @__PURE__ */ jsx8(Box7, { marginBottom: 1, children: /* @__PURE__ */ jsxs7(Text7, { color: "cyan", children: [
834
956
  "You: ",
835
957
  lastUserInput
836
958
  ] }) }),
837
- loading && /* @__PURE__ */ jsx9(Box8, { children: /* @__PURE__ */ jsx9(Text8, { color: "yellow", children: "Thinking\u2026" }) }),
838
- !loading && lastMessage?.role === "assistant" && /* @__PURE__ */ jsx9(Box8, { children: /* @__PURE__ */ jsxs8(Text8, { color: "green", children: [
959
+ loading && /* @__PURE__ */ jsx8(Box7, { children: /* @__PURE__ */ jsx8(Text7, { color: "yellow", children: "Thinking\u2026" }) }),
960
+ !loading && lastMessage?.role === "assistant" && /* @__PURE__ */ jsx8(Box7, { children: /* @__PURE__ */ jsxs7(Text7, { color: "green", children: [
839
961
  "Assistant: ",
840
962
  lastMessage.content
841
963
  ] }) }),
842
- !loading && lastMessage?.role === "error" && /* @__PURE__ */ jsx9(Box8, { children: /* @__PURE__ */ jsx9(Text8, { color: "red", children: lastMessage.content }) })
964
+ !loading && lastMessage?.role === "error" && /* @__PURE__ */ jsx8(Box7, { children: /* @__PURE__ */ jsx8(Text7, { color: "red", children: lastMessage.content }) })
843
965
  ] }),
844
- /* @__PURE__ */ jsx9(Box8, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsxs8(Text8, { children: [
845
- input.length > 0 ? input : /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: "Type a message and press Enter\u2026" }),
846
- /* @__PURE__ */ jsx9(Text8, { color: "cyan", children: "\u2588" })
847
- ] }) }),
848
- /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: "[Enter] Send [b/Esc] Back" }) })
966
+ !showModelPicker && /* @__PURE__ */ jsxs7(Fragment2, { children: [
967
+ /* @__PURE__ */ jsx8(Box7, { borderStyle: "round", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsx8(Text7, { children: input.length > 0 ? /* @__PURE__ */ jsxs7(Fragment2, { children: [
968
+ input,
969
+ /* @__PURE__ */ jsx8(Text7, { color: "cyan", children: cursorVisible ? "\u2588" : " " })
970
+ ] }) : /* @__PURE__ */ jsxs7(Fragment2, { children: [
971
+ /* @__PURE__ */ jsx8(Text7, { color: "cyan", children: cursorVisible ? "\u2588" : " " }),
972
+ /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: "Type a message and press Enter\u2026" })
973
+ ] }) }) }),
974
+ /* @__PURE__ */ jsx8(Box7, { marginTop: 1, children: /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: "[Enter] Send [\u2191\u2193] Model [Esc] Back" }) })
975
+ ] })
849
976
  ] });
850
977
  }
851
978
 
852
979
  // src/tui/panels/InfoPanel.tsx
853
- import { useState as useState7, useEffect as useEffect4 } from "react";
980
+ import { useState as useState7, useEffect as useEffect5 } from "react";
854
981
  import { Box as Box9, Text as Text9, useInput as useInput7 } from "ink";
855
982
 
856
983
  // src/tui/utils/clipboard.ts
@@ -893,6 +1020,93 @@ async function copyToClipboard(text) {
893
1020
  });
894
1021
  }
895
1022
 
1023
+ // src/tui/components/RequestTracker.tsx
1024
+ import { useState as useState6, useEffect as useEffect4 } from "react";
1025
+ import { Box as Box8, Text as Text8, useInput as useInput6 } from "ink";
1026
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
1027
+ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1028
+ function getSpinnerFrame() {
1029
+ return SPINNER_FRAMES[Math.floor(Date.now() / 100) % SPINNER_FRAMES.length];
1030
+ }
1031
+ function RequestEntryRow({ entry, tick: _tick }) {
1032
+ const elapsed = entry.status === "active" ? formatElapsed(Date.now() - entry.startTime) : entry.elapsedMs !== null ? formatElapsed(entry.elapsedMs) : "";
1033
+ if (entry.status === "active") {
1034
+ return /* @__PURE__ */ jsxs8(Text8, { children: [
1035
+ /* @__PURE__ */ jsx9(Text8, { color: "yellow", children: getSpinnerFrame() }),
1036
+ " ",
1037
+ /* @__PURE__ */ jsx9(Text8, { children: entry.method }),
1038
+ " ",
1039
+ /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: entry.path }),
1040
+ " ",
1041
+ /* @__PURE__ */ jsx9(Text8, { color: "yellow", children: elapsed })
1042
+ ] });
1043
+ }
1044
+ const codeColor = entry.statusCode !== null && entry.statusCode >= 200 && entry.statusCode < 300 ? "green" : "red";
1045
+ return /* @__PURE__ */ jsxs8(Text8, { children: [
1046
+ /* @__PURE__ */ jsx9(Text8, { color: codeColor, children: entry.statusCode ?? "???" }),
1047
+ " ",
1048
+ /* @__PURE__ */ jsx9(Text8, { children: entry.method }),
1049
+ " ",
1050
+ /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: entry.path }),
1051
+ " ",
1052
+ /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: elapsed })
1053
+ ] });
1054
+ }
1055
+ var VISIBLE_ROWS = 8;
1056
+ function RequestTracker() {
1057
+ const { state } = useAppContext();
1058
+ const { requestTracker: tracker, requestQueue: queue, gatewayStatus } = state;
1059
+ const online = gatewayStatus === "running";
1060
+ const [entries, setEntries] = useState6([]);
1061
+ useEffect4(() => {
1062
+ if (!tracker) {
1063
+ setEntries([]);
1064
+ return;
1065
+ }
1066
+ setEntries(tracker.getEntries());
1067
+ const unsub = tracker.subscribe((snapshot) => setEntries(snapshot));
1068
+ return unsub;
1069
+ }, [tracker]);
1070
+ const [tick, setTick] = useState6(0);
1071
+ const hasActive = entries.some((e) => e.status === "active");
1072
+ useEffect4(() => {
1073
+ if (!hasActive) return;
1074
+ const id = setInterval(() => setTick((t) => t + 1), 200);
1075
+ return () => clearInterval(id);
1076
+ }, [hasActive]);
1077
+ const [scrollOffset, setScrollOffset] = useState6(0);
1078
+ const maxOffset = Math.max(0, entries.length - VISIBLE_ROWS);
1079
+ useEffect4(() => {
1080
+ setScrollOffset((prev) => Math.min(prev, maxOffset));
1081
+ }, [maxOffset]);
1082
+ useInput6((_input, key) => {
1083
+ if (_input === "j" || key.downArrow) {
1084
+ setScrollOffset((prev) => Math.min(prev + 1, maxOffset));
1085
+ }
1086
+ if (_input === "k" || key.upArrow) {
1087
+ setScrollOffset((prev) => Math.max(prev - 1, 0));
1088
+ }
1089
+ });
1090
+ const visibleEntries = entries.slice(scrollOffset, scrollOffset + VISIBLE_ROWS);
1091
+ const queueDepth = formatQueueDepth(queue?.pending ?? 0, online);
1092
+ return /* @__PURE__ */ jsxs8(Box8, { flexDirection: "column", children: [
1093
+ /* @__PURE__ */ jsx9(Text8, { bold: true, children: "Server Activity" }),
1094
+ /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: queueDepth }),
1095
+ !online && /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text8, { color: "red", children: "Gateway offline" }) }),
1096
+ online && entries.length === 0 && /* @__PURE__ */ jsx9(Box8, { marginTop: 1, children: /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: "No recent activity" }) }),
1097
+ entries.length > 0 && /* @__PURE__ */ jsx9(Box8, { height: VISIBLE_ROWS, overflowY: "hidden", flexDirection: "column", marginTop: 1, children: visibleEntries.map((entry) => /* @__PURE__ */ jsx9(RequestEntryRow, { entry, tick }, entry.id)) }),
1098
+ entries.length > VISIBLE_ROWS && /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
1099
+ "\u2191\u2193/jk to scroll (",
1100
+ scrollOffset + 1,
1101
+ "\u2013",
1102
+ Math.min(scrollOffset + VISIBLE_ROWS, entries.length),
1103
+ " of ",
1104
+ entries.length,
1105
+ ")"
1106
+ ] })
1107
+ ] });
1108
+ }
1109
+
896
1110
  // src/tui/panels/InfoPanel.tsx
897
1111
  import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
898
1112
  function InfoPanel() {
@@ -904,7 +1118,7 @@ function InfoPanel() {
904
1118
  const baseUrl = `http://127.0.0.1:${port}`;
905
1119
  const [flash, setFlash] = useState7(null);
906
1120
  const [clipboardFailed, setClipboardFailed] = useState7({});
907
- useEffect4(() => {
1121
+ useEffect5(() => {
908
1122
  if (flash !== null) {
909
1123
  const timer = setTimeout(() => setFlash(null), 1500);
910
1124
  return () => clearTimeout(timer);
@@ -962,6 +1176,7 @@ function InfoPanel() {
962
1176
  clipboardFailed.key && apiKey && /* @__PURE__ */ jsx10(Box9, { marginTop: 1, children: /* @__PURE__ */ jsx10(Box9, { borderStyle: "single", borderColor: "yellow", paddingX: 1, children: /* @__PURE__ */ jsx10(Text9, { color: "yellow", children: apiKey }) }) })
963
1177
  ] }),
964
1178
  flash && /* @__PURE__ */ jsx10(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx10(Text9, { color: flash.startsWith("Copied") ? "green" : "yellow", children: flash }) }),
1179
+ /* @__PURE__ */ jsx10(Box9, { marginBottom: 1, children: /* @__PURE__ */ jsx10(RequestTracker, {}) }),
965
1180
  /* @__PURE__ */ jsx10(Box9, { children: /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
966
1181
  "[c] Copy URL ",
967
1182
  apiKey ? "[k] Copy Key " : "",
@@ -1145,14 +1360,14 @@ function SettingsPanel() {
1145
1360
  // src/tui/App.tsx
1146
1361
  import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
1147
1362
  function AppContent({ onRegisterShutdown }) {
1148
- const { state, navigate, setActiveModel } = useAppContext();
1149
- const { currentPanel, gatewayStatus, activeModel } = state;
1363
+ const { state, navigate } = useAppContext();
1364
+ const { currentPanel, gatewayStatus } = state;
1150
1365
  const [showQuit, setShowQuit] = useState9(false);
1151
1366
  const latestStateRef = useRef2(state);
1152
- useEffect5(() => {
1367
+ useEffect6(() => {
1153
1368
  latestStateRef.current = state;
1154
1369
  });
1155
- useEffect5(() => {
1370
+ useEffect6(() => {
1156
1371
  if (onRegisterShutdown) {
1157
1372
  onRegisterShutdown(async () => {
1158
1373
  const { gatewayStatus: status, serverHandle, chromeState, tokenRefresher } = latestStateRef.current;
@@ -1178,15 +1393,6 @@ function AppContent({ onRegisterShutdown }) {
1178
1393
  return /* @__PURE__ */ jsx12(MainMenu, { onQuit: () => setShowQuit(true) });
1179
1394
  case "gateway":
1180
1395
  return /* @__PURE__ */ jsx12(GatewayPanel, {});
1181
- case "model":
1182
- return /* @__PURE__ */ jsx12(
1183
- ModelSelector,
1184
- {
1185
- activeModel,
1186
- onSelect: (model) => setActiveModel(model),
1187
- onBack: () => navigate("menu")
1188
- }
1189
- );
1190
1396
  case "chat":
1191
1397
  return /* @__PURE__ */ jsx12(ChatPanel, {});
1192
1398
  case "info":
@@ -1264,4 +1470,4 @@ async function launchTui() {
1264
1470
  export {
1265
1471
  launchTui
1266
1472
  };
1267
- //# sourceMappingURL=tui-GRDJWXQL.js.map
1473
+ //# sourceMappingURL=tui-QXRXB44O.js.map