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 CHANGED
@@ -90,7 +90,6 @@ The header shows the gateway status at all times. Use arrow keys to navigate and
90
90
  │ Main Menu │
91
91
  │ │
92
92
  │ > Start Gateway │
93
- │ Model │
94
93
  │ Chat │
95
94
  │ Server Info │
96
95
  │ Settings │
@@ -170,38 +169,9 @@ Once all phases complete the gateway is live:
170
169
 
171
170
  ---
172
171
 
173
- ### Model Selector
174
-
175
- Choose the AI model for all requests. The currently active model is marked with a filled circle (`●`).
176
-
177
- ```text
178
- ┌─────────────────────────────────────────────────────────────┐
179
- │ jh-gateway ● Gateway: running │
180
- ├─────────────────────────────────────────────────────────────┤
181
- │ │
182
- │ Select Model │
183
- │ │
184
- │ > ● claude-opus-4.5 ← active model │
185
- │ ○ claude-sonnet-4.5 │
186
- │ ○ claude-haiku-4.5 │
187
- │ ○ gpt-4.1 │
188
- │ ○ gpt-5 │
189
- │ ○ gpt-5.1 │
190
- │ ○ o3 │
191
- │ ○ o3-mini │
192
- │ ○ llama3-3-70b-instruct │
193
- │ │
194
- │ [↑↓] Navigate [Enter] Select [b/Esc] Back │
195
- └─────────────────────────────────────────────────────────────┘
196
- ```
197
-
198
- The selection is saved to `~/.jh-gateway/config.json` as `defaultModel` immediately on press.
199
-
200
- ---
201
-
202
172
  ### Chat Panel
203
173
 
204
- Send a quick test message from inside the TUI — no external tool required. The panel shows the last exchange and streams a response from the running gateway.
174
+ Send a quick test message from inside the TUI — no external tool required. The panel shows the last exchange and streams a response from the running gateway. Use `↑`/`↓` to cycle through models without leaving the chat.
205
175
 
206
176
  ```text
207
177
  ┌─────────────────────────────────────────────────────────────┐
@@ -218,7 +188,7 @@ Send a quick test message from inside the TUI — no external tool required. The
218
188
  │ │ Type a message and press Enter… █ │ │
219
189
  │ ╰─────────────────────────────────────────────────────╯ │
220
190
  │ │
221
- │ [Enter] Send [b/Esc] Back
191
+ │ [Enter] Send [↑↓] Model [b/Esc] Back
222
192
  └─────────────────────────────────────────────────────────────┘
223
193
  ```
224
194
 
@@ -228,6 +198,7 @@ Send a quick test message from inside the TUI — no external tool required. The
228
198
  |-----|--------|
229
199
  | Type | Compose your message |
230
200
  | `Enter` | Send the message |
201
+ | `↑` / `↓` | Cycle through models |
231
202
  | `Backspace` | Delete last character |
232
203
  | `b` / `Esc` | Back to menu |
233
204
 
@@ -235,7 +206,7 @@ Send a quick test message from inside the TUI — no external tool required. The
235
206
 
236
207
  ### Server Info Panel
237
208
 
238
- Displays the live base URL and API key. Use one-key shortcuts to copy them directly to the clipboard.
209
+ Displays the live base URL, API key, and real-time server activity. Use one-key shortcuts to copy connection details directly to the clipboard.
239
210
 
