qwenproxy-cli 1.0.3 → 1.0.5

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/src/tui/screen.ts CHANGED
@@ -33,6 +33,10 @@ export class Screen {
33
33
  private exitHandler: (() => void) | null = null;
34
34
  private prevRenderedRows: string[] = [];
35
35
  private originalStdinEmit: typeof process.stdin.emit | null = null;
36
+ private lastHoverCol = -1;
37
+ private lastHoverRow = -1;
38
+ private lastHoverTime = 0;
39
+ private pendingHoverTimeout: NodeJS.Timeout | null = null;
36
40
 
37
41
  constructor() {
38
42
  this.exitHandler = () => this.stop();
@@ -116,13 +120,42 @@ export class Screen {
116
120
  mouse: { type: "drag", button: "left", col, row },
117
121
  });
118
122
  } else if (btn === 35) {
119
- self.dispatchKey({
120
- name: "hover",
121
- ctrl: false,
122
- shift: false,
123
- meta: false,
124
- mouse: { type: "hover", col, row },
125
- });
123
+ if (col === self.lastHoverCol && row === self.lastHoverRow) {
124
+ continue;
125
+ }
126
+ const now = Date.now();
127
+ const timeSinceLast = now - self.lastHoverTime;
128
+ const emitHover = (c: number, r: number) => {
129
+ if (!self.active) return;
130
+ self.lastHoverCol = c;
131
+ self.lastHoverRow = r;
132
+ self.lastHoverTime = Date.now();
133
+ self.dispatchKey({
134
+ name: "hover",
135
+ ctrl: false,
136
+ shift: false,
137
+ meta: false,
138
+ mouse: { type: "hover", col: c, row: r },
139
+ });
140
+ };
141
+
142
+ if (timeSinceLast >= 30) {
143
+ if (self.pendingHoverTimeout) {
144
+ clearTimeout(self.pendingHoverTimeout);
145
+ self.pendingHoverTimeout = null;
146
+ }
147
+ emitHover(col, row);
148
+ } else {
149
+ if (self.pendingHoverTimeout) {
150
+ clearTimeout(self.pendingHoverTimeout);
151
+ }
152
+ self.pendingHoverTimeout = setTimeout(() => {
153
+ self.pendingHoverTimeout = null;
154
+ if (col !== self.lastHoverCol || row !== self.lastHoverRow) {
155
+ emitHover(col, row);
156
+ }
157
+ }, 30 - timeSinceLast);
158
+ }
126
159
  }
127
160
  }
