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.
- package/README.md +9 -34
- package/dist/{chunk-Y2NMKJOG.js → chunk-E6JMUHPA.js} +82 -10
- package/dist/chunk-E6JMUHPA.js.map +1 -0
- package/dist/cli.js +3 -3
- package/dist/{tui-GRDJWXQL.js → tui-QXRXB44O.js} +313 -107
- package/dist/tui-QXRXB44O.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-Y2NMKJOG.js.map +0 -1
- package/dist/tui-GRDJWXQL.js.map +0 -1
|
@@ -12,14 +12,14 @@ import {
|
|
|
12
12
|
startServer,
|
|
13
13
|
updateConfig,
|
|
14
14
|
validateConfig
|
|
15
|
-
} from "./chunk-
|
|
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
|
|
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: "
|
|
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.
|
|
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/
|
|
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
|
|
680
|
-
const
|
|
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
|
-
|
|
692
|
-
if (
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
|
|
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__ */
|
|
819
|
-
/* @__PURE__ */
|
|
820
|
-
/* @__PURE__ */
|
|
821
|
-
/* @__PURE__ */
|
|
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__ */
|
|
825
|
-
/* @__PURE__ */
|
|
826
|
-
/* @__PURE__ */
|
|
827
|
-
/* @__PURE__ */
|
|
828
|
-
|
|
829
|
-
|
|
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__ */
|
|
833
|
-
|
|
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__ */
|
|
838
|
-
!loading && lastMessage?.role === "assistant" && /* @__PURE__ */
|
|
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__ */
|
|
964
|
+
!loading && lastMessage?.role === "error" && /* @__PURE__ */ jsx8(Box7, { children: /* @__PURE__ */ jsx8(Text7, { color: "red", children: lastMessage.content }) })
|
|
843
965
|
] }),
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1149
|
-
const { currentPanel, gatewayStatus
|
|
1363
|
+
const { state, navigate } = useAppContext();
|
|
1364
|
+
const { currentPanel, gatewayStatus } = state;
|
|
1150
1365
|
const [showQuit, setShowQuit] = useState9(false);
|
|
1151
1366
|
const latestStateRef = useRef2(state);
|
|
1152
|
-
|
|
1367
|
+
useEffect6(() => {
|
|
1153
1368
|
latestStateRef.current = state;
|
|
1154
1369
|
});
|
|
1155
|
-
|
|
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-
|
|
1473
|
+
//# sourceMappingURL=tui-QXRXB44O.js.map
|