qwenproxy-cli 1.0.32 → 1.2.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.es.md +267 -0
- package/README.md +174 -783
- package/README.pt-BR.md +1031 -0
- package/bin/qwenproxy.js +7 -3
- package/bin/update.d.ts +11 -0
- package/bin/update.js +155 -0
- package/package.json +4 -4
- package/src/api/models.ts +18 -2
- package/src/api/server.ts +18 -2
- package/src/core/accounts.ts +130 -0
- package/src/core/config.ts +96 -5
- package/src/core/model-alias.ts +17 -0
- package/src/index.ts +3 -0
- package/src/reset-cooldowns.ts +5 -1
- package/src/routes/chat/context.ts +9 -9
- package/src/routes/chat/index.ts +15 -6
- package/src/services/qwen-chat-pool.ts +4 -5
- package/src/services/qwen.ts +60 -27
- 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/tui/app.ts +27 -10
- package/src/tui/index.ts +3 -0
- package/src/tui/proxy-client.ts +20 -16
- package/src/tui/screen.ts +10 -3
- package/src/tui/settings.ts +2 -0
- package/src/tui/theme.ts +2 -0
- package/src/tui/types.ts +1 -0
- package/src/tui/views/accounts-view.ts +343 -23
- package/src/tui/views/chat-view.ts +302 -71
- package/src/tui/views/status-view.ts +99 -15
- package/src/tui/views/sync-view.ts +87 -32
- package/src/update-cli.ts +10 -154
|
@@ -5,10 +5,15 @@
|
|
|
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";
|
|
16
|
+
import { setRuntimeChatMode, getRuntimeChatMode } from "../../core/config.ts";
|
|
12
17
|
|
|
13
18
|
interface ChatMessage {
|
|
14
19
|
role: "user" | "assistant";
|
|
@@ -21,7 +26,11 @@ interface ChatMessage {
|
|
|
21
26
|
cachedReasoningBox?: string[];
|
|
22
27
|
cachedWidth?: number;
|
|
23
28
|
}
|
|
24
|
-
export function classifyModel(modelId: string): {
|
|
29
|
+
export function classifyModel(modelId: string): {
|
|
30
|
+
badge: string;
|
|
31
|
+
category: string;
|
|
32
|
+
supportsReasoning: boolean;
|
|
33
|
+
} {
|
|
25
34
|
const lower = modelId.toLowerCase();
|
|
26
35
|
if (
|
|
27
36
|
lower.includes("image") ||
|
|
@@ -29,12 +38,35 @@ export function classifyModel(modelId: string): { badge: string; category: strin
|
|
|
29
38
|
lower.includes("t2i") ||
|
|
30
39
|
lower.includes("i2i")
|
|
31
40
|
) {
|
|
32
|
-
return {
|
|
41
|
+
return {
|
|
42
|
+
badge: theme.lavender("[Imagem]"),
|
|
43
|
+
category: "Geração de Imagem",
|
|
44
|
+
supportsReasoning: false,
|
|
45
|
+
};
|
|
33
46
|
}
|
|
34
47
|
if (lower.includes("video") || lower.includes("t2v") || lower.includes("i2v")) {
|
|
35
|
-
return {
|
|
48
|
+
return {
|
|
49
|
+
badge: theme.peach("[Vídeo] "),
|
|
50
|
+
category: "Geração de Vídeo",
|
|
51
|
+
supportsReasoning: false,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
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
|
+
};
|
|
36
64
|
}
|
|
37
|
-
return {
|
|
65
|
+
return {
|
|
66
|
+
badge: theme.cyan("[Texto] "),
|
|
67
|
+
category: "Texto & Raciocínio",
|
|
68
|
+
supportsReasoning: true,
|
|
69
|
+
};
|
|
38
70
|
}
|
|
39
71
|
|
|
40
72
|
export class ChatView implements TuiView {
|
|
@@ -42,14 +74,7 @@ export class ChatView implements TuiView {
|
|
|
42
74
|
public readonly title = "Chat";
|
|
43
75
|
public readonly tabNumber = 2;
|
|
44
76
|
|
|
45
|
-
private availableModels = [
|
|
46
|
-
"qwen3.8-max",
|
|
47
|
-
"qwen3.7-plus",
|
|
48
|
-
"qwen3.7-max",
|
|
49
|
-
"z-image-turbo",
|
|
50
|
-
"qwen-image-3.0-pro",
|
|
51
|
-
"wan3.0-video",
|
|
52
|
-
];
|
|
77
|
+
private availableModels = [...DEFAULT_FALLBACK_MODELS];
|
|
53
78
|
private selectedModelIndex = 0;
|
|
54
79
|
private messages: ChatMessage[] = [];
|
|
55
80
|
private inputBuffer = "";
|
|
@@ -59,13 +84,15 @@ export class ChatView implements TuiView {
|
|
|
59
84
|
private lastHeight = 24;
|
|
60
85
|
private lastMaxOffset = 0;
|
|
61
86
|
private lastVisibleCapacity = 0;
|
|
62
|
-
private hoveredHeaderBtn: "model" | "effort" | null = null;
|
|
87
|
+
private hoveredHeaderBtn: "model" | "effort" | "mode" | null = null;
|
|
63
88
|
private isScrollbarHovered = false;
|
|
64
89
|
private isDraggingScrollbar = false;
|
|
65
90
|
private modelBtnStartCol = 0;
|
|
66
91
|
private modelBtnEndCol = 0;
|
|
67
92
|
private effortBtnStartCol = 0;
|
|
68
93
|
private effortBtnEndCol = 0;
|
|
94
|
+
private modeBtnStartCol = 0;
|
|
95
|
+
private modeBtnEndCol = 0;
|
|
69
96
|
private isModelModalOpen = false;
|
|
70
97
|
private modalSelectedIndex = 0;
|
|
71
98
|
private availableEfforts: Array<{
|
|
@@ -96,6 +123,40 @@ export class ChatView implements TuiView {
|
|
|
96
123
|
private selectedEffort: "high" | "medium" | "low" = "high";
|
|
97
124
|
private isEffortModalOpen = false;
|
|
98
125
|
private effortSelectedIndex = 0;
|
|
126
|
+
private availableModes: Array<{
|
|
127
|
+
id: "thread" | "thread-temp" | "stateless" | "stateless-temp";
|
|
128
|
+
label: string;
|
|
129
|
+
desc: string;
|
|
130
|
+
badge: string;
|
|
131
|
+
}> = [
|
|
132
|
+
{
|
|
133
|
+
id: "thread",
|
|
134
|
+
label: "thread (Padrão)",
|
|
135
|
+
desc: "Persistente Web (Delta ~1KB, salva na conta Qwen)",
|
|
136
|
+
badge: theme.cyan("[thread]"),
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "thread-temp",
|
|
140
|
+
label: "thread-temp",
|
|
141
|
+
desc: "Efêmero Rápido (Delta ~1KB, não salva no site)",
|
|
142
|
+
badge: theme.green("[thread-temp]"),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "stateless-temp",
|
|
146
|
+
label: "stateless-temp",
|
|
147
|
+
desc: "Oficial OpenAI Efêmero (Histórico total, não salva)",
|
|
148
|
+
badge: theme.yellow("[stateless-temp]"),
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "stateless",
|
|
152
|
+
label: "stateless",
|
|
153
|
+
desc: "Oficial OpenAI Salvo (Histórico total, salva no site)",
|
|
154
|
+
badge: theme.lavender("[stateless]"),
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
private selectedChatMode: "thread" | "thread-temp" | "stateless" | "stateless-temp" = "thread";
|
|
158
|
+
private isModeModalOpen = false;
|
|
159
|
+
private modeSelectedIndex = 0;
|
|
99
160
|
private isGenerating = false;
|
|
100
161
|
private currentAbortController: AbortController | null = null;
|
|
101
162
|
private statusNote = "";
|
|
@@ -138,14 +199,26 @@ export class ChatView implements TuiView {
|
|
|
138
199
|
this.effortSelectedIndex = effIdx;
|
|
139
200
|
}
|
|
140
201
|
}
|
|
202
|
+
const runtimeMode = getRuntimeChatMode();
|
|
203
|
+
this.selectedChatMode = runtimeMode;
|
|
204
|
+
const mIdx = this.availableModes.findIndex((m) => m.id === runtimeMode);
|
|
205
|
+
if (mIdx !== -1) {
|
|
206
|
+
this.modeSelectedIndex = mIdx;
|
|
207
|
+
}
|
|
141
208
|
void this.refreshModels();
|
|
142
209
|
}
|
|
143
210
|
public onActivate(): void {
|
|
211
|
+
const runtimeMode = getRuntimeChatMode();
|
|
212
|
+
this.selectedChatMode = runtimeMode;
|
|
213
|
+
const mIdx = this.availableModes.findIndex((m) => m.id === runtimeMode);
|
|
214
|
+
if (mIdx !== -1) {
|
|
215
|
+
this.modeSelectedIndex = mIdx;
|
|
216
|
+
}
|
|
144
217
|
void this.refreshModels();
|
|
145
218
|
}
|
|
146
219
|
|
|
147
220
|
public isModalOpen(): boolean {
|
|
148
|
-
return this.isModelModalOpen || this.isEffortModalOpen;
|
|
221
|
+
return this.isModelModalOpen || this.isEffortModalOpen || this.isModeModalOpen;
|
|
149
222
|
}
|
|
150
223
|
|
|
151
224
|
public async refreshModels(): Promise<void> {
|
|
@@ -179,6 +252,12 @@ export class ChatView implements TuiView {
|
|
|
179
252
|
{ key: "Esc", label: `${glyphs.cross} Manter` },
|
|
180
253
|
];
|
|
181
254
|
}
|
|
255
|
+
if (this.isModeModalOpen) {
|
|
256
|
+
return [
|
|
257
|
+
{ key: "Enter", label: `${glyphs.enter} Confirmar` },
|
|
258
|
+
{ key: "Esc", label: `${glyphs.cross} Manter` },
|
|
259
|
+
];
|
|
260
|
+
}
|
|
182
261
|
return [
|
|
183
262
|
{ key: "Enter", label: `${glyphs.enter} Enviar` },
|
|
184
263
|
{ key: "Esc", label: `${glyphs.cross} Parar` },
|
|
@@ -195,10 +274,11 @@ export class ChatView implements TuiView {
|
|
|
195
274
|
chat: {
|
|
196
275
|
model: chosen,
|
|
197
276
|
effort: this.selectedEffort,
|
|
277
|
+
mode: this.selectedChatMode,
|
|
198
278
|
},
|
|
199
279
|
});
|
|
200
280
|
const info = classifyModel(chosen);
|
|
201
|
-
if (info.
|
|
281
|
+
if (info.supportsReasoning) {
|
|
202
282
|
this.isEffortModalOpen = true;
|
|
203
283
|
const effIdx = this.availableEfforts.findIndex((e) => e.id === this.selectedEffort);
|
|
204
284
|
this.effortSelectedIndex = effIdx !== -1 ? effIdx : 0;
|
|
@@ -261,8 +341,9 @@ export class ChatView implements TuiView {
|
|
|
261
341
|
if (this.isEffortModalOpen) {
|
|
262
342
|
if (key.name === "hover" && key.mouse) {
|
|
263
343
|
const { row } = key.mouse;
|
|
264
|
-
|
|
265
|
-
|
|
344
|
+
const startRow = 12;
|
|
345
|
+
if (row >= startRow && row < startRow + this.availableEfforts.length) {
|
|
346
|
+
const hoverIdx = row - startRow;
|
|
266
347
|
if (this.effortSelectedIndex !== hoverIdx) {
|
|
267
348
|
this.effortSelectedIndex = hoverIdx;
|
|
268
349
|
this.onNeedsRender?.();
|
|
@@ -272,8 +353,10 @@ export class ChatView implements TuiView {
|
|
|
272
353
|
}
|
|
273
354
|
if (key.name === "click" && key.mouse) {
|
|
274
355
|
const { row } = key.mouse;
|
|
275
|
-
|
|
276
|
-
|
|
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;
|
|
277
360
|
this.isEffortModalOpen = false;
|
|
278
361
|
const currentM = this.availableModels[this.selectedModelIndex];
|
|
279
362
|
saveTuiSettings({
|
|
@@ -282,7 +365,7 @@ export class ChatView implements TuiView {
|
|
|
282
365
|
effort: this.selectedEffort,
|
|
283
366
|
},
|
|
284
367
|
});
|
|
285
|
-
this.statusNote = `Modelo: ${currentM} | Effort: ${this.availableEfforts[
|
|
368
|
+
this.statusNote = `Modelo: ${currentM} | Effort: ${this.availableEfforts[chosenIdx].label}`;
|
|
286
369
|
this.onNeedsRender?.();
|
|
287
370
|
return true;
|
|
288
371
|
}
|
|
@@ -311,6 +394,7 @@ export class ChatView implements TuiView {
|
|
|
311
394
|
chat: {
|
|
312
395
|
model: currentM,
|
|
313
396
|
effort: this.selectedEffort,
|
|
397
|
+
mode: this.selectedChatMode,
|
|
314
398
|
},
|
|
315
399
|
});
|
|
316
400
|
this.statusNote = `Modelo: ${currentM} | Effort: ${this.availableEfforts[this.effortSelectedIndex].label}`;
|
|
@@ -325,15 +409,90 @@ export class ChatView implements TuiView {
|
|
|
325
409
|
return true;
|
|
326
410
|
}
|
|
327
411
|
|
|
328
|
-
// 3.
|
|
412
|
+
// 3. Chat Mode Selection Modal Active
|
|
413
|
+
if (this.isModeModalOpen) {
|
|
414
|
+
if (key.name === "hover" && key.mouse) {
|
|
415
|
+
const { row } = key.mouse;
|
|
416
|
+
const startRow = 12;
|
|
417
|
+
if (row >= startRow && row < startRow + this.availableModes.length) {
|
|
418
|
+
const hoverIdx = row - startRow;
|
|
419
|
+
if (this.modeSelectedIndex !== hoverIdx) {
|
|
420
|
+
this.modeSelectedIndex = hoverIdx;
|
|
421
|
+
this.onNeedsRender?.();
|
|
422
|
+
return true;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (key.name === "click" && key.mouse) {
|
|
427
|
+
const { row } = key.mouse;
|
|
428
|
+
const startRow = 12;
|
|
429
|
+
if (row >= startRow && row < startRow + this.availableModes.length) {
|
|
430
|
+
const chosenIdx = row - startRow;
|
|
431
|
+
this.selectedChatMode = this.availableModes[chosenIdx].id;
|
|
432
|
+
this.isModeModalOpen = false;
|
|
433
|
+
setRuntimeChatMode(this.selectedChatMode);
|
|
434
|
+
saveTuiSettings({
|
|
435
|
+
chat: {
|
|
436
|
+
model: this.availableModels[this.selectedModelIndex],
|
|
437
|
+
effort: this.selectedEffort,
|
|
438
|
+
mode: this.selectedChatMode,
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
this.statusNote = theme.green(`✓ Modo global da API alterado para: ${this.selectedChatMode}`);
|
|
442
|
+
this.onNeedsRender?.();
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
this.isModeModalOpen = false;
|
|
446
|
+
this.onNeedsRender?.();
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
if (key.name === "up" || key.name === "wheelup" || (key.name === "k" && !key.ctrl)) {
|
|
450
|
+
this.modeSelectedIndex = Math.max(0, this.modeSelectedIndex - 1);
|
|
451
|
+
this.onNeedsRender?.();
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
if (key.name === "down" || key.name === "wheeldown" || (key.name === "j" && !key.ctrl)) {
|
|
455
|
+
this.modeSelectedIndex = Math.min(
|
|
456
|
+
this.availableModes.length - 1,
|
|
457
|
+
this.modeSelectedIndex + 1,
|
|
458
|
+
);
|
|
459
|
+
this.onNeedsRender?.();
|
|
460
|
+
return true;
|
|
461
|
+
}
|
|
462
|
+
if (key.name === "return") {
|
|
463
|
+
this.selectedChatMode = this.availableModes[this.modeSelectedIndex].id;
|
|
464
|
+
this.isModeModalOpen = false;
|
|
465
|
+
setRuntimeChatMode(this.selectedChatMode);
|
|
466
|
+
saveTuiSettings({
|
|
467
|
+
chat: {
|
|
468
|
+
model: this.availableModels[this.selectedModelIndex],
|
|
469
|
+
effort: this.selectedEffort,
|
|
470
|
+
mode: this.selectedChatMode,
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
this.statusNote = theme.green(`✓ Modo global da API alterado para: ${this.selectedChatMode}`);
|
|
474
|
+
this.onNeedsRender?.();
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
if (key.name === "escape") {
|
|
478
|
+
this.isModeModalOpen = false;
|
|
479
|
+
this.onNeedsRender?.();
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// 4. Header button hover (rows 4 to 6: Model, Effort, Mode)
|
|
329
486
|
if (key.name === "hover" && key.mouse) {
|
|
330
487
|
const { row, col } = key.mouse;
|
|
331
|
-
if (row >= 4 && row <= 6 && !this.isModelModalOpen && !this.isEffortModalOpen) {
|
|
332
|
-
let target: "model" | "effort" | null = null;
|
|
488
|
+
if (row >= 4 && row <= 6 && !this.isModelModalOpen && !this.isEffortModalOpen && !this.isModeModalOpen) {
|
|
489
|
+
let target: "model" | "effort" | "mode" | null = null;
|
|
333
490
|
if (this.modelBtnStartCol > 0 && col >= this.modelBtnStartCol - 1 && col <= this.modelBtnEndCol + 1) {
|
|
334
491
|
target = "model";
|
|
335
492
|
} else if (this.effortBtnStartCol > 0 && col >= this.effortBtnStartCol - 1 && col <= this.effortBtnEndCol + 1) {
|
|
336
493
|
target = "effort";
|
|
494
|
+
} else if (this.modeBtnStartCol > 0 && col >= this.modeBtnStartCol - 1 && col <= this.modeBtnEndCol + 1) {
|
|
495
|
+
target = "mode";
|
|
337
496
|
}
|
|
338
497
|
if (this.hoveredHeaderBtn !== target) {
|
|
339
498
|
this.hoveredHeaderBtn = target;
|
|
@@ -347,17 +506,19 @@ export class ChatView implements TuiView {
|
|
|
347
506
|
}
|
|
348
507
|
}
|
|
349
508
|
|
|
350
|
-
//
|
|
509
|
+
// 5. Header button click (rows 4 to 6: Model, Effort, Mode)
|
|
351
510
|
if (
|
|
352
511
|
key.name === "click" &&
|
|
353
512
|
key.mouse &&
|
|
354
513
|
key.mouse.row >= 4 &&
|
|
355
514
|
key.mouse.row <= 6 &&
|
|
356
515
|
!this.isModelModalOpen &&
|
|
357
|
-
!this.isEffortModalOpen
|
|
516
|
+
!this.isEffortModalOpen &&
|
|
517
|
+
!this.isModeModalOpen
|
|
358
518
|
) {
|
|
359
519
|
const col = key.mouse.col;
|
|
360
520
|
if (this.modelBtnStartCol > 0 && col >= this.modelBtnStartCol - 1 && col <= this.modelBtnEndCol + 1) {
|
|
521
|
+
void this.refreshModels();
|
|
361
522
|
this.isModelModalOpen = true;
|
|
362
523
|
this.modalSelectedIndex = this.selectedModelIndex;
|
|
363
524
|
this.hoveredHeaderBtn = null;
|
|
@@ -372,6 +533,14 @@ export class ChatView implements TuiView {
|
|
|
372
533
|
this.onNeedsRender?.();
|
|
373
534
|
return true;
|
|
374
535
|
}
|
|
536
|
+
if (this.modeBtnStartCol > 0 && col >= this.modeBtnStartCol - 1 && col <= this.modeBtnEndCol + 1) {
|
|
537
|
+
this.isModeModalOpen = true;
|
|
538
|
+
const idx = this.availableModes.findIndex((m) => m.id === this.selectedChatMode);
|
|
539
|
+
this.modeSelectedIndex = idx !== -1 ? idx : 0;
|
|
540
|
+
this.hoveredHeaderBtn = null;
|
|
541
|
+
this.onNeedsRender?.();
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
375
544
|
}
|
|
376
545
|
|
|
377
546
|
// Keyboard shortcuts to open modals
|
|
@@ -390,7 +559,7 @@ export class ChatView implements TuiView {
|
|
|
390
559
|
if (key.name === "f3") {
|
|
391
560
|
const currentM = this.availableModels[this.selectedModelIndex] || "qwen3.8-max";
|
|
392
561
|
const info = classifyModel(currentM);
|
|
393
|
-
if (info.
|
|
562
|
+
if (info.supportsReasoning) {
|
|
394
563
|
this.isEffortModalOpen = true;
|
|
395
564
|
const idx = this.availableEfforts.findIndex((e) => e.id === this.selectedEffort);
|
|
396
565
|
this.effortSelectedIndex = idx !== -1 ? idx : 0;
|
|
@@ -399,6 +568,14 @@ export class ChatView implements TuiView {
|
|
|
399
568
|
}
|
|
400
569
|
}
|
|
401
570
|
|
|
571
|
+
if (key.name === "f4") {
|
|
572
|
+
this.isModeModalOpen = true;
|
|
573
|
+
const idx = this.availableModes.findIndex((m) => m.id === this.selectedChatMode);
|
|
574
|
+
this.modeSelectedIndex = idx !== -1 ? idx : 0;
|
|
575
|
+
this.onNeedsRender?.();
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
|
|
402
579
|
// 5. Scrollbar hover, click & drag
|
|
403
580
|
const isMouseOnScrollbar = (col: number, row: number) => {
|
|
404
581
|
return (
|
|
@@ -662,10 +839,11 @@ export class ChatView implements TuiView {
|
|
|
662
839
|
.map((m) => ({ role: m.role, content: m.content }));
|
|
663
840
|
|
|
664
841
|
try {
|
|
665
|
-
const isReasoning = classifyModel(model).
|
|
842
|
+
const isReasoning = classifyModel(model).supportsReasoning;
|
|
666
843
|
const result = await streamChatCompletions({
|
|
667
844
|
model,
|
|
668
845
|
reasoning_effort: isReasoning ? this.selectedEffort : undefined,
|
|
846
|
+
chatMode: this.selectedChatMode,
|
|
669
847
|
messages: conversationPayload,
|
|
670
848
|
signal: this.currentAbortController.signal,
|
|
671
849
|
onReasoning: (chunk) => {
|
|
@@ -738,7 +916,7 @@ export class ChatView implements TuiView {
|
|
|
738
916
|
const currentModel = this.availableModels[this.selectedModelIndex] || "qwen3.8-max";
|
|
739
917
|
const currentInfo = classifyModel(currentModel);
|
|
740
918
|
const totalModels = this.availableModels.length;
|
|
741
|
-
const isReasoning = currentInfo.
|
|
919
|
+
const isReasoning = currentInfo.supportsReasoning;
|
|
742
920
|
|
|
743
921
|
const modelLabel = `[ ${currentModel} ]`;
|
|
744
922
|
const styledModel = this.hoveredHeaderBtn === "model"
|
|
@@ -747,10 +925,10 @@ export class ChatView implements TuiView {
|
|
|
747
925
|
|
|
748
926
|
const effortLabel = isReasoning
|
|
749
927
|
? this.selectedEffort === "high"
|
|
750
|
-
? "[ Effort: High
|
|
928
|
+
? "[ Effort: High ]"
|
|
751
929
|
: this.selectedEffort === "medium"
|
|
752
|
-
? "[ Effort:
|
|
753
|
-
: "[ Effort: Low
|
|
930
|
+
? "[ Effort: Med ]"
|
|
931
|
+
: "[ Effort: Low ]"
|
|
754
932
|
: "";
|
|
755
933
|
|
|
756
934
|
let styledEffort = "";
|
|
@@ -764,7 +942,18 @@ export class ChatView implements TuiView {
|
|
|
764
942
|
: theme.cyan(effortLabel);
|
|
765
943
|
}
|
|
766
944
|
|
|
767
|
-
const
|
|
945
|
+
const modeLabel = `[ Modo: ${this.selectedChatMode} ]`;
|
|
946
|
+
const styledMode = this.hoveredHeaderBtn === "mode"
|
|
947
|
+
? theme.bgHover(` ${theme.bold(theme.white(modeLabel))} `)
|
|
948
|
+
: this.selectedChatMode === "thread"
|
|
949
|
+
? theme.cyan(modeLabel)
|
|
950
|
+
: this.selectedChatMode === "thread-temp"
|
|
951
|
+
? theme.green(modeLabel)
|
|
952
|
+
: this.selectedChatMode === "stateless-temp"
|
|
953
|
+
? theme.yellow(modeLabel)
|
|
954
|
+
: theme.lavender(modeLabel);
|
|
955
|
+
|
|
956
|
+
const shortcutsLabel = `[ F2: Modelo${isReasoning ? " | F3: Effort" : ""} | F4: Modo ]`;
|
|
768
957
|
const styledShortcuts = theme.yellow(shortcutsLabel);
|
|
769
958
|
|
|
770
959
|
// Non-text models show their category badge ([Imagem] / [Vídeo]), while text models omit [Texto]
|
|
@@ -772,7 +961,7 @@ export class ChatView implements TuiView {
|
|
|
772
961
|
const nonTextCategory = !isReasoning ? theme.muted(`• ${currentInfo.category}`) : "";
|
|
773
962
|
|
|
774
963
|
const headerLine = hasAccounts
|
|
775
|
-
? ` ${theme.bold("Modelo:")} ${styledModel} ${nonTextBadge}${isReasoning ? styledEffort : nonTextCategory} ${styledShortcuts}`
|
|
964
|
+
? ` ${theme.bold("Modelo:")} ${styledModel} ${nonTextBadge}${isReasoning ? styledEffort + " " : nonTextCategory + " "}${styledMode} ${styledShortcuts}`
|
|
776
965
|
: ` ${theme.bold("Modelo:")} ${styledModel} ${theme.yellow("[ [!] Sem Contas: Adicione em [5] Contas ]")}`;
|
|
777
966
|
|
|
778
967
|
// Compute dynamic interactive column bounds:
|
|
@@ -781,16 +970,25 @@ export class ChatView implements TuiView {
|
|
|
781
970
|
this.modelBtnStartCol = modelStart;
|
|
782
971
|
this.modelBtnEndCol = modelEnd;
|
|
783
972
|
|
|
973
|
+
let nextStart = modelEnd + 3;
|
|
784
974
|
if (isReasoning) {
|
|
785
|
-
const effortStart =
|
|
975
|
+
const effortStart = nextStart;
|
|
786
976
|
const effortEnd = effortStart + stringWidth(effortLabel) - 1;
|
|
787
977
|
this.effortBtnStartCol = effortStart;
|
|
788
978
|
this.effortBtnEndCol = effortEnd;
|
|
979
|
+
nextStart = effortEnd + 3;
|
|
789
980
|
} else {
|
|
790
981
|
this.effortBtnStartCol = 0;
|
|
791
982
|
this.effortBtnEndCol = 0;
|
|
983
|
+
if (nonTextCategory) {
|
|
984
|
+
nextStart = modelEnd + stringWidth(` ${nonTextBadge}${nonTextCategory} `);
|
|
985
|
+
}
|
|
792
986
|
}
|
|
793
987
|
|
|
988
|
+
const modeStart = nextStart;
|
|
989
|
+
const modeEnd = modeStart + stringWidth(modeLabel) - 1;
|
|
990
|
+
this.modeBtnStartCol = modeStart;
|
|
991
|
+
this.modeBtnEndCol = modeEnd;
|
|
794
992
|
const headerBox = drawBox({
|
|
795
993
|
title: "Chat Tester",
|
|
796
994
|
width,
|
|
@@ -853,6 +1051,33 @@ export class ChatView implements TuiView {
|
|
|
853
1051
|
content: modalLines,
|
|
854
1052
|
});
|
|
855
1053
|
totalLines.push(...modalBox);
|
|
1054
|
+
} else if (this.isModeModalOpen) {
|
|
1055
|
+
const modalLines: string[] = [
|
|
1056
|
+
"",
|
|
1057
|
+
` ${theme.bold("Modo de Conversa:")} ${theme.cyan(this.selectedChatMode)}`,
|
|
1058
|
+
` ${theme.dim("Escolha como o histórico é transmitido e persistido no Qwen:")}`,
|
|
1059
|
+
"",
|
|
1060
|
+
];
|
|
1061
|
+
for (let i = 0; i < this.availableModes.length; i++) {
|
|
1062
|
+
const m = this.availableModes[i];
|
|
1063
|
+
const isSel = i === this.modeSelectedIndex;
|
|
1064
|
+
const isCurrent = m.id === this.selectedChatMode;
|
|
1065
|
+
const pointer = isSel ? theme.cyan("▸ ") : " ";
|
|
1066
|
+
const radio = isCurrent ? theme.green(glyphs.radioOn) : theme.muted(glyphs.radioOff);
|
|
1067
|
+
const line = `${pointer}${radio} ${pad(m.badge, 17)} ${pad(m.label, 17)} • ${m.desc}`;
|
|
1068
|
+
modalLines.push(isSel ? theme.bgSelected(line) : line);
|
|
1069
|
+
}
|
|
1070
|
+
modalLines.push("");
|
|
1071
|
+
|
|
1072
|
+
const modalBox = drawBox({
|
|
1073
|
+
title: "Selecionar Modo de Conversa [ Enter: Confirmar • Esc: Manter ]",
|
|
1074
|
+
width,
|
|
1075
|
+
height: chatHeight,
|
|
1076
|
+
borderColor: theme.borderActive,
|
|
1077
|
+
titleColor: theme.cyan,
|
|
1078
|
+
content: modalLines,
|
|
1079
|
+
});
|
|
1080
|
+
totalLines.push(...modalBox);
|
|
856
1081
|
} else {
|
|
857
1082
|
const chatContent: string[] = [];
|
|
858
1083
|
|
|
@@ -870,48 +1095,42 @@ export class ChatView implements TuiView {
|
|
|
870
1095
|
}
|
|
871
1096
|
}
|
|
872
1097
|
for (const msg of this.messages) {
|
|
873
|
-
chatContent.push("");
|
|
874
1098
|
if (msg.role === "user") {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
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
|
+
);
|
|
882
1111
|
}
|
|
1112
|
+
|
|
1113
|
+
// Bottom padding inside user card
|
|
1114
|
+
chatContent.push(` ${theme.cyan("▌")}${theme.bgUserCard(" ".repeat(cardW))}`);
|
|
1115
|
+
chatContent.push("");
|
|
883
1116
|
} else {
|
|
884
1117
|
const messageModel = msg.model || currentModel;
|
|
885
|
-
|
|
886
|
-
// 1.
|
|
1118
|
+
|
|
1119
|
+
// 1. OpenCode-style Thinking (Reasoning): Clean, indented, dimmed and unboxed
|
|
887
1120
|
if (msg.reasoning && msg.reasoning.trim().length > 0) {
|
|
888
|
-
|
|
889
|
-
|
|
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] || "⠋";
|
|
890
1124
|
|
|
891
|
-
if (
|
|
892
|
-
|
|
1125
|
+
if (isStillThinking) {
|
|
1126
|
+
chatContent.push(` ${theme.yellow(`🧠 ${spinner} Raciocinando...`)}`);
|
|
893
1127
|
} else {
|
|
894
|
-
|
|
895
|
-
if (this.isGenerating && !msg.content && this.messages.indexOf(msg) === this.messages.length - 1) {
|
|
896
|
-
const spinner = this.spinnerFrames[this.spinnerIndex] || "⠋";
|
|
897
|
-
rLines.push("");
|
|
898
|
-
rLines.push(` ${theme.yellow(`${spinner} Raciocinando...`)}`);
|
|
899
|
-
}
|
|
900
|
-
thinkLines = drawBox({
|
|
901
|
-
title: "🧠 Raciocínio",
|
|
902
|
-
width: thinkWidth,
|
|
903
|
-
borderColor: theme.borderInactive,
|
|
904
|
-
titleColor: theme.muted,
|
|
905
|
-
content: rLines,
|
|
906
|
-
});
|
|
907
|
-
if (!this.isGenerating) {
|
|
908
|
-
msg.cachedReasoningBox = thinkLines;
|
|
909
|
-
msg.cachedWidth = innerChatW;
|
|
910
|
-
}
|
|
1128
|
+
chatContent.push(` ${theme.yellow("🧠 Raciocínio:")}`);
|
|
911
1129
|
}
|
|
912
1130
|
|
|
913
|
-
|
|
914
|
-
|
|
1131
|
+
const rLines = formatReasoning(msg.reasoning, innerChatW - 8);
|
|
1132
|
+
for (const r of rLines) {
|
|
1133
|
+
chatContent.push(` ${r}`);
|
|
915
1134
|
}
|
|
916
1135
|
chatContent.push("");
|
|
917
1136
|
}
|
|
@@ -936,11 +1155,18 @@ export class ChatView implements TuiView {
|
|
|
936
1155
|
chatContent.push(` ${theme.yellow(`${spinner} Pensando...`)}`);
|
|
937
1156
|
}
|
|
938
1157
|
|
|
939
|
-
|
|
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("");
|
|
940
1165
|
chatContent.push(
|
|
941
|
-
` ${theme.
|
|
1166
|
+
` ${theme.cyan("▣")} ${theme.bold("Qwen")} ${theme.dim("·")} ${theme.cyan(messageModel)}${timingStr}`,
|
|
942
1167
|
);
|
|
943
1168
|
}
|
|
1169
|
+
chatContent.push("");
|
|
944
1170
|
}
|
|
945
1171
|
}
|
|
946
1172
|
|
|
@@ -1042,13 +1268,18 @@ export class ChatView implements TuiView {
|
|
|
1042
1268
|
? `${spinner} Gerando... (Esc para cancelar)`
|
|
1043
1269
|
: actionLabel;
|
|
1044
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
|
+
|
|
1045
1276
|
const inputBox = drawBox({
|
|
1046
1277
|
title: inputTitle,
|
|
1047
1278
|
width,
|
|
1048
1279
|
height: 3,
|
|
1049
1280
|
borderColor: this.isGenerating ? theme.yellow : theme.borderActive,
|
|
1050
1281
|
titleColor: this.isGenerating ? theme.yellow : theme.cyan,
|
|
1051
|
-
footer:
|
|
1282
|
+
footer: inputFooter,
|
|
1052
1283
|
content: inputContent,
|
|
1053
1284
|
});
|
|
1054
1285
|
totalLines.push(...inputBox);
|