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.
@@ -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 { streamChatCompletions, fetchLiveModels } from "../proxy-client.ts";
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): { badge: string; category: 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 { badge: theme.lavender("[Imagem]"), category: "Geração de Imagem" };
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 { badge: theme.peach("[Vídeo] "), category: "Geração de Vídeo" };
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 { badge: theme.cyan("[Texto] "), category: "Texto & Raciocínio" };
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.category === "Texto & Raciocínio") {
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
- if (row >= 9 && row < 9 + this.availableEfforts.length) {
265
- const hoverIdx = row - 9;
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
- if (row >= 9 && row < 9 + this.availableEfforts.length) {
276
- this.selectedEffort = this.availableEfforts[row - 9].id;
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[row - 9].label}`;
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. Header button hover (rows 4 to 6: Model, Effort)
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
- // 4. Header button click (rows 4 to 6: Model, Effort)
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.category === "Texto & Raciocínio") {
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).category === "Texto & Raciocínio";
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.category === "Texto & Raciocínio";
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 (Thinking) ]"
928
+ ? "[ Effort: High ]"
751
929
  : this.selectedEffort === "medium"
752
- ? "[ Effort: Medium (Auto) ]"
753
- : "[ Effort: Low (Fast) ]"
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 shortcutsLabel = `[ F2: Modelo${isReasoning ? " | F3: Effort" : ""} (${this.selectedModelIndex + 1}/${totalModels}) ]`;
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 = modelEnd + 3; // 2 spaces
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
- const userLines = msg.content.split(/\r?\n/);
876
- for (let u = 0; u < userLines.length; u++) {
877
- if (u === 0) {
878
- chatContent.push(` ${theme.blue(glyphs.pointer + " Você:")} ${theme.white(userLines[u])}`);
879
- } else {
880
- chatContent.push(` ${theme.white(userLines[u])}`);
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
- chatContent.push(` ${theme.green(glyphs.bullet + " Qwen (" + messageModel + "):")}`);
886
- // 1. Dedicated Thinking (Reasoning) Container - Opaque, Dimmed, and Cached
1118
+
1119
+ // 1. OpenCode-style Thinking (Reasoning): Clean, indented, dimmed and unboxed
887
1120
  if (msg.reasoning && msg.reasoning.trim().length > 0) {
888
- const thinkWidth = Math.max(20, innerChatW - 4);
889
- let thinkLines: string[];
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 (msg.cachedWidth === innerChatW && msg.cachedReasoningBox) {
892
- thinkLines = msg.cachedReasoningBox;
1125
+ if (isStillThinking) {
1126
+ chatContent.push(` ${theme.yellow(`🧠 ${spinner} Raciocinando...`)}`);
893
1127
  } else {
894
- const rLines = formatReasoning(msg.reasoning, thinkWidth - 4).map((l) => ` ${l}`);
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
- for (const line of thinkLines) {
914
- chatContent.push(` ${line}`);
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
- if (msg.totalTimeMs) {
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.dim(`[TTFB: ${msg.ttfbMs}ms | Total: ${(msg.totalTimeMs / 1000).toFixed(2)}s]`)}`,
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: this.statusNote ? stripAnsi(this.statusNote) : undefined,
1282
+ footer: inputFooter,
1052
1283
  content: inputContent,
1053
1284
  });
1054
1285
  totalLines.push(...inputBox);