240
211
  ```text
241
212
  ┌─────────────────────────────────────────────────────────────┐
@@ -249,6 +220,10 @@ Displays the live base URL and API key. Use one-key shortcuts to copy them direc
249
220
  │ │ API Key: jh-local-xxxxxxxxxxxxxxxxxxxxxxxx │ │
250
221
  │ ╰───────────────────────────────────────────────────╯ │
251
222
  │ │
223
+ │ Activity │
224
+ │ ● POST /v1/chat/completions 200 1.2s claude-opus-4.5 │
225
+ │ ● GET /v1/models 200 0.1s │
226
+ │ │
252
227
  │ Copied URL! │
253
228
  │ │
254
229
  │ [c] Copy URL [k] Copy Key [b/Esc] Back │
@@ -304,7 +279,7 @@ Changes are validated and written to `~/.jh-gateway/config.json` on confirm.
304
279
 
305
280
  | Key | Context | Action |
306
281
  |-----|---------|--------|
307
- | `↑` / `↓` | Menu, Model, Settings | Navigate items |
282
+ | `↑` / `↓` | Menu, Chat, Settings | Navigate items / cycle models |
308
283
  | `Enter` | Main Menu | Open selected panel |
309
284
  | `Enter` | Gateway Panel | Start / Stop gateway |
310
285
  | `Enter` | Chat | Send message |
@@ -958,9 +958,11 @@ function escapeXmlAttr(s) {
958
958
  import { randomBytes as randomBytes2 } from "crypto";
959
959
 
960
960
  // src/core/tool-parser.ts
961
- var TOOL_CALL_RE = /<tool_call\s+id="([^"]*)"\s+name="([^"]*)">([\s\S]*?)<\/tool_call>/g;
961
+ var TOOL_CALL_RE = /<tool_call\s+(?=[^>]*\bid="([^"]*)")(?=[^>]*\bname="([^"]*)")(?:[^>]*)>([\s\S]*?)<\/tool_call>/g;
962
+ var TOOL_RESPONSE_RE = /<tool_response\b[^>]*>[\s\S]*?<\/tool_response>/g;
962
963
  var THINK_RE = /<think>([\s\S]*?)<\/think>/g;
963
964
  var PARTIAL_TOOL_CALL_RE = /<tool_call\b[^>]*>(?:(?!<\/tool_call>)[\s\S])*$/;
965
+ var PARTIAL_TOOL_RESPONSE_RE = /<tool_response\b[^>]*>(?:(?!<\/tool_response>)[\s\S])*$/;
964
966
  function parseToolsAndThinking(text) {
965
967
  const toolCalls = [];
966
968
  let thinking = null;
@@ -969,6 +971,7 @@ function parseToolsAndThinking(text) {
969
971
  thinking = thinkMatches.map((m) => m[1]).join("\n");
970
972
  }
971
973
  let remaining = text.replace(THINK_RE, "");
974
+ remaining = remaining.replace(TOOL_RESPONSE_RE, "");
972
975
  const toolMatches = [...remaining.matchAll(TOOL_CALL_RE)];
973
976
  for (const match of toolMatches) {
974
977
  const id = match[1];
@@ -1002,17 +1005,57 @@ function toOpenAIToolCalls(calls) {
1002
1005
  }
1003
1006
  }));
1004
1007
  }
1008
+ var WATCHED_TAGS = ["<tool_call", "<tool_response"];
1009
+ var MAX_TAG_PREFIX_LEN = Math.max(...WATCHED_TAGS.map((t) => t.length));
1010
+ function findWatchedTagPrefixAtEnd(text) {
1011
+ const maxLen = Math.min(text.length, MAX_TAG_PREFIX_LEN);
1012
+ for (let len = maxLen; len >= 1; len--) {
1013
+ const tail = text.slice(-len);
1014
+ for (const tag of WATCHED_TAGS) {
1015
+ if (tag.startsWith(tail)) {
1016
+ return text.length - len;
1017
+ }
1018
+ }
1019
+ }
1020
+ return -1;
1021
+ }
1022
+ function findPartialWatchedTag(text) {
1023
+ if (PARTIAL_TOOL_RESPONSE_RE.test(text)) {
1024
+ const idx = text.search(/<tool_response\b/);
1025
+ if (idx >= 0) return idx;
1026
+ }
1027
+ if (PARTIAL_TOOL_CALL_RE.test(text)) {
1028
+ const idx = text.search(/<tool_call\b/);
1029
+ if (idx >= 0) return idx;
1030
+ }
1031
+ return -1;
1032
+ }
1033
+ function findUnclosedWatchedTag(text) {
1034
+ for (const tag of WATCHED_TAGS) {
1035
+ const idx = text.lastIndexOf(tag);
1036
+ if (idx >= 0) {
1037
+ const afterTag = text.slice(idx);
1038
+ if (!afterTag.includes(">")) {
1039
+ return idx;
1040
+ }
1041
+ }
1042
+ }
1043
+ return -1;
1044
+ }
1005
1045
  var StreamingToolBuffer = class {
1006
1046
  buffer = "";
1007
1047
  /**
1008
1048
  * Push a text chunk. Returns an object with:
1009
- * - `text`: safe-to-emit text (outside any partial tag)
1049
+ * - `text`: safe-to-emit text (outside any partial/complete tag)
1010
1050
  * - `completedCalls`: fully parsed tool calls from this chunk
1011
1051
  */
1012
1052
  push(chunk) {
1013
1053
  this.buffer += chunk;
1014
1054
  const completedCalls = [];
1015
1055
  let safeText = "";
1056
+ this.buffer = this.buffer.replace(TOOL_RESPONSE_RE, (match2, offset) => {
1057
+ return "";
1058
+ });
1016
1059
  let match;
1017
1060
  const re = new RegExp(TOOL_CALL_RE.source, "g");
1018
1061
  let lastIndex = 0;
@@ -1032,15 +1075,27 @@ var StreamingToolBuffer = class {
1032
1075
  lastIndex = match.index + match[0].length;
1033
1076
  }
1034
1077
  const remainder = this.buffer.slice(lastIndex);
1035
- if (PARTIAL_TOOL_CALL_RE.test(remainder)) {
1036
- const partialStart = remainder.search(/<tool_call\b/);
1037
- if (partialStart > 0) {
1038
- safeText += remainder.slice(0, partialStart);
1078
+ const partialIdx = findPartialWatchedTag(remainder);
1079
+ if (partialIdx >= 0) {
1080
+ if (partialIdx > 0) {
1081
+ safeText += remainder.slice(0, partialIdx);
1039
1082
  }
1040
- this.buffer = partialStart >= 0 ? remainder.slice(partialStart) : remainder;
1083
+ this.buffer = remainder.slice(partialIdx);
1041
1084
  } else {
1042
- safeText += remainder;
1043
- this.buffer = "";
1085
+ const unclosedIdx = findUnclosedWatchedTag(remainder);
1086
+ if (unclosedIdx >= 0) {
1087
+ safeText += remainder.slice(0, unclosedIdx);
1088
+ this.buffer = remainder.slice(unclosedIdx);
1089
+ } else {
1090
+ const prefixStart = findWatchedTagPrefixAtEnd(remainder);
1091
+ if (prefixStart >= 0) {
1092
+ safeText += remainder.slice(0, prefixStart);
1093
+ this.buffer = remainder.slice(prefixStart);
1094
+ } else {
1095
+ safeText += remainder;
1096
+ this.buffer = "";
1097
+ }
1098
+ }
1044
1099
  }
1045
1100
  return { text: safeText, completedCalls };
1046
1101
  }
@@ -1610,10 +1665,27 @@ var Logger = class {
1610
1665
  };
1611
1666
 
1612
1667
  // src/server.ts
1668
+ function requestTrackerMiddleware(tracker) {
1669
+ return async (c, next) => {
1670
+ const id = crypto.randomUUID();
1671
+ c.set("requestId", id);
1672
+ tracker.start(id, c.req.method, c.req.path);
1673
+ try {
1674
+ await next();
1675
+ tracker.end(id, c.res.status);
1676
+ } catch (err) {
1677
+ tracker.end(id, 500);
1678
+ throw err;
1679
+ }
1680
+ };
1681
+ }
1613
1682
  function createServer(config, deps) {
1614
1683
  const app = new Hono4();
1615
1684
  const startTime = Date.now();
1616
1685
  const logger = new Logger();
1686
+ if (deps?.requestTracker) {
1687
+ app.use("*", requestTrackerMiddleware(deps.requestTracker));
1688
+ }
1617
1689
  app.use("/v1/*", authMiddleware(config));
1618
1690
  app.use("*", async (c, next) => {
1619
1691
  const start = Date.now();
@@ -2323,4 +2395,4 @@ export {
2323
2395
  TokenRefresher,
2324
2396
  ChromeManager
2325
2397
  };
2326
- //# sourceMappingURL=chunk-Y2NMKJOG.js.map
2398
+ //# sourceMappingURL=chunk-E6JMUHPA.js.map