qwenproxy-cli 1.0.25 → 1.0.27

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/theme.ts CHANGED
@@ -47,11 +47,14 @@ export const theme = {
47
47
 
48
48
  import { execSync, spawnSync } from "node:child_process";
49
49
 
50
+ let memoryClipboard = "";
51
+
50
52
  /**
51
53
  * Safely writes text to the system clipboard on Windows/macOS/Linux.
52
54
  * Also emits OSC 52 to copy inside terminal emulators supporting it.
53
55
  */
54
56
  export function setClipboardText(text: string): boolean {
57
+ memoryClipboard = text;
55
58
  try {
56
59
  // 1. Emit OSC 52 sequence for terminal emulators supporting it natively (only when interactive TTY)
57
60
  try {
@@ -64,8 +67,7 @@ export function setClipboardText(text: string): boolean {
64
67
  // 2. OS-level clipboard utility
65
68
  if (process.platform === "win32") {
66
69
  const p = spawnSync("clip.exe", {
67
- input: text,
68
- encoding: "utf-8",
70
+ input: Buffer.from(text, "utf16le"),
69
71
  windowsHide: true,
70
72
  });
71
73
  return p.status === 0;
@@ -90,17 +92,21 @@ export function setClipboardText(text: string): boolean {
90
92
  export function getClipboardText(): string {
91
93
  try {
92
94
  if (process.platform === "win32") {
93
- return execSync("powershell -NoProfile -Command Get-Clipboard", {
94
- timeout: 1000,
95
- windowsHide: true,
96
- stdio: ["pipe", "pipe", "ignore"],
97
- })
98
- .toString()
95
+ const res = execSync(
96
+ 'powershell -NoProfile -Command "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-Clipboard"',
97
+ {
98
+ timeout: 2000,
99
+ windowsHide: true,
100
+ stdio: ["pipe", "pipe", "ignore"],
101
+ },
102
+ )
103
+ .toString("utf8")
99
104
  .replace(/\r?\n/g, "")
100
105
  .trim();
106
+ if (res) return res;
101
107
  }
102
108
  } catch {}
103
- return "";
109
+ return memoryClipboard;
104
110
  }
105
111
 
106
112
  export const glyphs = {
package/src/tui/types.ts CHANGED
@@ -21,6 +21,22 @@ export interface ProxyStatusSnapshot {
21
21
  systemMemoryPct?: number;
22
22
  activeStreams?: number;
23
23
  waitingStreams?: number;
24
+ metrics?: {
25
+ requestsTotal: number;
26
+ requestsErrors: number;
27
+ successRate: number;
28
+ latencyAvgMs: number;
29
+ deltasCount: number;
30
+ fullReplaysCount: number;
31
+ deltaRatio: number;
32
+ toolCallsCount: number;
33
+ toolCallsRecovered: number;
34
+ captchasDetected: number;
35
+ captchasSolved: number;
36
+ chatsCleaned: number;
37
+ cacheHitRatio?: number;
38
+ cacheBytesSaved?: number;
39
+ };
24
40
  accounts: Array<{
25
41
  id: string;
26
42
  emailOrName: string;
@@ -28,7 +44,10 @@ export interface ProxyStatusSnapshot {
28
44
  cooldownUntil: number | null;
29
45
  onCooldown: boolean;
30
46
  remainingCooldownMs: number;
47
+ cooldownReason?: string | null;
31
48
  headersReady: boolean;
32
49
  isInitialized?: boolean;
50
+ activeStreams?: number;
51
+ streamLimit?: number;
33
52
  }>;
34
53
  }
@@ -13,6 +13,33 @@ import {
13
13
  import { addAccount, removeAccount } from "../../core/accounts.ts";
14
14
  import { ServerManager } from "../server-manager.ts";
15
15
  import { config } from "../../core/config.ts";
16
+ export function formatCooldownReason(reason?: string | null, maxLen = 28): string {
17
+ if (!reason) return theme.yellow("Cooldown ativo");
18
+ if (
19
+ reason.startsWith("AuthFailed") ||
20
+ reason.startsWith("AuthPermanentFailure") ||
21
+ reason.includes("All login methods exhausted")
22
+ ) {
23
+ return theme.red(truncate("❌ Senha/Login inválido", maxLen));
24
+ }
25
+ if (reason === "AuthInitFailed") {
26
+ return theme.yellow(truncate("⚠️ Timeout Inicial (WAF/Headers)", maxLen));
27
+ }
28
+ if (reason === "RateLimited" || reason === "QuotaExceeded") {
29
+ return theme.yellow(truncate("⏳ Cota Excedida (Reset 00:00 UTC)", maxLen));
30
+ }
31
+ if (reason === "WafChallenge") {
32
+ return theme.peach(truncate("🛡️ Bloqueio WAF/Anti-Bot", maxLen));
33
+ }
34
+ if (reason.startsWith("StandbyValidationError")) {
35
+ return theme.red(truncate("❌ Falha Validação Standby", maxLen));
36
+ }
37
+ if (reason === "MediaGenFailed") {
38
+ return theme.yellow(truncate("⚠️ Falha Geração de Mídia", maxLen));
39
+ }
40
+ return theme.yellow(truncate(reason, maxLen));
41
+ }
42
+
16
43
  export class AccountsView implements TuiView {
17
44
  public readonly id = "accounts";
18
45
  public readonly title = "Contas";
@@ -662,8 +689,22 @@ export class AccountsView implements TuiView {
662
689
 
663
690
  let status = theme.green(`${glyphs.bullet} Pronto `);
664
691
  if (acc.onCooldown) {
665
- const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
666
- status = theme.yellow(`⚠️ ${mins}m cd `);
692
+ const reason = acc.cooldownReason || "";
693
+ if (
694
+ reason.startsWith("AuthFailed") ||
695
+ reason.startsWith("AuthPermanentFailure") ||
696
+ reason.includes("All login methods exhausted")
697
+ ) {
698
+ status = theme.red(`❌ Auth Fail `);
699
+ } else if (reason === "WafChallenge") {
700
+ status = theme.peach(`🛡️ WAF Block `);
701
+ } else if (reason === "AuthInitFailed") {
702
+ const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
703
+ status = theme.yellow(`⚠️ ${mins}m init `);
704
+ } else {
705
+ const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
706
+ status = theme.yellow(`⚠️ ${mins}m cd `);
707
+ }
667
708
  } else if (!acc.headersReady) {
668
709
  status = acc.isInitialized
669
710
  ? theme.yellow(`◐ Aquecendo...`)
@@ -724,7 +765,13 @@ export class AccountsView implements TuiView {
724
765
  ? theme.yellow(`◐ Aquecendo...`)
725
766
  : theme.muted(`${glyphs.circle} Standby (Sob Demanda)`);
726
767
  rightContent.push(` ${theme.bold("Headers:")} ${hStatus}`);
727
- rightContent.push("");
768
+ if (selected.onCooldown && selected.cooldownReason) {
769
+ const maxReasonW = Math.max(16, rightW - 14);
770
+ const cdReason = formatCooldownReason(selected.cooldownReason, maxReasonW);
771
+ rightContent.push(` ${theme.bold("Motivo:")} ${cdReason}`);
772
+ } else {
773
+ rightContent.push("");
774
+ }
728
775
  rightContent.push(` ${theme.dim("─────────────────────────────────")}`);
729
776
  rightContent.push(` ${this.hoveredActionRow === 15 ? theme.bgHover(` ${theme.cyan("[ A ] Adicionar Conta")} `) : `${theme.cyan("[ A ]")} Adicionar Conta`}`);
730
777
  rightContent.push(` ${this.hoveredActionRow === 16 ? theme.bgHover(` ${theme.red("[ D ] Remover Conta")} `) : `${theme.red("[ D ]")} Remover Conta`}`);
@@ -8,6 +8,7 @@ import { theme, glyphs, drawBox, stringWidth, truncate, stripAnsi, pad, wrapCont
8
8
  import { streamChatCompletions, fetchLiveModels } from "../proxy-client.ts";
9
9
  import { ServerManager } from "../server-manager.ts";
10
10
  import { formatMarkdown, formatReasoning } from "../markdown.ts";
11
+ import { loadTuiSettings, saveTuiSettings } from "../settings.ts";
11
12
 
12
13
  interface ChatMessage {
13
14
  role: "user" | "assistant";
@@ -122,6 +123,21 @@ export class ChatView implements TuiView {
122
123
 
123
124
  constructor(onNeedsRender?: () => void) {
124
125
  this.onNeedsRender = onNeedsRender;
126
+ const saved = loadTuiSettings();
127
+ if (saved.chat?.model) {
128
+ const idx = this.availableModels.indexOf(saved.chat.model);
129
+ if (idx !== -1) {
130
+ this.selectedModelIndex = idx;
131
+ }
132
+ }
133
+ const savedEffort = saved.chat?.effort;
134
+ if (savedEffort && ["high", "medium", "low"].includes(savedEffort)) {
135
+ this.selectedEffort = savedEffort;
136
+ const effIdx = this.availableEfforts.findIndex((e) => e.id === savedEffort);
137
+ if (effIdx !== -1) {
138
+ this.effortSelectedIndex = effIdx;
139
+ }
140
+ }
125
141
  void this.refreshModels();
126
142
  }
127
143
  public onActivate(): void {
@@ -138,7 +154,13 @@ export class ChatView implements TuiView {
138
154
  if (live.length > 0) {
139
155
  const current = this.availableModels[this.selectedModelIndex];
140
156
  this.availableModels = live;
141
- const foundIdx = this.availableModels.indexOf(current);
157
+ let foundIdx = this.availableModels.indexOf(current);
158
+ if (foundIdx === -1) {
159
+ const saved = loadTuiSettings();
160
+ if (saved.chat?.model) {
161
+ foundIdx = this.availableModels.indexOf(saved.chat.model);
162
+ }
163
+ }
142
164
  this.selectedModelIndex = foundIdx !== -1 ? foundIdx : 0;
143
165
  this.onNeedsRender?.();
144
166
  }
@@ -169,7 +191,12 @@ export class ChatView implements TuiView {
169
191
  if (!chosen) return;
170
192
  this.selectedModelIndex = idx;
171
193
  this.isModelModalOpen = false;
172
-
194
+ saveTuiSettings({
195
+ chat: {
196
+ model: chosen,
197
+ effort: this.selectedEffort,
198
+ },
199
+ });
173
200
  const info = classifyModel(chosen);
174
201
  if (info.category === "Texto & Raciocínio") {
175
202
  this.isEffortModalOpen = true;
@@ -249,6 +276,12 @@ export class ChatView implements TuiView {
249
276
  this.selectedEffort = this.availableEfforts[row - 9].id;
250
277
  this.isEffortModalOpen = false;
251
278
  const currentM = this.availableModels[this.selectedModelIndex];
279
+ saveTuiSettings({
280
+ chat: {
281
+ model: currentM,
282
+ effort: this.selectedEffort,
283
+ },
284
+ });
252
285
  this.statusNote = `Modelo: ${currentM} | Effort: ${this.availableEfforts[row - 9].label}`;
253
286
  this.onNeedsRender?.();
254
287
  return true;
@@ -274,6 +307,12 @@ export class ChatView implements TuiView {
274
307
  this.selectedEffort = this.availableEfforts[this.effortSelectedIndex].id;
275
308
  this.isEffortModalOpen = false;
276
309
  const currentM = this.availableModels[this.selectedModelIndex];
310
+ saveTuiSettings({
311
+ chat: {
312
+ model: currentM,
313
+ effort: this.selectedEffort,
314
+ },
315
+ });
277
316
  this.statusNote = `Modelo: ${currentM} | Effort: ${this.availableEfforts[this.effortSelectedIndex].label}`;
278
317
  this.onNeedsRender?.();
279
318
  return true;
@@ -7,6 +7,7 @@ import type { TuiView } from "../types.ts";
7
7
  import type { KeyEvent } from "../screen.ts";
8
8
  import { theme, drawBox, stringWidth, truncate, pad, stripAnsi, setClipboardText } from "../theme.ts";
9
9
  import { ServerManager } from "../server-manager.ts";
10
+ import { loadTuiSettings, saveTuiSettings } from "../settings.ts";
10
11
 
11
12
  export class LogsView implements TuiView {
12
13
  public readonly id = "logs";
@@ -14,6 +15,21 @@ export class LogsView implements TuiView {
14
15
  public readonly tabNumber = 6;
15
16
 
16
17
  private filter: "all" | "warn" | "error" = "all";
18
+
19
+ constructor() {
20
+ const saved = loadTuiSettings();
21
+ if (saved.logs?.filter && ["all", "warn", "error"].includes(saved.logs.filter)) {
22
+ this.filter = saved.logs.filter;
23
+ }
24
+ }
25
+
26
+ private setFilter(newFilter: "all" | "warn" | "error"): void {
27
+ this.filter = newFilter;
28
+ this.scrollOffset = 0;
29
+ this.selectedLogIndex = null;
30
+ saveTuiSettings({ logs: { filter: newFilter } });
31
+ }
32
+
17
33
  private scrollOffset = 0; // 0 = at the bottom (follow newest)
18
34
  private hoveredChip: "all" | "warn" | "error" | "copy" | "clear" | null = null;
19
35
  private selectedLogIndex: number | null = null;
@@ -103,21 +119,15 @@ export class LogsView implements TuiView {
103
119
  for (const c of chips) {
104
120
  if (col >= c.startCol && col <= c.endCol) {
105
121
  if (c.id === "all") {
106
- this.filter = "all";
107
- this.scrollOffset = 0;
108
- this.selectedLogIndex = null;
122
+ this.setFilter("all");
109
123
  return true;
110
124
  }
111
125
  if (c.id === "warn") {
112
- this.filter = "warn";
113
- this.scrollOffset = 0;
114
- this.selectedLogIndex = null;
126
+ this.setFilter("warn");
115
127
  return true;
116
128
  }
117
129
  if (c.id === "error") {
118
- this.filter = "error";
119
- this.scrollOffset = 0;
120
- this.selectedLogIndex = null;
130
+ this.setFilter("error");
121
131
  return true;
122
132
  }
123
133
  if (c.id === "copy") {
@@ -202,21 +212,15 @@ export class LogsView implements TuiView {
202
212
 
203
213
  // Filter toggles
204
214
  if ((key.name === "t" || key.name === "T") && !key.ctrl) {
205
- this.filter = "all";
206
- this.scrollOffset = 0;
207
- this.selectedLogIndex = null;
215
+ this.setFilter("all");
208
216
  return true;
209
217
  }
210
218
  if ((key.name === "w" || key.name === "W") && !key.ctrl) {
211
- this.filter = "warn";
212
- this.scrollOffset = 0;
213
- this.selectedLogIndex = null;
219
+ this.setFilter("warn");
214
220
  return true;
215
221
  }
216
222
  if ((key.name === "e" || key.name === "E") && !key.ctrl) {
217
- this.filter = "error";
218
- this.scrollOffset = 0;
219
- this.selectedLogIndex = null;
223
+ this.setFilter("error");
220
224
  return true;
221
225
  }
222
226
 
@@ -8,6 +8,48 @@ 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
 
11
+ export function renderProgressBar(
12
+ pct: number,
13
+ width = 8,
14
+ colorFn: (s: string) => string = theme.cyan,
15
+ ): string {
16
+ const safePct = Math.max(0, Math.min(100, isNaN(pct) ? 0 : pct));
17
+ const rawFilled = Math.round((safePct / 100) * width);
18
+ const filled = safePct > 0.05 ? Math.max(1, rawFilled) : 0;
19
+ const empty = Math.max(0, width - filled);
20
+ return colorFn("█".repeat(filled)) + theme.muted("░".repeat(empty));
21
+ }
22
+ export function getAccountReadinessScore(acc: {
23
+ onCooldown: boolean;
24
+ headersReady: boolean;
25
+ activeStreams?: number;
26
+ isInitialized?: boolean;
27
+ cooldownReason?: string | null;
28
+ }): number {
29
+ if (acc.onCooldown) {
30
+ const reason = acc.cooldownReason || "";
31
+ if (
32
+ reason.startsWith("AuthFailed") ||
33
+ reason.startsWith("AuthPermanentFailure") ||
34
+ reason.includes("login methods exhausted")
35
+ ) {
36
+ return 0; // Auth Fail at the very bottom
37
+ }
38
+ return 10; // Other cooldowns (RateLimited, WafChallenge, etc.)
39
+ }
40
+ if (acc.activeStreams && acc.activeStreams > 0 && acc.headersReady) {
41
+ return 100; // Actively generating code
42
+ }
43
+ if (acc.headersReady) {
44
+ return 80; // Ready / Warm
45
+ }
46
+ if (acc.isInitialized) {
47
+ return 60; // Warming up
48
+ }
49
+ return 40; // Healthy standby
50
+ }
51
+
52
+
11
53
  export class StatusView implements TuiView {
12
54
  public readonly id = "status";
13
55
  public readonly title = "Status";
@@ -17,8 +59,9 @@ export class StatusView implements TuiView {
17
59
  private actionMessage = "";
18
60
  private actionMessageTimeout: NodeJS.Timeout | null = null;
19
61
  private hoveredActionRow: number | null = null;
20
- private lastLeftW = 34;
21
-
62
+ private lastLeftW = 38;
63
+ private lastActionRecarregarRow = 20;
64
+ private lastActionZerarRow = 21;
22
65
  constructor() {
23
66
  this.refresh();
24
67
  }
@@ -58,8 +101,12 @@ export class StatusView implements TuiView {
58
101
  // Mouse hover over quick actions
59
102
  if (key.name === "hover" && key.mouse) {
60
103
  const { row, col } = key.mouse;
61
- const leftW = this.lastLeftW || 34;
62
- if (col >= 2 && col <= leftW - 1 && (row === 13 || row === 14)) {
104
+ const leftW = this.lastLeftW || 38;
105
+ if (
106
+ col >= 2 &&
107
+ col <= leftW - 1 &&
108
+ (row === this.lastActionRecarregarRow || row === this.lastActionZerarRow)
109
+ ) {
63
110
  if (this.hoveredActionRow !== row) {
64
111
  this.hoveredActionRow = row;
65
112
  return true;
@@ -73,14 +120,14 @@ export class StatusView implements TuiView {
73
120
  // Mouse click interactions
74
121
  if (key.name === "click" && key.mouse) {
75
122
  const { row, col } = key.mouse;
76
- const leftW = this.lastLeftW || 34;
123
+ const leftW = this.lastLeftW || 38;
77
124
  if (col >= 2 && col <= leftW - 1) {
78
- if (row === 13) {
125
+ if (row === this.lastActionRecarregarRow) {
79
126
  await this.refresh();
80
127
  this.setMessage(theme.green("✓ Status atualizado"));
81
128
  return true;
82
129
  }
83
- if (row === 14) {
130
+ if (row === this.lastActionZerarRow) {
84
131
  const cleared = resetAllCooldowns();
85
132
  await this.refresh();
86
133
  this.setMessage(theme.green(`✓ Cooldowns zerados: ${cleared} conta(s) liberada(s)`));
@@ -108,12 +155,13 @@ export class StatusView implements TuiView {
108
155
  const isOnline = data?.online ?? false;
109
156
  const contentH = Math.max(10, height);
110
157
 
111
- // Two-column layout
112
- const leftW = Math.max(34, Math.floor(width * 0.42));
158
+ // Two-column layout: give left box 48-52 cols so rich metrics never truncate,
159
+ // and right box takes the remaining width (at least 38 cols).
160
+ const leftW = width >= 96
161
+ ? Math.min(52, Math.max(48, Math.floor(width * 0.48)))
162
+ : Math.min(46, Math.max(38, Math.floor(width * 0.50)));
113
163
  this.lastLeftW = leftW;
114
- const rightW = Math.max(34, width - leftW - 1);
115
-
116
- // Left Column: System & Proxy Status
164
+ const rightW = Math.max(36, width - leftW - 1);
117
165
  const serverState = ServerManager.getInstance().getState();
118
166
  let onlineBadge: string;
119
167
  if (isOnline || serverState === "online") {
@@ -128,67 +176,158 @@ export class StatusView implements TuiView {
128
176
 
129
177
  const uptimeSecs = data?.uptimeSeconds || Math.floor(process.uptime());
130
178
  const uptimeStr = formatUptime(uptimeSecs);
131
-
132
179
  const baseUrl = `http://${data?.host || "127.0.0.1"}:${data?.port || 7936}/v1`;
133
180
 
134
- const leftContent = [
135
- "",
136
- ` ${theme.bold("Status:")} ${onlineBadge}`,
137
- ` ${theme.bold("Base URL:")} ${theme.cyan(baseUrl)}`,
138
- ` ${theme.bold("Uptime:")} ${theme.cyan(uptimeStr)}`,
139
- ` ${theme.bold("RAM:")} ${theme.cyan(String(data?.rssMb || 0) + " MB")}`,
140
- ` ${theme.bold("Conexões:")} ${data?.activeStreams ? theme.yellow(String(data.activeStreams) + " ativas") : "0 ativas"}`,
141
- "",
181
+ const m = data?.metrics;
182
+ const reqsTotal = m?.requestsTotal ?? 0;
183
+ const reqsErrors = m?.requestsErrors ?? 0;
184
+ const successPct = m?.successRate ?? (reqsTotal > 0 ? Number((((reqsTotal - reqsErrors) / reqsTotal) * 100).toFixed(1)) : 100);
185
+ const latencyAvg = m?.latencyAvgMs ? `${m.latencyAvgMs}ms` : "–";
186
+ const deltasCount = m?.deltasCount ?? 0;
187
+ const fullCount = m?.fullReplaysCount ?? 0;
188
+ const deltaRatio = m?.deltaRatio != null ? `${m.deltaRatio}%` : "";
189
+ const toolCalls = m?.toolCallsCount ?? 0;
190
+ const toolRecovered = m?.toolCallsRecovered ?? 0;
191
+ const captchasDetected = m?.captchasDetected ?? 0;
192
+ const captchasSolved = m?.captchasSolved ?? 0;
193
+ const chatsCleaned = m?.chatsCleaned ?? 0;
194
+ const lbl = (s: string) => pad(s, 13);
195
+ const innerLeftW = Math.max(30, leftW - 2);
196
+ const deltaDetail = innerLeftW < 44
197
+ ? `(${deltasCount}d / ${fullCount}f)`
198
+ : `(${deltasCount} delta / ${fullCount} full)`;
199
+ const ramPct = data?.systemMemoryPct || 0;
200
+ const ramBarColor = ramPct >= 80 ? theme.red : ramPct >= 60 ? theme.yellow : theme.cyan;
201
+ const ramBar = renderProgressBar(ramPct, 7, ramBarColor);
202
+ const ramStr = `${data?.rssMb || 0} MB [${ramBar}] ${ramPct}%`;
203
+
204
+ let connsStr = "0 ativas";
205
+ if (data?.activeStreams && data.activeStreams > 0) {
206
+ connsStr = theme.yellow(`⚡ ${data.activeStreams} ativa(s)`);
207
+ } else {
208
+ connsStr = theme.dim("0 ativas");
209
+ }
210
+ if (data?.waitingStreams && data.waitingStreams > 0) {
211
+ connsStr += theme.peach(` (${data.waitingStreams} na fila)`);
212
+ }
213
+
214
+ const leftContent: string[] = [
215
+ ` ${theme.bold(lbl("Status:"))} ${onlineBadge}`,
216
+ ` ${theme.bold(lbl("Base URL:"))} ${theme.cyan(baseUrl)}`,
217
+ ` ${theme.bold(lbl("Uptime:"))} ${theme.cyan(uptimeStr)}`,
218
+ ` ${theme.bold(lbl("Memória:"))} ${theme.cyan(ramStr)}`,
219
+ ` ${theme.bold(lbl("Conexões:"))} ${connsStr}`,
220
+ ` ${theme.dim("───────────────────────────────────────")}`,
221
+ ` ${theme.bold("Tráfego & Performance:")}`,
222
+ ` ${theme.dim(lbl("Requisições:"))} ${theme.cyan(String(reqsTotal))} ${theme.green(`(${successPct}% ok)`)} · ${reqsErrors > 0 ? theme.red(`${reqsErrors} err`) : theme.dim("0 err")}`,
223
+ ` ${theme.dim(lbl("Latência:"))} ${theme.yellow(latencyAvg)} méd`,
224
+ ` ${theme.dim(lbl("Deltas:"))} ${theme.green(deltaRatio)} ${theme.dim(deltaDetail)}`,
225
+ ` ${theme.dim("───────────────────────────────────────")}`,
226
+ ` ${theme.bold("Agentes & Operações:")}`,
227
+ ` ${theme.dim(lbl("Tool Calls:"))} ${theme.cyan(String(toolCalls))} ${toolRecovered > 0 ? theme.green(`(${toolRecovered} curadas)`) : ""}`,
228
+ ` ${theme.dim(lbl("Captchas:"))} ${captchasSolved > 0 ? theme.green(`${captchasSolved}/${captchasDetected} resolvidos`) : theme.dim(`${captchasDetected} detectados`)}`,
229
+ ` ${theme.dim(lbl("Chats Limpos:"))} ${theme.cyan(String(chatsCleaned))} ${theme.dim("excluídos (>24h)")}`,
230
+ ` ${theme.dim("───────────────────────────────────────")}`,
142
231
  ` ${theme.bold("Ações:")}`,
143
- ` ${this.hoveredActionRow === 13 ? theme.bgHover(` ${theme.cyan("[ R ] Recarregar")} `) : `${theme.cyan("[ R ]")} Recarregar`}`,
144
- ` ${this.hoveredActionRow === 14 ? theme.bgHover(` ${theme.yellow("[ Z ] Zerar Cooldowns")} `) : `${theme.yellow("[ Z ]")} Zerar Cooldowns`}`,
145
- "",
146
- this.actionMessage ? ` ${this.actionMessage}` : "",
147
232
  ];
148
233
 
234
+ const recarregarIdx = leftContent.length;
235
+ const zerarIdx = leftContent.length + 1;
236
+ this.lastActionRecarregarRow = 5 + recarregarIdx;
237
+ this.lastActionZerarRow = 5 + zerarIdx;
238
+
239
+ leftContent.push(
240
+ ` ${this.hoveredActionRow === this.lastActionRecarregarRow ? theme.bgHover(` ${theme.cyan("[ R ] Recarregar")} `) : ` ${theme.cyan("[ R ]")} Recarregar`}`,
241
+ ` ${this.hoveredActionRow === this.lastActionZerarRow ? theme.bgHover(` ${theme.yellow("[ Z ] Zerar Cooldowns")} `) : ` ${theme.yellow("[ Z ]")} Zerar Cooldowns`}`,
242
+ );
243
+
244
+ const boxHeight = Math.max(contentH, leftContent.length + 2);
245
+
149
246
  const leftBox = drawBox({
150
- title: "Sistema",
247
+ title: "Sistema & Performance",
151
248
  width: leftW,
152
- height: contentH,
249
+ height: boxHeight,
153
250
  borderColor: theme.borderInactive,
154
251
  titleColor: theme.cyan,
252
+ footer: this.actionMessage || undefined,
155
253
  content: leftContent,
156
254
  });
157
-
158
255
  // Right Column: Accounts Pool Status
159
- const accounts = data?.accounts || [];
256
+ const rawAccounts = data?.accounts || [];
257
+ const accounts = [...rawAccounts].sort((a, b) => {
258
+ const scoreA = getAccountReadinessScore(a);
259
+ const scoreB = getAccountReadinessScore(b);
260
+ if (scoreB !== scoreA) {
261
+ return scoreB - scoreA;
262
+ }
263
+ return 0;
264
+ });
265
+ const availableCount = accounts.filter((a) => !a.onCooldown).length;
160
266
  const readyCount = accounts.filter((a) => !a.onCooldown && a.headersReady).length;
267
+ const standbyCount = accounts.filter((a) => !a.onCooldown && !a.headersReady).length;
268
+ const cooldownCount = accounts.filter((a) => a.onCooldown).length;
269
+ const poolPct = accounts.length > 0 ? Math.round((availableCount / accounts.length) * 100) : 0;
270
+ const poolColor = poolPct >= 70 ? theme.green : poolPct >= 40 ? theme.yellow : theme.red;
271
+ const poolBar = renderProgressBar(poolPct, 8, poolColor);
272
+ const innerRightW = Math.max(30, rightW - 2);
273
+ const emailWidth = Math.max(16, Math.min(20, innerRightW - 27));
161
274
  const rightContent: string[] = [
162
- "",
163
- ` ${theme.dim("# Conta Status")}`,
164
- ` ${theme.dim("───────────────────────────────────────")}`,
275
+ ` ${theme.bold("Disponibilidade:")} [${poolBar}] ${poolColor(`${availableCount}/${accounts.length} (${poolPct}%)`)}`,
276
+ ` ${theme.dim("".repeat(Math.max(32, innerRightW - 2)))}`,
277
+ ` ${theme.dim(`# ${pad("Conta", emailWidth)} Carga Status`)}`,
278
+ ` ${theme.dim("─".repeat(Math.max(32, innerRightW - 2)))}`,
165
279
  ];
166
280
 
167
281
  if (accounts.length === 0) {
168
282
  rightContent.push(` ${theme.muted("Nenhuma conta adicionada. (Vá em [5] Contas)")}`);
169
283
  } else {
170
- accounts.slice(0, contentH - 5).forEach((acc, idx) => {
171
- const num = pad(String(idx + 1), 3);
172
- const name = pad(truncate(acc.emailOrName, 20), 20);
284
+ const maxVisibleAccounts = Math.max(6, boxHeight - 7);
285
+ accounts.slice(0, maxVisibleAccounts).forEach((acc, idx) => {
286
+ const num = pad(String(idx + 1) + ".", 4);
287
+ const name = pad(truncate(acc.emailOrName, emailWidth - 1), emailWidth);
288
+ const active = acc.activeStreams || 0;
289
+ const limit = acc.streamLimit || 1;
290
+ const loadBadge = active > 0 ? theme.yellow(`[${active}/${limit}]`) : theme.dim(`[0/${limit}]`);
291
+
173
292
  let status = theme.green(`${glyphs.bullet} Pronto`);
174
- if (acc.onCooldown) {
175
- const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
176
- status = theme.yellow(`⚠️ Cooldown ${mins}m`);
293
+ if (active > 0 && !acc.onCooldown && acc.headersReady) {
294
+ status = theme.yellow(`● Gerando `);
295
+ } else if (acc.onCooldown) {
296
+ const reason = acc.cooldownReason || "";
297
+ if (
298
+ reason.startsWith("AuthFailed") ||
299
+ reason.startsWith("AuthPermanentFailure") ||
300
+ reason.includes("login methods exhausted")
301
+ ) {
302
+ status = theme.red(`❌ Auth Fail`);
303
+ } else if (reason === "WafChallenge") {
304
+ status = theme.peach(`🛡️ WAF Block`);
305
+ } else {
306
+ const mins = Math.max(1, Math.round(acc.remainingCooldownMs / 60000));
307
+ status = theme.yellow(`⚠️ ${mins}m cd`);
308
+ }
177
309
  } else if (!acc.headersReady) {
178
310
  status = acc.isInitialized
179
311
  ? theme.yellow(`◐ Aquecendo...`)
180
312
  : theme.muted(`○ Standby`);
181
313
  }
182
- rightContent.push(` ${num} ${name} ${status}`);
314
+ rightContent.push(` ${num}${name} ${loadBadge} ${status}`);
183
315
  });
184
316
  }
185
317
 
318
+ const summaryParts: string[] = [];
319
+ if (readyCount > 0) summaryParts.push(`${readyCount} warm`);
320
+ if (standbyCount > 0) summaryParts.push(`${standbyCount} standby`);
321
+ if (cooldownCount > 0) summaryParts.push(`${cooldownCount} cd`);
322
+ const rightFooter = summaryParts.length > 0 ? `💡 ${summaryParts.join(" · ")} · [5] Contas` : undefined;
323
+
186
324
  const rightBox = drawBox({
187
- title: `Contas (${readyCount}/${accounts.length})`,
325
+ title: `Contas Pool (${availableCount}/${accounts.length})`,
188
326
  width: rightW,
189
- height: contentH,
327
+ height: boxHeight,
190
328
  borderColor: theme.borderInactive,
191
329
  titleColor: theme.lavender,
330
+ footer: rightFooter,
192
331
  content: rightContent,
193
332
  });
194
333