qwenproxy-cli 1.1.0 → 1.2.1
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.es.md +1 -3
- package/README.md +1 -3
- package/README.pt-BR.md +2 -4
- package/bin/qwenproxy.js +7 -3
- package/bin/update.d.ts +11 -0
- package/bin/update.js +155 -0
- package/package.json +3 -3
- package/src/api/models.ts +18 -2
- package/src/core/config.ts +1 -1
- package/src/core/model-alias.ts +17 -0
- package/src/reset-cooldowns.ts +5 -1
- package/src/services/captcha-coordinator.ts +1 -1
- package/src/services/playwright.ts +22 -11
- package/src/services/qwen.ts +58 -22
- package/src/sync/claude-code.ts +41 -4
- package/src/sync/cline.ts +34 -4
- package/src/sync/codex.ts +29 -4
- package/src/sync/index.ts +98 -70
- package/src/sync/omp.ts +23 -4
- package/src/sync/opencode.ts +28 -4
- package/src/sync/utils.ts +32 -3
- package/src/sync/zed.ts +28 -4
- package/src/sync-clients.ts +3 -4
- package/src/tools/parser.ts +16 -11
- package/src/tui/app.ts +19 -1
- package/src/tui/proxy-client.ts +17 -14
- package/src/tui/theme.ts +1 -0
- package/src/tui/views/chat-view.ts +97 -61
- package/src/tui/views/status-view.ts +68 -15
- package/src/tui/views/sync-view.ts +87 -32
- package/src/update-cli.ts +10 -154
package/src/tui/proxy-client.ts
CHANGED
|
@@ -336,18 +336,17 @@ export async function streamChatCompletions(
|
|
|
336
336
|
let cachedLiveModels: string[] | null = null;
|
|
337
337
|
let liveModelsPromise: Promise<string[]> | null = null;
|
|
338
338
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
];
|
|
339
|
+
export { DEFAULT_FALLBACK_MODELS } from "../core/model-alias.ts";
|
|
340
|
+
import { DEFAULT_FALLBACK_MODELS } from "../core/model-alias.ts";
|
|
341
|
+
|
|
342
|
+
export function getCachedLiveModels(): string[] | null {
|
|
343
|
+
return cachedLiveModels;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function resetCachedLiveModelsForTests(): void {
|
|
347
|
+
cachedLiveModels = null;
|
|
348
|
+
liveModelsPromise = null;
|
|
349
|
+
}
|
|
351
350
|
|
|
352
351
|
export async function fetchLiveModels(forceRefresh = false): Promise<string[]> {
|
|
353
352
|
if (!forceRefresh && cachedLiveModels && cachedLiveModels.length > 0) {
|
|
@@ -365,7 +364,9 @@ export async function fetchLiveModels(forceRefresh = false): Promise<string[]> {
|
|
|
365
364
|
|
|
366
365
|
liveModelsPromise = (async () => {
|
|
367
366
|
const controller = new AbortController();
|
|
368
|
-
|
|
367
|
+
// Cold start with Playwright navigation and bx security token acquisition can take 5-12s
|
|
368
|
+
const timeoutMs = cachedLiveModels ? 4000 : 12000;
|
|
369
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
369
370
|
try {
|
|
370
371
|
const resp = await fetch(`http://${host}:${port}/v1/models`, {
|
|
371
372
|
headers: { Authorization: `Bearer ${apiKey}` },
|
|
@@ -389,7 +390,9 @@ export async function fetchLiveModels(forceRefresh = false): Promise<string[]> {
|
|
|
389
390
|
}
|
|
390
391
|
}
|
|
391
392
|
}
|
|
392
|
-
} catch {
|
|
393
|
+
} catch {
|
|
394
|
+
// Don't poison cachedLiveModels on network error so subsequent polls can retry
|
|
395
|
+
} finally {
|
|
393
396
|
clearTimeout(timeout);
|
|
394
397
|
liveModelsPromise = null;
|
|
395
398
|
}
|
package/src/tui/theme.ts
CHANGED
|
@@ -39,6 +39,7 @@ export const theme = {
|
|
|
39
39
|
borderInactive: (s: string) => `\x1b[38;2;53;53;61m${s}\x1b[39m`, // #35353d (Qwen Line Primary Border)
|
|
40
40
|
bgSelected: (s: string) => `\x1b[48;2;45;35;85m\x1b[38;2;247;248;252m${s}\x1b[49m\x1b[39m`, // #2d2355 Deep Violet + #f7f8fc White
|
|
41
41
|
bgHover: (s: string) => `\x1b[48;2;58;46;110m\x1b[38;2;247;248;252m${s}\x1b[49m\x1b[39m`, // #3a2e6e Violet Hover + #f7f8fc White
|
|
42
|
+
bgUserCard: (s: string) => `\x1b[48;2;38;40;58m${s}\x1b[49m`, // #26283a (Catppuccin Surface / Distinct Lighter User Message Card)
|
|
42
43
|
bold: (s: string) => `\x1b[1m${s}\x1b[22m`,
|
|
43
44
|
italic: (s: string) => `\x1b[3m${s}\x1b[23m`,
|
|
44
45
|
dim: (s: string) => `\x1b[2m${s}\x1b[22m`,
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
import type { TuiView, ProxyStatusSnapshot } from "../types.ts";
|
|
6
6
|
import type { KeyEvent } from "../screen.ts";
|
|
7
7
|
import { theme, glyphs, drawBox, stringWidth, truncate, stripAnsi, pad, wrapContentLine } from "../theme.ts";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
fetchLiveModels,
|
|
10
|
+
streamChatCompletions,
|
|
11
|
+
DEFAULT_FALLBACK_MODELS,
|
|
12
|
+
} from "../proxy-client.ts";
|
|
9
13
|
import { ServerManager } from "../server-manager.ts";
|
|
10
14
|
import { formatMarkdown, formatReasoning } from "../markdown.ts";
|
|
11
15
|
import { loadTuiSettings, saveTuiSettings } from "../settings.ts";
|
|
@@ -22,7 +26,11 @@ interface ChatMessage {
|
|
|
22
26
|
cachedReasoningBox?: string[];
|
|
23
27
|
cachedWidth?: number;
|
|
24
28
|
}
|
|
25
|
-
export function classifyModel(modelId: string): {
|
|
29
|
+
export function classifyModel(modelId: string): {
|
|
30
|
+
badge: string;
|
|
31
|
+
category: string;
|
|
32
|
+
supportsReasoning: boolean;
|
|
33
|
+
} {
|
|
26
34
|
const lower = modelId.toLowerCase();
|
|
27
35
|
if (
|
|
28
36
|
lower.includes("image") ||
|
|
@@ -30,12 +38,35 @@ export function classifyModel(modelId: string): { badge: string; category: strin
|
|
|
30
38
|
lower.includes("t2i") ||
|
|
31
39
|
lower.includes("i2i")
|
|
32
40
|
) {
|
|
33
|
-
return {
|
|
41
|
+
return {
|
|
42
|
+
badge: theme.lavender("[Imagem]"),
|
|
43
|
+
category: "Geração de Imagem",
|
|
44
|
+
supportsReasoning: false,
|
|
45
|
+
};
|
|
34
46
|
}
|
|
35
47
|
if (lower.includes("video") || lower.includes("t2v") || lower.includes("i2v")) {
|
|
36
|
-
return {
|
|
48
|
+
return {
|
|
49
|
+
badge: theme.peach("[Vídeo] "),
|
|
50
|
+
category: "Geração de Vídeo",
|
|
51
|
+
supportsReasoning: false,
|
|
52
|
+
};
|
|
37
53
|
}
|
|
38
|
-
|
|
54
|
+
if (
|
|
55
|
+
lower.includes("omni") ||
|
|
56
|
+
lower.includes("audio") ||
|
|
57
|
+
lower.includes("speech")
|
|
58
|
+
) {
|
|
59
|
+
return {
|
|
60
|
+
badge: theme.green("[Omni] "),
|
|
61
|
+
category: "Multimodal / Omni",
|
|
62
|
+
supportsReasoning: true,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
badge: theme.cyan("[Texto] "),
|
|
67
|
+
category: "Texto & Raciocínio",
|
|
68
|
+
supportsReasoning: true,
|
|
69
|
+
};
|
|
39
70
|
}
|
|
40
71
|
|
|
41
72
|
export class ChatView implements TuiView {
|
|
@@ -43,14 +74,7 @@ export class ChatView implements TuiView {
|
|
|
43
74
|
public readonly title = "Chat";
|
|
44
75
|
public readonly tabNumber = 2;
|
|
45
76
|
|
|
46
|
-
private availableModels = [
|
|
47
|
-
"qwen3.8-max",
|
|
48
|
-
"qwen3.7-plus",
|
|
49
|
-
"qwen3.7-max",
|
|
50
|
-
"z-image-turbo",
|
|
51
|
-
"qwen-image-3.0-pro",
|
|
52
|
-
"wan3.0-video",
|
|
53
|
-
];
|
|
77
|
+
private availableModels = [...DEFAULT_FALLBACK_MODELS];
|
|
54
78
|
private selectedModelIndex = 0;
|
|
55
79
|
private messages: ChatMessage[] = [];
|
|
56
80
|
private inputBuffer = "";
|
|
@@ -254,7 +278,7 @@ export class ChatView implements TuiView {
|
|
|
254
278
|
},
|
|
255
279
|
});
|
|
256
280
|
const info = classifyModel(chosen);
|
|
257
|
-
if (info.
|
|
281
|
+
if (info.supportsReasoning) {
|
|
258
282
|
this.isEffortModalOpen = true;
|
|
259
283
|
const effIdx = this.availableEfforts.findIndex((e) => e.id === this.selectedEffort);
|
|
260
284
|
this.effortSelectedIndex = effIdx !== -1 ? effIdx : 0;
|
|
@@ -317,8 +341,9 @@ export class ChatView implements TuiView {
|
|
|
317
341
|
if (this.isEffortModalOpen) {
|
|
318
342
|
if (key.name === "hover" && key.mouse) {
|
|
319
343
|
const { row } = key.mouse;
|
|
320
|
-
|
|
321
|
-
|
|
344
|
+
const startRow = 12;
|
|
345
|
+
if (row >= startRow && row < startRow + this.availableEfforts.length) {
|
|
346
|
+
const hoverIdx = row - startRow;
|
|
322
347
|
if (this.effortSelectedIndex !== hoverIdx) {
|
|
323
348
|
this.effortSelectedIndex = hoverIdx;
|
|
324
349
|
this.onNeedsRender?.();
|
|
@@ -328,8 +353,10 @@ export class ChatView implements TuiView {
|
|
|
328
353
|
}
|
|
329
354
|
if (key.name === "click" && key.mouse) {
|
|
330
355
|
const { row } = key.mouse;
|
|
331
|
-
|
|
332
|
-
|
|
356
|
+
const startRow = 12;
|
|
357
|
+
if (row >= startRow && row < startRow + this.availableEfforts.length) {
|
|
358
|
+
const chosenIdx = row - startRow;
|
|
359
|
+
this.selectedEffort = this.availableEfforts[chosenIdx].id;
|
|
333
360
|
this.isEffortModalOpen = false;
|
|
334
361
|
const currentM = this.availableModels[this.selectedModelIndex];
|
|
335
362
|
saveTuiSettings({
|
|
@@ -338,7 +365,7 @@ export class ChatView implements TuiView {
|
|
|
338
365
|
effort: this.selectedEffort,
|
|
339
366
|
},
|
|
340
367
|
});
|
|
341
|
-
this.statusNote = `Modelo: ${currentM} | Effort: ${this.availableEfforts[
|
|
368
|
+
this.statusNote = `Modelo: ${currentM} | Effort: ${this.availableEfforts[chosenIdx].label}`;
|
|
342
369
|
this.onNeedsRender?.();
|
|
343
370
|
return true;
|
|
344
371
|
}
|
|
@@ -386,7 +413,7 @@ export class ChatView implements TuiView {
|
|
|
386
413
|
if (this.isModeModalOpen) {
|
|
387
414
|
if (key.name === "hover" && key.mouse) {
|
|
388
415
|
const { row } = key.mouse;
|
|
389
|
-
const startRow =
|
|
416
|
+
const startRow = 12;
|
|
390
417
|
if (row >= startRow && row < startRow + this.availableModes.length) {
|
|
391
418
|
const hoverIdx = row - startRow;
|
|
392
419
|
if (this.modeSelectedIndex !== hoverIdx) {
|
|
@@ -398,9 +425,10 @@ export class ChatView implements TuiView {
|
|
|
398
425
|
}
|
|
399
426
|
if (key.name === "click" && key.mouse) {
|
|
400
427
|
const { row } = key.mouse;
|
|
401
|
-
const startRow =
|
|
428
|
+
const startRow = 12;
|
|
402
429
|
if (row >= startRow && row < startRow + this.availableModes.length) {
|
|
403
|
-
|
|
430
|
+
const chosenIdx = row - startRow;
|
|
431
|
+
this.selectedChatMode = this.availableModes[chosenIdx].id;
|
|
404
432
|
this.isModeModalOpen = false;
|
|
405
433
|
setRuntimeChatMode(this.selectedChatMode);
|
|
406
434
|
saveTuiSettings({
|
|
@@ -490,6 +518,7 @@ export class ChatView implements TuiView {
|
|
|
490
518
|
) {
|
|
491
519
|
const col = key.mouse.col;
|
|
492
520
|
if (this.modelBtnStartCol > 0 && col >= this.modelBtnStartCol - 1 && col <= this.modelBtnEndCol + 1) {
|
|
521
|
+
void this.refreshModels();
|
|
493
522
|
this.isModelModalOpen = true;
|
|
494
523
|
this.modalSelectedIndex = this.selectedModelIndex;
|
|
495
524
|
this.hoveredHeaderBtn = null;
|
|
@@ -530,7 +559,7 @@ export class ChatView implements TuiView {
|
|
|
530
559
|
if (key.name === "f3") {
|
|
531
560
|
const currentM = this.availableModels[this.selectedModelIndex] || "qwen3.8-max";
|
|
532
561
|
const info = classifyModel(currentM);
|
|
533
|
-
if (info.
|
|
562
|
+
if (info.supportsReasoning) {
|
|
534
563
|
this.isEffortModalOpen = true;
|
|
535
564
|
const idx = this.availableEfforts.findIndex((e) => e.id === this.selectedEffort);
|
|
536
565
|
this.effortSelectedIndex = idx !== -1 ? idx : 0;
|
|
@@ -810,7 +839,7 @@ export class ChatView implements TuiView {
|
|
|
810
839
|
.map((m) => ({ role: m.role, content: m.content }));
|
|
811
840
|
|
|
812
841
|
try {
|
|
813
|
-
const isReasoning = classifyModel(model).
|
|
842
|
+
const isReasoning = classifyModel(model).supportsReasoning;
|
|
814
843
|
const result = await streamChatCompletions({
|
|
815
844
|
model,
|
|
816
845
|
reasoning_effort: isReasoning ? this.selectedEffort : undefined,
|
|
@@ -887,7 +916,7 @@ export class ChatView implements TuiView {
|
|
|
887
916
|
const currentModel = this.availableModels[this.selectedModelIndex] || "qwen3.8-max";
|
|
888
917
|
const currentInfo = classifyModel(currentModel);
|
|
889
918
|
const totalModels = this.availableModels.length;
|
|
890
|
-
const isReasoning = currentInfo.
|
|
919
|
+
const isReasoning = currentInfo.supportsReasoning;
|
|
891
920
|
|
|
892
921
|
const modelLabel = `[ ${currentModel} ]`;
|
|
893
922
|
const styledModel = this.hoveredHeaderBtn === "model"
|
|
@@ -1066,47 +1095,42 @@ export class ChatView implements TuiView {
|
|
|
1066
1095
|
}
|
|
1067
1096
|
}
|
|
1068
1097
|
for (const msg of this.messages) {
|
|
1069
|
-
chatContent.push("");
|
|
1070
1098
|
if (msg.role === "user") {
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1099
|
+
chatContent.push("");
|
|
1100
|
+
const cardW = Math.max(20, innerChatW - 4);
|
|
1101
|
+
const userLines = wrapContentLine(msg.content, cardW - 4);
|
|
1102
|
+
|
|
1103
|
+
// Top padding inside user card (gives height and breathability)
|
|
1104
|
+
chatContent.push(` ${theme.cyan("▌")}${theme.bgUserCard(" ".repeat(cardW))}`);
|
|
1105
|
+
|
|
1106
|
+
// Content lines with distinct lighter background
|
|
1107
|
+
for (const u of userLines) {
|
|
1108
|
+
chatContent.push(
|
|
1109
|
+
` ${theme.cyan("▌")}${theme.bgUserCard(" " + pad(theme.bold(theme.white(u)), cardW - 3))}`,
|
|
1110
|
+
);
|
|
1078
1111
|
}
|
|
1112
|
+
|
|
1113
|
+
// Bottom padding inside user card
|
|
1114
|
+
chatContent.push(` ${theme.cyan("▌")}${theme.bgUserCard(" ".repeat(cardW))}`);
|
|
1115
|
+
chatContent.push("");
|
|
1079
1116
|
} else {
|
|
1080
1117
|
const messageModel = msg.model || currentModel;
|
|
1081
|
-
|
|
1082
|
-
// 1.
|
|
1118
|
+
|
|
1119
|
+
// 1. OpenCode-style Thinking (Reasoning): Clean, indented, dimmed and unboxed
|
|
1083
1120
|
if (msg.reasoning && msg.reasoning.trim().length > 0) {
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1121
|
+
chatContent.push("");
|
|
1122
|
+
const isStillThinking = this.isGenerating && !msg.content && this.messages.indexOf(msg) === this.messages.length - 1;
|
|
1123
|
+
const spinner = this.spinnerFrames[this.spinnerIndex] || "⠋";
|
|
1124
|
+
|
|
1125
|
+
if (isStillThinking) {
|
|
1126
|
+
chatContent.push(` ${theme.yellow(`🧠 ${spinner} Raciocinando...`)}`);
|
|
1088
1127
|
} else {
|
|
1089
|
-
|
|
1090
|
-
if (this.isGenerating && !msg.content && this.messages.indexOf(msg) === this.messages.length - 1) {
|
|
1091
|
-
const spinner = this.spinnerFrames[this.spinnerIndex] || "⠋";
|
|
1092
|
-
rLines.push("");
|
|
1093
|
-
rLines.push(` ${theme.yellow(`${spinner} Raciocinando...`)}`);
|
|
1094
|
-
}
|
|
1095
|
-
thinkLines = drawBox({
|
|
1096
|
-
title: "🧠 Raciocínio",
|
|
1097
|
-
width: thinkWidth,
|
|
1098
|
-
borderColor: theme.borderInactive,
|
|
1099
|
-
titleColor: theme.muted,
|
|
1100
|
-
content: rLines,
|
|
1101
|
-
});
|
|
1102
|
-
if (!this.isGenerating) {
|
|
1103
|
-
msg.cachedReasoningBox = thinkLines;
|
|
1104
|
-
msg.cachedWidth = innerChatW;
|
|
1105
|
-
}
|
|
1128
|
+
chatContent.push(` ${theme.yellow("🧠 Raciocínio:")}`);
|
|
1106
1129
|
}
|
|
1107
1130
|
|
|
1108
|
-
|
|
1109
|
-
|
|
1131
|
+
const rLines = formatReasoning(msg.reasoning, innerChatW - 8);
|
|
1132
|
+
for (const r of rLines) {
|
|
1133
|
+
chatContent.push(` ${r}`);
|
|
1110
1134
|
}
|
|
1111
1135
|
chatContent.push("");
|
|
1112
1136
|
}
|
|
@@ -1131,11 +1155,18 @@ export class ChatView implements TuiView {
|
|
|
1131
1155
|
chatContent.push(` ${theme.yellow(`${spinner} Pensando...`)}`);
|
|
1132
1156
|
}
|
|
1133
1157
|
|
|
1134
|
-
|
|
1158
|
+
// 3. OpenCode-style execution badge with model and timing metadata
|
|
1159
|
+
const isDoneGenerating = !this.isGenerating || this.messages.indexOf(msg) !== this.messages.length - 1;
|
|
1160
|
+
if (isDoneGenerating && (msg.content || msg.reasoning)) {
|
|
1161
|
+
const timingStr = msg.totalTimeMs
|
|
1162
|
+
? ` ${theme.dim("·")} ${theme.dim(`${(msg.totalTimeMs / 1000).toFixed(2)}s`)}${msg.ttfbMs ? ` ${theme.dim(`(TTFB ${msg.ttfbMs}ms)`)}` : ""}`
|
|
1163
|
+
: "";
|
|
1164
|
+
chatContent.push("");
|
|
1135
1165
|
chatContent.push(
|
|
1136
|
-
` ${theme.
|
|
1166
|
+
` ${theme.cyan("▣")} ${theme.bold("Qwen")} ${theme.dim("·")} ${theme.cyan(messageModel)}${timingStr}`,
|
|
1137
1167
|
);
|
|
1138
1168
|
}
|
|
1169
|
+
chatContent.push("");
|
|
1139
1170
|
}
|
|
1140
1171
|
}
|
|
1141
1172
|
|
|
@@ -1237,13 +1268,18 @@ export class ChatView implements TuiView {
|
|
|
1237
1268
|
? `${spinner} Gerando... (Esc para cancelar)`
|
|
1238
1269
|
: actionLabel;
|
|
1239
1270
|
|
|
1271
|
+
const defaultFooter = `${currentModel} · ${isReasoning ? `Effort: ${this.selectedEffort}` : currentInfo.category} · Modo: ${this.selectedChatMode}`;
|
|
1272
|
+
const inputFooter = this.statusNote
|
|
1273
|
+
? stripAnsi(this.statusNote)
|
|
1274
|
+
: defaultFooter;
|
|
1275
|
+
|
|
1240
1276
|
const inputBox = drawBox({
|
|
1241
1277
|
title: inputTitle,
|
|
1242
1278
|
width,
|
|
1243
1279
|
height: 3,
|
|
1244
1280
|
borderColor: this.isGenerating ? theme.yellow : theme.borderActive,
|
|
1245
1281
|
titleColor: this.isGenerating ? theme.yellow : theme.cyan,
|
|
1246
|
-
footer:
|
|
1282
|
+
footer: inputFooter,
|
|
1247
1283
|
content: inputContent,
|
|
1248
1284
|
});
|
|
1249
1285
|
totalLines.push(...inputBox);
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { TuiView, ProxyStatusSnapshot } from "../types.ts";
|
|
6
6
|
import type { KeyEvent } from "../screen.ts";
|
|
7
|
-
import { theme, glyphs, drawBox, pad, truncate } from "../theme.ts";
|
|
7
|
+
import { theme, glyphs, drawBox, pad, truncate, setClipboardText } from "../theme.ts";
|
|
8
8
|
import { fetchProxyStatus, resetAllCooldowns, formatUptime } from "../proxy-client.ts";
|
|
9
9
|
import { ServerManager } from "../server-manager.ts";
|
|
10
10
|
import { getRuntimeChatMode, cycleNextChatMode } from "../../core/config.ts";
|
|
@@ -61,14 +61,21 @@ export class StatusView implements TuiView {
|
|
|
61
61
|
private actionMessage = "";
|
|
62
62
|
private actionMessageTimeout: NodeJS.Timeout | null = null;
|
|
63
63
|
private hoveredActionRow: number | null = null;
|
|
64
|
+
private isBaseUrlHovered = false;
|
|
65
|
+
private copiedRecently = false;
|
|
66
|
+
private copiedTimeout: NodeJS.Timeout | null = null;
|
|
67
|
+
private lastBaseUrl = "http://127.0.0.1:7936/v1";
|
|
68
|
+
private lastBaseUrlRow = 6;
|
|
69
|
+
private lastModoApiRow = 7;
|
|
64
70
|
private lastLeftW = 38;
|
|
65
71
|
private lastActionRecarregarRow = 20;
|
|
66
72
|
private lastActionZerarRow = 21;
|
|
67
73
|
private lastActionModoRow = 22;
|
|
74
|
+
private lastActionCopiarRow = 23;
|
|
75
|
+
|
|
68
76
|
constructor() {
|
|
69
77
|
this.refresh();
|
|
70
78
|
}
|
|
71
|
-
|
|
72
79
|
public async refresh(): Promise<void> {
|
|
73
80
|
try {
|
|
74
81
|
if (process.stdout.isTTY && !process.env.NODE_TEST_CONTEXT) {
|
|
@@ -90,6 +97,7 @@ export class StatusView implements TuiView {
|
|
|
90
97
|
{ key: "r", label: "Recarregar" },
|
|
91
98
|
{ key: "z", label: "Zerar Cooldowns" },
|
|
92
99
|
{ key: "m", label: "Alternar Modo" },
|
|
100
|
+
{ key: "c", label: "Copiar URL" },
|
|
93
101
|
];
|
|
94
102
|
}
|
|
95
103
|
|
|
@@ -102,25 +110,34 @@ export class StatusView implements TuiView {
|
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
public async handleKey(key: KeyEvent): Promise<boolean | void> {
|
|
105
|
-
// Mouse hover over quick actions
|
|
113
|
+
// Mouse hover over quick actions or Base URL
|
|
106
114
|
if (key.name === "hover" && key.mouse) {
|
|
107
115
|
const { row, col } = key.mouse;
|
|
108
116
|
const leftW = this.lastLeftW || 38;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
117
|
+
const isOverLeft = col >= 2 && col <= leftW - 1;
|
|
118
|
+
const isOverBaseUrl = isOverLeft && row === this.lastBaseUrlRow;
|
|
119
|
+
const isOverAction =
|
|
120
|
+
isOverLeft &&
|
|
112
121
|
(row === this.lastActionRecarregarRow ||
|
|
113
122
|
row === this.lastActionZerarRow ||
|
|
114
|
-
row === this.lastActionModoRow
|
|
115
|
-
|
|
123
|
+
row === this.lastActionModoRow ||
|
|
124
|
+
row === this.lastActionCopiarRow);
|
|
125
|
+
|
|
126
|
+
let changed = false;
|
|
127
|
+
if (isOverBaseUrl !== this.isBaseUrlHovered) {
|
|
128
|
+
this.isBaseUrlHovered = isOverBaseUrl;
|
|
129
|
+
changed = true;
|
|
130
|
+
}
|
|
131
|
+
if (isOverAction) {
|
|
116
132
|
if (this.hoveredActionRow !== row) {
|
|
117
133
|
this.hoveredActionRow = row;
|
|
118
|
-
|
|
134
|
+
changed = true;
|
|
119
135
|
}
|
|
120
136
|
} else if (this.hoveredActionRow !== null) {
|
|
121
137
|
this.hoveredActionRow = null;
|
|
122
|
-
|
|
138
|
+
changed = true;
|
|
123
139
|
}
|
|
140
|
+
if (changed) return true;
|
|
124
141
|
}
|
|
125
142
|
|
|
126
143
|
// Mouse click interactions
|
|
@@ -128,6 +145,17 @@ export class StatusView implements TuiView {
|
|
|
128
145
|
const { row, col } = key.mouse;
|
|
129
146
|
const leftW = this.lastLeftW || 38;
|
|
130
147
|
if (col >= 2 && col <= leftW - 1) {
|
|
148
|
+
if (row === this.lastBaseUrlRow || row === this.lastActionCopiarRow) {
|
|
149
|
+
setClipboardText(this.lastBaseUrl);
|
|
150
|
+
this.copiedRecently = true;
|
|
151
|
+
if (this.copiedTimeout) clearTimeout(this.copiedTimeout);
|
|
152
|
+
this.copiedTimeout = setTimeout(() => {
|
|
153
|
+
this.copiedRecently = false;
|
|
154
|
+
this.copiedTimeout = null;
|
|
155
|
+
}, 2500);
|
|
156
|
+
this.setMessage(theme.green(`✓ Base URL copiada: ${this.lastBaseUrl}`));
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
131
159
|
if (row === this.lastActionRecarregarRow) {
|
|
132
160
|
await this.refresh();
|
|
133
161
|
this.setMessage(theme.green("✓ Status atualizado"));
|
|
@@ -139,7 +167,7 @@ export class StatusView implements TuiView {
|
|
|
139
167
|
this.setMessage(theme.green(`✓ Cooldowns zerados: ${cleared} conta(s) liberada(s)`));
|
|
140
168
|
return true;
|
|
141
169
|
}
|
|
142
|
-
if (row === this.lastActionModoRow) {
|
|
170
|
+
if (row === this.lastActionModoRow || row === this.lastModoApiRow) {
|
|
143
171
|
const nextMode = cycleNextChatMode();
|
|
144
172
|
saveTuiSettings({ chat: { mode: nextMode } });
|
|
145
173
|
await this.refresh();
|
|
@@ -168,8 +196,19 @@ export class StatusView implements TuiView {
|
|
|
168
196
|
this.setMessage(theme.green(`✓ Modo global da API: ${nextMode}`));
|
|
169
197
|
return true;
|
|
170
198
|
}
|
|
171
|
-
}
|
|
172
199
|
|
|
200
|
+
if ((key.name === "c" || key.name === "C") && !key.ctrl && !key.meta) {
|
|
201
|
+
setClipboardText(this.lastBaseUrl);
|
|
202
|
+
this.copiedRecently = true;
|
|
203
|
+
if (this.copiedTimeout) clearTimeout(this.copiedTimeout);
|
|
204
|
+
this.copiedTimeout = setTimeout(() => {
|
|
205
|
+
this.copiedRecently = false;
|
|
206
|
+
this.copiedTimeout = null;
|
|
207
|
+
}, 2500);
|
|
208
|
+
this.setMessage(theme.green(`✓ Base URL copiada: ${this.lastBaseUrl}`));
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
173
212
|
public render(width: number, height: number, snapshot?: ProxyStatusSnapshot | null): string[] {
|
|
174
213
|
const data = snapshot || this.statusData;
|
|
175
214
|
const isOnline = data?.online ?? false;
|
|
@@ -197,7 +236,9 @@ export class StatusView implements TuiView {
|
|
|
197
236
|
const uptimeSecs = data?.uptimeSeconds || Math.floor(process.uptime());
|
|
198
237
|
const uptimeStr = formatUptime(uptimeSecs);
|
|
199
238
|
const baseUrl = `http://${data?.host || "127.0.0.1"}:${data?.port || 7936}/v1`;
|
|
200
|
-
|
|
239
|
+
this.lastBaseUrl = baseUrl;
|
|
240
|
+
this.lastBaseUrlRow = 6;
|
|
241
|
+
this.lastModoApiRow = 7;
|
|
201
242
|
const m = data?.metrics;
|
|
202
243
|
const reqsTotal = m?.requestsTotal ?? 0;
|
|
203
244
|
const reqsErrors = m?.requestsErrors ?? 0;
|
|
@@ -230,10 +271,18 @@ export class StatusView implements TuiView {
|
|
|
230
271
|
if (data?.waitingStreams && data.waitingStreams > 0) {
|
|
231
272
|
connsStr += theme.peach(` (${data.waitingStreams} na fila)`);
|
|
232
273
|
}
|
|
274
|
+
let urlDisplay: string;
|
|
275
|
+
if (this.copiedRecently) {
|
|
276
|
+
urlDisplay = theme.bold(theme.green(baseUrl));
|
|
277
|
+
} else if (this.isBaseUrlHovered) {
|
|
278
|
+
urlDisplay = theme.bold(theme.underline(theme.cyan(baseUrl)));
|
|
279
|
+
} else {
|
|
280
|
+
urlDisplay = theme.cyan(baseUrl);
|
|
281
|
+
}
|
|
233
282
|
|
|
234
283
|
const leftContent: string[] = [
|
|
235
284
|
` ${theme.bold(lbl("Status:"))} ${onlineBadge}`,
|
|
236
|
-
` ${theme.bold(lbl("Base URL:"))} ${
|
|
285
|
+
` ${theme.bold(lbl("Base URL:"))} ${urlDisplay}`,
|
|
237
286
|
` ${theme.bold(lbl("Modo API:"))} ${
|
|
238
287
|
getRuntimeChatMode() === "thread"
|
|
239
288
|
? theme.cyan("[thread]")
|
|
@@ -261,15 +310,19 @@ export class StatusView implements TuiView {
|
|
|
261
310
|
|
|
262
311
|
const recarregarIdx = leftContent.length;
|
|
263
312
|
const zerarIdx = leftContent.length + 1;
|
|
313
|
+
const modoIdx = leftContent.length + 2;
|
|
314
|
+
const copiarIdx = leftContent.length + 3;
|
|
315
|
+
|
|
264
316
|
this.lastActionRecarregarRow = 5 + recarregarIdx;
|
|
265
317
|
this.lastActionZerarRow = 5 + zerarIdx;
|
|
266
|
-
const modoIdx = leftContent.length + 2;
|
|
267
318
|
this.lastActionModoRow = 5 + modoIdx;
|
|
319
|
+
this.lastActionCopiarRow = 5 + copiarIdx;
|
|
268
320
|
|
|
269
321
|
leftContent.push(
|
|
270
322
|
` ${this.hoveredActionRow === this.lastActionRecarregarRow ? theme.bgHover(` ${theme.cyan("[ R ] Recarregar")} `) : ` ${theme.cyan("[ R ]")} Recarregar`}`,
|
|
271
323
|
` ${this.hoveredActionRow === this.lastActionZerarRow ? theme.bgHover(` ${theme.yellow("[ Z ] Zerar Cooldowns")} `) : ` ${theme.yellow("[ Z ]")} Zerar Cooldowns`}`,
|
|
272
324
|
` ${this.hoveredActionRow === this.lastActionModoRow ? theme.bgHover(` ${theme.lavender("[ M ] Alternar Modo")} `) : ` ${theme.lavender("[ M ]")} Alternar Modo`}`,
|
|
325
|
+
` ${this.hoveredActionRow === this.lastActionCopiarRow ? theme.bgHover(` ${theme.green("[ C ] Copiar URL")} `) : ` ${theme.green("[ C ]")} Copiar URL`}`,
|
|
273
326
|
);
|
|
274
327
|
|
|
275
328
|
const boxHeight = Math.max(contentH, leftContent.length + 2);
|