128
161
  if (handled) {
@@ -212,6 +245,13 @@ export class Screen {
212
245
  process.stdin.emit = this.originalStdinEmit;
213
246
  this.originalStdinEmit = null;
214
247
  }
248
+ if (this.pendingHoverTimeout) {
249
+ clearTimeout(this.pendingHoverTimeout);
250
+ this.pendingHoverTimeout = null;
251
+ }
252
+ this.lastHoverCol = -1;
253
+ this.lastHoverRow = -1;
254
+ this.lastHoverTime = 0;
215
255
 
216
256
  // Restore original screen buffer, cursor, and disable all mouse tracking modes synchronously
217
257
  const restoreSeq = ANSI.disableMouse + ANSI.exitAltScreen + ANSI.showCursor + ANSI.reset;
@@ -95,15 +95,23 @@ export class ServerManager {
95
95
  let line = raw.trim();
96
96
  if (!line) continue;
97
97
 
98
- // Filter out raw ASCII box frames (like +-----+ or empty row | |)
98
+ // Filter out raw ASCII box frames and border rows entirely
99
99
  if (/^[+\-=#]{5,}$/.test(line)) continue;
100
100
  if (/^\|\s*\|$/.test(line)) continue;
101
-
102
- // If line is an ASCII box row like "| Endpoint http://... |", extract the text
103
- if (line.startsWith("|") && line.endsWith("|")) {
104
- line = line.slice(1, -1).trim();
101
+ if (line.startsWith("|") && line.endsWith("|")) continue;
102
+
103
+ // Filter out any box remnants (startup banner is for headless npm start only)
104
+ if (
105
+ line === "QwenProxy" ||
106
+ line === "OpenAI & Anthropic Compatible API" ||
107
+ /^Endpoint\s+http/i.test(line) ||
108
+ /^Port\s+\d+/i.test(line) ||
109
+ /^Accounts\s+\d+\/\d+/i.test(line) ||
110
+ /^API Key\s+/i.test(line) ||
111
+ /^Status\s+●/i.test(line)
112
+ ) {
113
+ continue;
105
114
  }
106
-
107
115
  if (!line) continue;
108
116
 
109
117
  // Prevent identical consecutive duplicate logs in the same second
@@ -224,7 +232,7 @@ export class ServerManager {
224
232
  this.state = "online";
225
233
  this.appendLog(
226
234
  "INFO",
227
- `QwenProxy está em execução na porta ${port}. Conectado com sucesso!`,
235
+ `✨ [Server] Conectado à instância em execução na porta ${port}`,
228
236
  );
229
237
  return;
230
238
  }
@@ -234,25 +242,25 @@ export class ServerManager {
234
242
  this.state = "warming";
235
243
  this.appendLog(
236
244
  "INFO",
237
- `Iniciando servidor QwenProxy na porta ${port} e aquecendo Playwright...`,
245
+ `🚀 [Server] Iniciando servidor na porta ${port}...`,
238
246
  );
239
247
 
240
248
  this.interceptLogs();
241
249
 
242
250
  this.startPromise = (async () => {
243
251
  try {
244
- await startServer({ installSignalHandlers: false });
252
+ await startServer({ installSignalHandlers: false, showBanner: false });
245
253
  this.state = "online";
246
254
  this.appendLog(
247
255
  "INFO",
248
- `✓ Servidor QwenProxy pronto e online em http://${cleanHost}:${port}/v1`,
256
+ `✨ [Server] QwenProxy pronto e online em http://${cleanHost}:${port}/v1`,
249
257
  );
250
258
  } catch (err: any) {
251
259
  this.state = "error";
252
260
  this.lastError = err?.message || String(err);
253
261
  this.appendLog(
254
262
  "ERROR",
255
- `✗ Falha ao iniciar servidor: ${this.lastError}`,
263
+ `❌ [Server] Falha ao iniciar servidor: ${this.lastError}`,
256
264
  );
257
265
  } finally {
258
266
  this.startPromise = null;
package/src/tui/theme.ts CHANGED
@@ -16,7 +16,7 @@ export const ANSI = {
16
16
  showCursor: "\x1b[?25h",
17
17
  enterAltScreen: "\x1b[?1049h",
18
18
  exitAltScreen: "\x1b[?1049l",
19
- enableMouse: "\x1b[?1000h\x1b[?1002h\x1b[?1006h",
19
+ enableMouse: "\x1b[?1000h\x1b[?1002h\x1b[?1003h\x1b[?1006h",
20
20
  disableMouse: "\x1b[?1006l\x1b[?1005l\x1b[?1004l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?1015l",
21
21
  };
22
22
 
@@ -181,22 +181,39 @@ const ANSI_REGEX =
181
181
  export function stripAnsi(str: string): string {
182
182
  return str.replace(ANSI_REGEX, "");
183
183
  }
184
+ const graphemeSegmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
184
185
 
185
186
  /**
186
- * Computes visual display width of string, accounting for wide characters and emojis.
187
+ * Computes visual display width of string, accounting for wide characters,
188
+ * grapheme clusters, variation selectors (VS16), and terminal-wide emojis.
187
189
  */
188
190
  export function stringWidth(str: string): number {
189
191
  const clean = stripAnsi(str).replace(/\t/g, " ").replace(/\r/g, "");
190
192
  let width = 0;
191
- for (const char of clean) {
192
- const code = char.codePointAt(0) || 0;
193
- // Zero-width characters (variation selectors, zero-width space/joiner)
194
- if ((code >= 0xfe00 && code <= 0xfe0f) || (code >= 0x200b && code <= 0x200d)) {
193
+ for (const { segment } of graphemeSegmenter.segment(clean)) {
194
+ const code = segment.codePointAt(0) || 0;
195
+ // Standalone zero-width characters (variation selectors, zero-width space/joiner)
196
+ if (
197
+ segment === "\u200b" ||
198
+ segment === "\u200c" ||
199
+ segment === "\u200d" ||
200
+ (code >= 0xfe00 && code <= 0xfe0f && segment.length === 1)
201
+ ) {
202
+ continue;
203
+ }
204
+ // Graphemes containing variation selector 16 (emoji presentation)
205
+ if (segment.includes("\ufe0f")) {
206
+ width += 2;
195
207
  continue;
196
208
  }
197
- // Specific BMP emojis and symbols that occupy 2 visual terminal cells (e.g. ⚠️, ⚡, ✅, ❌, ✨, )
209
+ // Specific BMP emojis and symbols that occupy 2 visual terminal cells (e.g. ⚠️, ⏱, ⚡, ✅, ❌, ✨, ☕, ⚙)
198
210
  const isBmpEmoji =
199
211
  code === 0x26a0 || // ⚠️ (WARNING SIGN)
212
+ code === 0x23f1 || // ⏱ (STOPWATCH)
213
+ code === 0x23f0 || // ⏰
214
+ code === 0x23f3 || // ⏳
215
+ code === 0x231a || // ⌚
216
+ code === 0x231b || // ⌛
200
217
  code === 0x2705 || // ✅
201
218
  code === 0x2728 || // ✨
202
219
  code === 0x274c || // ❌
@@ -207,10 +224,9 @@ export function stringWidth(str: string): number {
207
224
  code === 0x2b55 || // ⭕
208
225
  code === 0x26a1 || // ⚡
209
226
  code === 0x2615 || // ☕
210
- code === 0x231a || //
211
- code === 0x231b || //
212
- code === 0x23f0 || // ⏰
213
- code === 0x23f3; // ⏳
227
+ code === 0x2699 || //
228
+ code === 0x2709; //
229
+
214
230
  // Common emoji and CJK full-width ranges (SMP Emojis 0x1f300 - 0x1faff)
215
231
  if (
216
232
  isBmpEmoji ||
@@ -244,15 +260,15 @@ export function truncate(str: string, maxWidth: number, ellipsis = "…"): strin
244
260
  const targetW = Math.max(0, maxWidth - ellipsisW);
245
261
 
246
262
  let currentW = 0;
247
- let cutIndex = 0;
248
- for (const char of clean) {
249
- const charW = stringWidth(char);
250
- if (currentW + charW > targetW) break;
251
- currentW += charW;
252
- cutIndex += char.length;
263
+ let result = "";
264
+ for (const { segment } of graphemeSegmenter.segment(clean)) {
265
+ const segW = stringWidth(segment);
266
+ if (currentW + segW > targetW) break;
267
+ currentW += segW;
268
+ result += segment;
253
269
  }
254
270
 
255
- return clean.slice(0, cutIndex) + ellipsis;
271
+ return result + ellipsis;
256
272
  }
257
273
 
258
274
  /**
@@ -328,6 +344,7 @@ export interface BoxOptions {
328
344
  borderColor?: (s: string) => string;
329
345
  titleColor?: (s: string) => string;
330
346
  footerColor?: (s: string) => string;
347
+ wrap?: boolean;
331
348
  content: string[];
332
349
  }
333
350
 
@@ -379,16 +396,17 @@ export function drawBox(options: BoxOptions): string[] {
379
396
  }
380
397
  lines.push(borderColor(b.tl) + topHeader + borderColor(b.tr));
381
398
 
382
- // Flatten and auto-wrap content lines so words are never cut off with ellipsis,
383
- // and strictly prevent any embedded \n or \r from leaking into a terminal row!
399
+ // Flatten content lines and optionally wrap prose lines.
400
+ // Fixed-height boxes do not auto-wrap by default to preserve row and scrollbar alignment.
401
+ const shouldWrap = options.wrap === true || (!options.height && options.wrap !== false);
384
402
  const expandedContent: string[] = [];
385
403
  for (const item of content) {
386
404
  const subItems = String(item ?? "").split(/\r?\n/);
387
405
  for (const sub of subItems) {
388
- if (stringWidth(sub) <= innerW) {
389
- expandedContent.push(sub);
390
- } else {
406
+ if (shouldWrap && stringWidth(sub) > innerW) {
391
407
  expandedContent.push(...wrapContentLine(sub, innerW));
408
+ } else {
409
+ expandedContent.push(sub);
392
410
  }
393
411
  }
394
412
  }
@@ -33,6 +33,16 @@ export class AccountsView implements TuiView {
33
33
  private modalHoveredField: "email" | "password" | "save" | "cancel" | null = null;
34
34
  private lastModalLeftPad = 0;
35
35
  private lastLeftW = 46;
36
+ private confirmDialog: {
37
+ type: "remove_account" | "delete_account_chats" | "delete_all_chats";
38
+ title: string;
39
+ message: string;
40
+ detail: string;
41
+ onConfirm: () => Promise<void>;
42
+ } | null = null;
43
+ private confirmDialogHovered: "confirm" | "cancel" | null = null;
44
+ private lastConfirmModalLeftPad = 0;
45
+ private lastConfirmModalStartRow = 0;
36
46
  constructor() {
37
47
  this.refresh();
38
48
  }
@@ -42,9 +52,15 @@ export class AccountsView implements TuiView {
42
52
  }
43
53
 
44
54
  public isCapturingText(): boolean {
45
- return this.isAddModalOpen;
55
+ return this.isAddModalOpen || this.confirmDialog !== null;
46
56
  }
47
57
  public getShortcuts(): Array<{ key: string; label: string }> {
58
+ if (this.confirmDialog) {
59
+ return [
60
+ { key: "S / Enter", label: "Confirmar" },
61
+ { key: "N / Esc", label: "Cancelar" },
62
+ ];
63
+ }
48
64
  if (this.isAddModalOpen) {
49
65
  return [
50
66
  { key: "↑↓/Mouse", label: "Alternar" },
@@ -55,6 +71,8 @@ export class AccountsView implements TuiView {
55
71
  return [
56
72
  { key: "a", label: "Adicionar Conta" },
57
73
  { key: "d", label: "Remover Conta" },
74
+ { key: "x", label: "Limpar Chats" },
75
+ { key: "l", label: "Limpar Todos Chats" },
58
76
  { key: "c", label: "Zerar Cooldown" },
59
77
  { key: "z", label: "Zerar Todas" },
60
78
  ];
@@ -125,6 +143,63 @@ export class AccountsView implements TuiView {
125
143
  }
126
144
  }
127
145
  public async handleKey(key: KeyEvent): Promise<boolean | void> {
146
+ // 0. Confirm Dialog Active
147
+ if (this.confirmDialog) {
148
+ if (key.name === "s" || key.name === "S" || key.name === "enter" || key.name === "return") {
149
+ const dialog = this.confirmDialog;
150
+ this.confirmDialog = null;
151
+ this.confirmDialogHovered = null;
152
+ await dialog.onConfirm();
153
+ return true;
154
+ }
155
+ if (key.name === "escape" || key.name === "n" || key.name === "N") {
156
+ this.confirmDialog = null;
157
+ this.confirmDialogHovered = null;
158
+ this.setStatusMessage(theme.muted("Ação cancelada"));
159
+ return true;
160
+ }
161
+ if (key.name === "hover" && key.mouse) {
162
+ const { row, col } = key.mouse;
163
+ const btnRow = this.lastConfirmModalStartRow + 4;
164
+ if (row === btnRow) {
165
+ const startCol = this.lastConfirmModalLeftPad + 2;
166
+ if (col >= startCol && col <= startCol + 25) {
167
+ this.confirmDialogHovered = "confirm";
168
+ return true;
169
+ }
170
+ if (col >= startCol + 26 && col <= startCol + 50) {
171
+ this.confirmDialogHovered = "cancel";
172
+ return true;
173
+ }
174
+ }
175
+ if (this.confirmDialogHovered !== null) {
176
+ this.confirmDialogHovered = null;
177
+ return true;
178
+ }
179
+ }
180
+ if (key.name === "click" && key.mouse) {
181
+ const { row, col } = key.mouse;
182
+ const btnRow = this.lastConfirmModalStartRow + 4;
183
+ if (row === btnRow) {
184
+ const startCol = this.lastConfirmModalLeftPad + 2;
185
+ if (col >= startCol && col <= startCol + 25) {
186
+ const dialog = this.confirmDialog;
187
+ this.confirmDialog = null;
188
+ this.confirmDialogHovered = null;
189
+ await dialog.onConfirm();
190
+ return true;
191
+ }
192
+ if (col >= startCol + 26 && col <= startCol + 50) {
193
+ this.confirmDialog = null;
194
+ this.confirmDialogHovered = null;
195
+ this.setStatusMessage(theme.muted("Ação cancelada"));
196
+ return true;
197
+ }
198
+ }
199
+ }
200
+ return true;
201
+ }
202
+
128
203
  // 1. Add Account Modal Active
129
204
  if (this.isAddModalOpen) {
130
205
  if (key.name === "escape") {
@@ -335,22 +410,83 @@ export class AccountsView implements TuiView {
335
410
  return true;
336
411
  }
337
412
 
338
- // Delete selected account with 'd' or 'D'
413
+ // Delete selected account with 'd' or 'D' (requires confirmation)
339
414
  if ((key.name === "d" || key.name === "D") && !key.ctrl) {
340
415
  const selected = accounts[this.selectedIndex];
341
416
  if (!selected) {
342
417
  this.setStatusMessage(theme.yellow("[!] Nenhuma conta selecionada para remover"));
343
418
  return true;
344
419
  }
345
- removeAccount(selected.id);
346
- void (async () => {
347
- try {
348
- const { closePlaywrightForAccount } = await import("../../services/playwright.ts");
349
- await closePlaywrightForAccount(selected.id);
350
- } catch {}
351
- })();
352
- await this.refresh();
353
- this.setStatusMessage(theme.green(`✓ Conta ${selected.emailOrName} removida com sucesso`));
420
+ this.confirmDialog = {
421
+ type: "remove_account",
422
+ title: "⚠️ Confirmar Remoção de Conta",
423
+ message: `Deseja remover a conta ${selected.emailOrName}?`,
424
+ detail: "A conta será excluída do banco de dados e sua sessão encerrada.",
425
+ onConfirm: async () => {
426
+ removeAccount(selected.id);
427
+ try {
428
+ const { closePlaywrightForAccount, removePlaywrightProfile } = await import("../../services/playwright.ts");
429
+ const { getAccountProfilePath } = await import("../../core/paths.ts");
430
+ await closePlaywrightForAccount(selected.id);
431
+ removePlaywrightProfile(getAccountProfilePath(selected.id));
432
+ } catch {}
433
+ await this.refresh();
434
+ this.setStatusMessage(theme.green(`✓ Conta ${selected.emailOrName} removida com sucesso`));
435
+ },
436
+ };
437
+ return true;
438
+ }
439
+
440
+ // Delete chats of selected account with 'x' or 'X' (requires confirmation)
441
+ if ((key.name === "x" || key.name === "X") && !key.ctrl) {
442
+ const selected = accounts[this.selectedIndex];
443
+ if (!selected) {
444
+ this.setStatusMessage(theme.yellow("[!] Nenhuma conta selecionada"));
445
+ return true;
446
+ }
447
+ this.confirmDialog = {
448
+ type: "delete_account_chats",
449
+ title: "⚠️ Apagar Chats Remotos no Qwen",
450
+ message: `Apagar TODOS os chats no Qwen da conta ${selected.emailOrName}?`,
451
+ detail: "Esta ação é irreversível e limpará todas as conversas em chat.qwen.ai.",
452
+ onConfirm: async () => {
453
+ this.setStatusMessage(theme.yellow(`⏳ Apagando chats no Qwen para ${selected.emailOrName}...`));
454
+ try {
455
+ const { deleteChatsForAccountId } = await import("../../services/chat-cleanup.ts");
456
+ await deleteChatsForAccountId(selected.id);
457
+ await this.refresh();
458
+ this.setStatusMessage(theme.green(`✓ Todos os chats de ${selected.emailOrName} foram apagados no Qwen!`));
459
+ } catch (err: any) {
460
+ this.setStatusMessage(theme.red(`✗ Falha ao apagar chats: ${err?.message || String(err)}`));
461
+ }
462
+ },
463
+ };
464
+ return true;
465
+ }
466
+
467
+ // Delete chats of all accounts with 'l' or 'L' (requires confirmation)
468
+ if ((key.name === "l" || key.name === "L") && !key.ctrl) {
469
+ if (accounts.length === 0) {
470
+ this.setStatusMessage(theme.yellow("[!] Nenhuma conta configurada"));
471
+ return true;
472
+ }
473
+ this.confirmDialog = {
474
+ type: "delete_all_chats",
475
+ title: "⚠️ Apagar Chats de TODAS as Contas",
476
+ message: `Apagar TODOS os chats remotos de TODAS as ${accounts.length} contas no Qwen?`,
477
+ detail: "Esta ação é irreversível e limpará o histórico no chat.qwen.ai.",
478
+ onConfirm: async () => {
479
+ this.setStatusMessage(theme.yellow(`⏳ Apagando chats no Qwen de todas as contas...`));
480
+ try {
481
+ const { deleteChatsForConfiguredAccounts } = await import("../../services/chat-cleanup.ts");
482
+ const res = await deleteChatsForConfiguredAccounts(true);
483
+ await this.refresh();
484
+ this.setStatusMessage(theme.green(`✓ Chats apagados no Qwen: ${res.succeeded}/${res.attempted} contas limpas!`));
485
+ } catch (err: any) {
486
+ this.setStatusMessage(theme.red(`✗ Falha ao apagar chats: ${err?.message || String(err)}`));
487
+ }
488
+ },
489
+ };
354
490
  return true;
355
491
  }
356
492
 
@@ -371,9 +507,9 @@ export class AccountsView implements TuiView {
371
507
  return true;
372
508
  }
373
509
 
374
- // Right panel action buttons hover (rows 15 to 18)
510
+ // Right panel action buttons hover (rows 15 to 20)
375
511
  if (col >= leftW) {
376
- if (row >= 15 && row <= 18) {
512
+ if (row >= 15 && row <= 20) {
377
513
  if (this.hoveredActionRow !== row) {
378
514
  this.hoveredActionRow = row;
379
515
  return true;
@@ -387,7 +523,6 @@ export class AccountsView implements TuiView {
387
523
  return true;
388
524
  }
389
525
  }
390
-
391
526
  // Mouse click on account rows or action buttons
392
527
  if (key.name === "click" && key.mouse) {
393
528
  const { row, col } = key.mouse;
@@ -416,9 +551,16 @@ export class AccountsView implements TuiView {
416
551
  await this.handleKey({ name: "z", ctrl: false, shift: false, meta: false });
417
552
  return true;
418
553
  }
554
+ if (row === 19) {
555
+ await this.handleKey({ name: "x", ctrl: false, shift: false, meta: false });
556
+ return true;
557
+ }
558
+ if (row === 20) {
559
+ await this.handleKey({ name: "l", ctrl: false, shift: false, meta: false });
560
+ return true;
561
+ }
419
562
  }
420
563
  }
421
- // Navigate accounts list (Mouse wheel or Up/Down keys)
422
564
  if (key.name === "up" || key.name === "wheelup" || (key.name === "k" && !key.ctrl)) {
423
565
  if (accounts.length > 0) {
424
566
  this.selectedIndex = Math.max(0, this.selectedIndex - 1);
@@ -469,6 +611,9 @@ export class AccountsView implements TuiView {
469
611
  this.lastLeftW = leftW;
470
612
  const rightW = Math.max(30, width - leftW - 1);
471
613
 
614
+ if (snapshot) {
615
+ this.statusData = snapshot;
616
+ }
472
617
  const data = snapshot || this.statusData;
473
618
  const accounts = data?.accounts || [];
474
619
  const selected = accounts[this.selectedIndex];
@@ -490,12 +635,12 @@ export class AccountsView implements TuiView {
490
635
  const isHovered = idx === this.hoveredAccountIndex;
491
636
  const pointer = isFocused ? theme.cyan(`${glyphs.pointer} `) : " ";
492
637
  const num = pad(String(idx + 1) + ".", 4);
493
- const name = pad(acc.emailOrName, 22);
638
+ const name = pad(truncate(acc.emailOrName, 20), 22);
494
639
 
495
640
  let status = theme.green(`${glyphs.bullet} Pronto `);
496
641
  if (acc.onCooldown) {
497
642
  const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
498
- status = theme.yellow(`⚠ ${mins}m cd `);
643
+ status = theme.yellow(`⚠️ ${mins}m cd `);
499
644
  } else if (!acc.headersReady) {
500
645
  status = acc.isInitialized
501
646
  ? theme.yellow(`◐ Aquecendo...`)
@@ -562,8 +707,9 @@ export class AccountsView implements TuiView {
562
707
  rightContent.push(` ${this.hoveredActionRow === 16 ? theme.bgHover(` ${theme.red("[ D ] Remover Conta")} `) : `${theme.red("[ D ]")} Remover Conta`}`);
563
708
  rightContent.push(` ${this.hoveredActionRow === 17 ? theme.bgHover(` ${theme.yellow("[ C ] Zerar Cooldown")} `) : `${theme.yellow("[ C ]")} Zerar Cooldown`}`);
564
709
  rightContent.push(` ${this.hoveredActionRow === 18 ? theme.bgHover(` ${theme.green("[ Z ] Zerar Todas")} `) : `${theme.green("[ Z ]")} Zerar Todas`}`);
710
+ rightContent.push(` ${this.hoveredActionRow === 19 ? theme.bgHover(` ${theme.peach("[ X ] Limpar Chats (Conta)")} `) : `${theme.peach("[ X ]")} Limpar Chats (Conta)`}`);
711
+ rightContent.push(` ${this.hoveredActionRow === 20 ? theme.bgHover(` ${theme.red("[ L ] Limpar Todos os Chats")} `) : `${theme.red("[ L ]")} Limpar Todos os Chats`}`);
565
712
  }
566
-
567
713
  const rightBox = drawBox({
568
714
  title: "Inspeção de Conta",
569
715
  width: rightW,
@@ -655,6 +801,39 @@ export class AccountsView implements TuiView {
655
801
  return modalBox.map((line) => padStr + line);
656
802
  }
657
803
 
804
+ if (this.confirmDialog) {
805
+ const modalW = Math.min(width - 4, 66);
806
+ this.lastConfirmModalLeftPad = Math.max(0, Math.floor((width - modalW) / 2));
807
+ const confirmBtn =
808
+ this.confirmDialogHovered === "confirm"
809
+ ? theme.bgHover(theme.red(" [ S / Enter ] Sim, Confirmar "))
810
+ : theme.red("[ S / Enter ] Sim, Confirmar");
811
+ const cancelBtn =
812
+ this.confirmDialogHovered === "cancel"
813
+ ? theme.bgHover(theme.green(" [ N / Esc ] Cancelar "))
814
+ : theme.green("[ N / Esc ] Cancelar");
815
+
816
+ const modalContent = [
817
+ "",
818
+ ` ${theme.bold(this.confirmDialog.message)}`,
819
+ ` ${theme.muted(this.confirmDialog.detail)}`,
820
+ "",
821
+ ` ${confirmBtn} ${cancelBtn}`,
822
+ ];
823
+
824
+ const modalBox = drawBox({
825
+ title: this.confirmDialog.title,
826
+ width: modalW,
827
+ height: Math.min(contentH, 8),
828
+ borderColor: theme.red,
829
+ titleColor: theme.red,
830
+ content: modalContent,
831
+ });
832
+
833
+ const padStr = " ".repeat(this.lastConfirmModalLeftPad);
834
+ return modalBox.map((line) => padStr + line);
835
+ }
836
+
658
837
  return mergedLines;
659
838
  }
660
839
  }
@@ -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 } from "../theme.ts";
7
+ import { theme, glyphs, drawBox, pad, truncate } from "../theme.ts";
8
8
  import { fetchProxyStatus, resetAllCooldowns, formatUptime } from "../proxy-client.ts";
9
9
  import { ServerManager } from "../server-manager.ts";
10
10
 
@@ -169,17 +169,17 @@ export class StatusView implements TuiView {
169
169
  } else {
170
170
  accounts.slice(0, contentH - 5).forEach((acc, idx) => {
171
171
  const num = pad(String(idx + 1), 3);
172
- const name = pad(acc.emailOrName, 22);
172
+ const name = pad(truncate(acc.emailOrName, 20), 20);
173
173
  let status = theme.green(`${glyphs.bullet} Pronto`);
174
174
  if (acc.onCooldown) {
175
175
  const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
176
- status = theme.yellow(`⚠ Cooldown ${mins}m`);
176
+ status = theme.yellow(`⚠️ Cooldown ${mins}m`);
177
177
  } else if (!acc.headersReady) {
178
178
  status = acc.isInitialized
179
179
  ? theme.yellow(`◐ Aquecendo...`)
180
180
  : theme.muted(`○ Standby`);
181
181
  }
182
- rightContent.push(` ${num} ${name} ${status}`);
182
+ rightContent.push(` ${num} ${name} ${status}`);
183
183
  });
184
184
  }
185
185