qwenproxy-cli 1.2.10 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,11 +4,12 @@
4
4
 
5
5
  import type { TuiView, ProxyStatusSnapshot } from "../types.ts";
6
6
  import type { KeyEvent } from "../screen.ts";
7
- import { theme, glyphs, drawBox, pad, truncate, setClipboardText } from "../theme.ts";
7
+ import { theme, glyphs, drawBox, pad, truncate, setClipboardText, getClipboardText } from "../theme.ts";
8
8
  import { fetchProxyStatus, resetAllCooldowns, formatUptime } from "../proxy-client.ts";
9
9
  import { ServerManager } from "../server-manager.ts";
10
- import { getRuntimeChatMode, cycleNextChatMode } from "../../core/config.ts";
10
+ import { getRuntimeChatMode, cycleNextChatMode, config } from "../../core/config.ts";
11
11
  import { saveTuiSettings } from "../settings.ts";
12
+ import { persistServerPort, persistCustomApiKey } from "../../core/local-auth.ts";
12
13
 
13
14
  export function renderProgressBar(
14
15
  pct: number,
@@ -72,6 +73,24 @@ export class StatusView implements TuiView {
72
73
  private lastActionZerarRow = 21;
73
74
  private lastActionModoRow = 22;
74
75
  private lastActionCopiarRow = 23;
76
+ private lastActionPortRow = 24;
77
+ private lastActionApiKeyRow = 25;
78
+
79
+ public isPortModalOpen = false;
80
+ public portInput = "";
81
+ public portCursor = 0;
82
+ public portError = "";
83
+ public portModalHoveredBtn: "save" | "cancel" | null = null;
84
+ public lastPortModalBtnRow = 0;
85
+ public lastPortModalLeftPad = 0;
86
+
87
+ public isApiKeyModalOpen = false;
88
+ public apiKeyInput = "";
89
+ public apiKeyCursor = 0;
90
+ public apiKeyError = "";
91
+ public apiKeyModalHoveredBtn: "save" | "cancel" | null = null;
92
+ public lastApiKeyModalBtnRow = 0;
93
+ public lastApiKeyModalLeftPad = 0;
75
94
 
76
95
  constructor() {
77
96
  this.refresh();
@@ -92,12 +111,31 @@ export class StatusView implements TuiView {
92
111
  this.refresh();
93
112
  }
94
113
 
114
+ public isCapturingText(): boolean {
115
+ return this.isPortModalOpen || this.isApiKeyModalOpen;
116
+ }
117
+
95
118
  public getShortcuts(): Array<{ key: string; label: string }> {
119
+ if (this.isPortModalOpen) {
120
+ return [
121
+ { key: "Enter", label: "Salvar Porta" },
122
+ { key: "Esc", label: "Cancelar" },
123
+ ];
124
+ }
125
+ if (this.isApiKeyModalOpen) {
126
+ return [
127
+ { key: "Enter", label: "Salvar API Key" },
128
+ { key: "Ctrl+V", label: "Colar" },
129
+ { key: "Esc", label: "Cancelar" },
130
+ ];
131
+ }
96
132
  return [
97
133
  { key: "r", label: "Recarregar" },
98
134
  { key: "z", label: "Zerar Cooldowns" },
99
135
  { key: "m", label: "Alternar Modo" },
100
136
  { key: "c", label: "Copiar URL" },
137
+ { key: "p", label: "Alterar Porta" },
138
+ { key: "k", label: "Alterar API Key" },
101
139
  ];
102
140
  }
103
141
 
@@ -109,7 +147,276 @@ export class StatusView implements TuiView {
109
147
  }, 4000);
110
148
  }
111
149
 
150
+ private async savePortFromModal(): Promise<boolean> {
151
+ const val = this.portInput.trim();
152
+ const p = parseInt(val, 10);
153
+ if (isNaN(p) || p < 1 || p > 65535) {
154
+ this.portError = "Porta inválida (deve ser entre 1 e 65535)";
155
+ return false;
156
+ }
157
+ persistServerPort(p);
158
+ if (config.server) {
159
+ config.server.port = p;
160
+ }
161
+ this.isPortModalOpen = false;
162
+ this.portError = "";
163
+ this.portModalHoveredBtn = null;
164
+ this.setMessage(theme.green(`✓ Porta alterada para ${p} (salva no .env)`));
165
+ await this.refresh();
166
+ const sManager = ServerManager.getInstance();
167
+ if (sManager.getState() === "online") {
168
+ void sManager.restart();
169
+ }
170
+ return true;
171
+ }
172
+
173
+ private async saveApiKeyFromModal(): Promise<boolean> {
174
+ const k = this.apiKeyInput.trim();
175
+ persistCustomApiKey(k);
176
+ this.isApiKeyModalOpen = false;
177
+ this.apiKeyError = "";
178
+ this.apiKeyModalHoveredBtn = null;
179
+ this.setMessage(theme.green(`✓ API Key salva no .env com sucesso`));
180
+ await this.refresh();
181
+ return true;
182
+ }
183
+
112
184
  public async handleKey(key: KeyEvent): Promise<boolean | void> {
185
+ // 1. Port Modal key & mouse handling
186
+ if (this.isPortModalOpen) {
187
+ // Mouse hover in Port Modal
188
+ if (key.name === "hover" && key.mouse) {
189
+ const { row, col } = key.mouse;
190
+ if (row === this.lastPortModalBtnRow) {
191
+ const relCol = col - this.lastPortModalLeftPad;
192
+ let btn: "save" | "cancel" | null = null;
193
+ if (relCol >= 2 && relCol <= 28) {
194
+ btn = "save";
195
+ } else if (relCol >= 29 && relCol <= 48) {
196
+ btn = "cancel";
197
+ }
198
+ if (this.portModalHoveredBtn !== btn) {
199
+ this.portModalHoveredBtn = btn;
200
+ return true;
201
+ }
202
+ return false;
203
+ }
204
+ if (this.portModalHoveredBtn !== null) {
205
+ this.portModalHoveredBtn = null;
206
+ return true;
207
+ }
208
+ return false;
209
+ }
210
+
211
+ // Mouse click in Port Modal
212
+ if (key.name === "click" && key.mouse) {
213
+ const { row, col } = key.mouse;
214
+ if (row === this.lastPortModalBtnRow) {
215
+ const relCol = col - this.lastPortModalLeftPad;
216
+ if (relCol >= 2 && relCol <= 28) {
217
+ await this.savePortFromModal();
218
+ return true;
219
+ }
220
+ if (relCol >= 29 && relCol <= 48) {
221
+ this.isPortModalOpen = false;
222
+ this.portError = "";
223
+ this.portModalHoveredBtn = null;
224
+ return true;
225
+ }
226
+ }
227
+ return true;
228
+ }
229
+
230
+ if (key.name === "escape") {
231
+ this.isPortModalOpen = false;
232
+ this.portError = "";
233
+ this.portModalHoveredBtn = null;
234
+ return true;
235
+ }
236
+ if (key.name === "enter") {
237
+ await this.savePortFromModal();
238
+ return true;
239
+ }
240
+ if (key.name === "backspace") {
241
+ const cur = Math.max(0, Math.min(this.portInput.length, this.portCursor));
242
+ if (cur > 0) {
243
+ this.portInput =
244
+ this.portInput.slice(0, cur - 1) +
245
+ this.portInput.slice(cur);
246
+ this.portCursor = cur - 1;
247
+ this.portError = "";
248
+ }
249
+ return true;
250
+ }
251
+ if (key.name === "delete") {
252
+ const cur = Math.max(0, Math.min(this.portInput.length, this.portCursor));
253
+ if (cur < this.portInput.length) {
254
+ this.portInput =
255
+ this.portInput.slice(0, cur) +
256
+ this.portInput.slice(cur + 1);
257
+ this.portError = "";
258
+ }
259
+ return true;
260
+ }
261
+ if (key.name === "left") {
262
+ this.portCursor = Math.max(0, this.portCursor - 1);
263
+ return true;
264
+ }
265
+ if (key.name === "right") {
266
+ this.portCursor = Math.min(this.portInput.length, this.portCursor + 1);
267
+ return true;
268
+ }
269
+ if (key.name === "home") {
270
+ this.portCursor = 0;
271
+ return true;
272
+ }
273
+ if (key.name === "end") {
274
+ this.portCursor = this.portInput.length;
275
+ return true;
276
+ }
277
+ // Numeric input
278
+ const ch = key.char || (key.name.length === 1 && /^\d$/.test(key.name) ? key.name : "");
279
+ if (ch && /^\d$/.test(ch) && !key.ctrl && !key.meta) {
280
+ if (this.portInput.length < 5) {
281
+ const cur = Math.max(0, Math.min(this.portInput.length, this.portCursor));
282
+ this.portInput =
283
+ this.portInput.slice(0, cur) +
284
+ ch +
285
+ this.portInput.slice(cur);
286
+ this.portCursor = cur + ch.length;
287
+ this.portError = "";
288
+ }
289
+ return true;
290
+ }
291
+ return true; // Swallow all keys in modal
292
+ }
293
+
294
+ // 2. API Key Modal key & mouse handling
295
+ if (this.isApiKeyModalOpen) {
296
+ // Mouse hover in API Key Modal
297
+ if (key.name === "hover" && key.mouse) {
298
+ const { row, col } = key.mouse;
299
+ if (row === this.lastApiKeyModalBtnRow) {
300
+ const relCol = col - this.lastApiKeyModalLeftPad;
301
+ let btn: "save" | "cancel" | null = null;
302
+ if (relCol >= 2 && relCol <= 30) {
303
+ btn = "save";
304
+ } else if (relCol >= 31 && relCol <= 50) {
305
+ btn = "cancel";
306
+ }
307
+ if (this.apiKeyModalHoveredBtn !== btn) {
308
+ this.apiKeyModalHoveredBtn = btn;
309
+ return true;
310
+ }
311
+ return false;
312
+ }
313
+ if (this.apiKeyModalHoveredBtn !== null) {
314
+ this.apiKeyModalHoveredBtn = null;
315
+ return true;
316
+ }
317
+ return false;
318
+ }
319
+
320
+ // Mouse click in API Key Modal
321
+ if (key.name === "click" && key.mouse) {
322
+ const { row, col } = key.mouse;
323
+ if (row === this.lastApiKeyModalBtnRow) {
324
+ const relCol = col - this.lastApiKeyModalLeftPad;
325
+ if (relCol >= 2 && relCol <= 30) {
326
+ await this.saveApiKeyFromModal();
327
+ return true;
328
+ }
329
+ if (relCol >= 31 && relCol <= 50) {
330
+ this.isApiKeyModalOpen = false;
331
+ this.apiKeyError = "";
332
+ this.apiKeyModalHoveredBtn = null;
333
+ return true;
334
+ }
335
+ }
336
+ return true;
337
+ }
338
+
339
+ if (key.name === "escape") {
340
+ this.isApiKeyModalOpen = false;
341
+ this.apiKeyError = "";
342
+ this.apiKeyModalHoveredBtn = null;
343
+ return true;
344
+ }
345
+ if (key.name === "enter") {
346
+ await this.saveApiKeyFromModal();
347
+ return true;
348
+ }
349
+ if (key.ctrl && (key.name === "v" || key.raw === "\x16")) {
350
+ const pasted = getClipboardText();
351
+ if (pasted) {
352
+ const clean = pasted.replace(/[\r\n]/g, "").trim();
353
+ const cur = Math.max(0, Math.min(this.apiKeyInput.length, this.apiKeyCursor));
354
+ this.apiKeyInput =
355
+ this.apiKeyInput.slice(0, cur) +
356
+ clean +
357
+ this.apiKeyInput.slice(cur);
358
+ this.apiKeyCursor = cur + clean.length;
359
+ this.apiKeyError = "";
360
+ }
361
+ return true;
362
+ }
363
+ if (key.ctrl && key.name === "c") {
364
+ this.apiKeyInput = "";
365
+ this.apiKeyCursor = 0;
366
+ return true;
367
+ }
368
+ if (key.name === "backspace") {
369
+ const cur = Math.max(0, Math.min(this.apiKeyInput.length, this.apiKeyCursor));
370
+ if (cur > 0) {
371
+ this.apiKeyInput =
372
+ this.apiKeyInput.slice(0, cur - 1) +
373
+ this.apiKeyInput.slice(cur);
374
+ this.apiKeyCursor = cur - 1;
375
+ this.apiKeyError = "";
376
+ }
377
+ return true;
378
+ }
379
+ if (key.name === "delete") {
380
+ const cur = Math.max(0, Math.min(this.apiKeyInput.length, this.apiKeyCursor));
381
+ if (cur < this.apiKeyInput.length) {
382
+ this.apiKeyInput =
383
+ this.apiKeyInput.slice(0, cur) +
384
+ this.apiKeyInput.slice(cur + 1);
385
+ this.apiKeyError = "";
386
+ }
387
+ return true;
388
+ }
389
+ if (key.name === "left") {
390
+ this.apiKeyCursor = Math.max(0, this.apiKeyCursor - 1);
391
+ return true;
392
+ }
393
+ if (key.name === "right") {
394
+ this.apiKeyCursor = Math.min(this.apiKeyInput.length, this.apiKeyCursor + 1);
395
+ return true;
396
+ }
397
+ if (key.name === "home") {
398
+ this.apiKeyCursor = 0;
399
+ return true;
400
+ }
401
+ if (key.name === "end") {
402
+ this.apiKeyCursor = this.apiKeyInput.length;
403
+ return true;
404
+ }
405
+ // Text character input
406
+ const ch = key.char || (key.name.length === 1 && !key.ctrl && !key.meta ? key.name : "");
407
+ if (ch && ch.length === 1 && ch >= " " && !key.ctrl && !key.meta) {
408
+ const cur = Math.max(0, Math.min(this.apiKeyInput.length, this.apiKeyCursor));
409
+ this.apiKeyInput =
410
+ this.apiKeyInput.slice(0, cur) +
411
+ ch +
412
+ this.apiKeyInput.slice(cur);
413
+ this.apiKeyCursor = cur + ch.length;
414
+ this.apiKeyError = "";
415
+ return true;
416
+ }
417
+ return true; // Swallow all keys in modal
418
+ }
419
+
113
420
  // Mouse hover over quick actions or Base URL
114
421
  if (key.name === "hover" && key.mouse) {
115
422
  const { row, col } = key.mouse;
@@ -121,7 +428,9 @@ export class StatusView implements TuiView {
121
428
  (row === this.lastActionRecarregarRow ||
122
429
  row === this.lastActionZerarRow ||
123
430
  row === this.lastActionModoRow ||
124
- row === this.lastActionCopiarRow);
431
+ row === this.lastActionCopiarRow ||
432
+ row === this.lastActionPortRow ||
433
+ row === this.lastActionApiKeyRow);
125
434
 
126
435
  let changed = false;
127
436
  if (isOverBaseUrl !== this.isBaseUrlHovered) {
@@ -174,6 +483,22 @@ export class StatusView implements TuiView {
174
483
  this.setMessage(theme.green(`✓ Modo global da API: ${nextMode}`));
175
484
  return true;
176
485
  }
486
+ if (row === this.lastActionPortRow) {
487
+ this.isPortModalOpen = true;
488
+ this.isApiKeyModalOpen = false;
489
+ this.portInput = String(this.statusData?.port || config.server?.port || 7936);
490
+ this.portCursor = this.portInput.length;
491
+ this.portError = "";
492
+ return true;
493
+ }
494
+ if (row === this.lastActionApiKeyRow) {
495
+ this.isApiKeyModalOpen = true;
496
+ this.isPortModalOpen = false;
497
+ this.apiKeyInput = (process.env.API_KEY || "").trim();
498
+ this.apiKeyCursor = this.apiKeyInput.length;
499
+ this.apiKeyError = "";
500
+ return true;
501
+ }
177
502
  }
178
503
  }
179
504
  if ((key.name === "r" || key.name === "R") && !key.ctrl) {
@@ -208,12 +533,174 @@ export class StatusView implements TuiView {
208
533
  this.setMessage(theme.green(`✓ Base URL copiada: ${this.lastBaseUrl}`));
209
534
  return true;
210
535
  }
536
+
537
+ if ((key.name === "p" || key.name === "P") && !key.ctrl) {
538
+ this.isPortModalOpen = true;
539
+ this.isApiKeyModalOpen = false;
540
+ this.portInput = String(this.statusData?.port || config.server?.port || 7936);
541
+ this.portCursor = this.portInput.length;
542
+ this.portError = "";
543
+ return true;
544
+ }
545
+
546
+ if ((key.name === "k" || key.name === "K") && !key.ctrl) {
547
+ this.isApiKeyModalOpen = true;
548
+ this.isPortModalOpen = false;
549
+ this.apiKeyInput = (process.env.API_KEY || "").trim();
550
+ this.apiKeyCursor = this.apiKeyInput.length;
551
+ this.apiKeyError = "";
552
+ return true;
553
+ }
211
554
  }
212
555
  public render(width: number, height: number, snapshot?: ProxyStatusSnapshot | null): string[] {
213
556
  const data = snapshot || this.statusData;
214
557
  const isOnline = data?.online ?? false;
215
558
  const contentH = Math.max(10, height);
216
559
 
560
+ if (this.isPortModalOpen) {
561
+ const modalW = Math.min(width - 4, 62);
562
+ const padCount = Math.max(0, Math.floor((width - modalW) / 2));
563
+ const padStr = " ".repeat(padCount);
564
+
565
+ const safeCursor = Math.max(0, Math.min(this.portInput.length, this.portCursor));
566
+ let inputDisplay: string;
567
+ if (this.portInput.length === 0) {
568
+ inputDisplay = `${theme.inverse(" ")} ${theme.dim("(ex: 7936)")}`;
569
+ } else {
570
+ const before = this.portInput.slice(0, safeCursor);
571
+ const at = this.portInput[safeCursor] || " ";
572
+ const after = this.portInput.slice(safeCursor + 1);
573
+ inputDisplay = `${theme.cyan(before)}${theme.inverse(at)}${theme.cyan(after)}`;
574
+ }
575
+
576
+ const modalContent: string[] = [
577
+ "",
578
+ ` ${theme.bold("Alterar Porta do Servidor:")}`,
579
+ ` ${theme.dim("Digite a nova porta para o servidor QwenProxy (1-65535):")}`,
580
+ ` ${theme.dim("────────────────────────────────────────────────────────")}`,
581
+ ` Porta: [ ${inputDisplay} ]`,
582
+ ` ${theme.dim("────────────────────────────────────────────────────────")}`,
583
+ ];
584
+
585
+ if (this.portError) {
586
+ modalContent.push(` ${theme.red(`❌ ${this.portError}`)}`);
587
+ } else {
588
+ modalContent.push(` ${theme.dim("Porta padrão: 7936 (mnemônico Q-W-E-N no teclado)")}`);
589
+ }
590
+
591
+ const isSaveHovered = this.portModalHoveredBtn === "save";
592
+ const isCancelHovered = this.portModalHoveredBtn === "cancel";
593
+
594
+ const saveLabel = " [ Enter ] Salvar Porta ";
595
+ const cancelLabel = " [ Esc ] Cancelar ";
596
+
597
+ const saveBtn = isSaveHovered
598
+ ? theme.bgHover(theme.bold(theme.green(saveLabel)))
599
+ : theme.green(saveLabel);
600
+
601
+ const cancelBtn = isCancelHovered
602
+ ? theme.bgHover(theme.bold(theme.red(cancelLabel)))
603
+ : theme.muted(cancelLabel);
604
+
605
+ modalContent.push("");
606
+ const btnContentIdx = modalContent.length;
607
+ modalContent.push(
608
+ ` ${saveBtn} ${cancelBtn}`,
609
+ "",
610
+ ` ${theme.dim("Nota: Salva no arquivo .env e reinicia o servidor se estiver ativo.")}`,
611
+ );
612
+
613
+ const modalBox = drawBox({
614
+ title: "Alterar Porta do Servidor",
615
+ width: modalW,
616
+ height: Math.min(contentH, modalContent.length + 2),
617
+ borderColor: theme.borderActive,
618
+ titleColor: theme.cyan,
619
+ content: modalContent,
620
+ });
621
+
622
+ const topPadCount = Math.max(1, Math.floor((contentH - modalBox.length) / 3));
623
+ this.lastPortModalBtnRow = 5 + topPadCount + btnContentIdx;
624
+ this.lastPortModalLeftPad = padCount;
625
+
626
+ const topPad = Array(topPadCount).fill(" ".repeat(width));
627
+ const res = [...topPad, ...modalBox.map((line) => padStr + line)];
628
+ while (res.length < contentH) res.push(" ".repeat(width));
629
+ return res;
630
+ }
631
+
632
+ if (this.isApiKeyModalOpen) {
633
+ const modalW = Math.min(width - 4, 68);
634
+ const padCount = Math.max(0, Math.floor((width - modalW) / 2));
635
+ const padStr = " ".repeat(padCount);
636
+
637
+ const safeCursor = Math.max(0, Math.min(this.apiKeyInput.length, this.apiKeyCursor));
638
+ let inputDisplay: string;
639
+ if (this.apiKeyInput.length === 0) {
640
+ inputDisplay = `${theme.inverse(" ")} ${theme.dim("(vazio = sem autenticação local)")}`;
641
+ } else {
642
+ const before = this.apiKeyInput.slice(0, safeCursor);
643
+ const at = this.apiKeyInput[safeCursor] || " ";
644
+ const after = this.apiKeyInput.slice(safeCursor + 1);
645
+ inputDisplay = `${theme.peach(before)}${theme.inverse(at)}${theme.peach(after)}`;
646
+ }
647
+
648
+ const isProtected = this.apiKeyInput.trim().length > 0;
649
+ const statusBadge = isProtected
650
+ ? theme.green("Protegido (Requer Bearer Token)")
651
+ : theme.yellow("Livre (Apenas Loopback 127.0.0.1)");
652
+
653
+ const isSaveHovered = this.apiKeyModalHoveredBtn === "save";
654
+ const isCancelHovered = this.apiKeyModalHoveredBtn === "cancel";
655
+
656
+ const saveLabel = " [ Enter ] Salvar API Key ";
657
+ const cancelLabel = " [ Esc ] Cancelar ";
658
+
659
+ const saveBtn = isSaveHovered
660
+ ? theme.bgHover(theme.bold(theme.green(saveLabel)))
661
+ : theme.green(saveLabel);
662
+
663
+ const cancelBtn = isCancelHovered
664
+ ? theme.bgHover(theme.bold(theme.red(cancelLabel)))
665
+ : theme.muted(cancelLabel);
666
+
667
+ const modalContent: string[] = [
668
+ "",
669
+ ` ${theme.bold("Configurar Chave de Autenticação (API Key):")}`,
670
+ ` ${theme.dim("Digite ou cole (Ctrl+V) a chave Bearer para proteger o proxy:")}`,
671
+ ` ${theme.dim("────────────────────────────────────────────────────────────")}`,
672
+ ` API Key: [ ${inputDisplay} ]`,
673
+ ` ${theme.dim("────────────────────────────────────────────────────────────")}`,
674
+ ` Modo de Acesso: ${statusBadge}`,
675
+ ];
676
+
677
+ modalContent.push("");
678
+ const btnContentIdx = modalContent.length;
679
+ modalContent.push(
680
+ ` ${saveBtn} ${cancelBtn} ${theme.dim("(Ctrl+V colar)")}`,
681
+ "",
682
+ ` ${theme.dim("Nota: Salva a chave no arquivo .env. Clientes devem enviar 'Bearer <key>'.")}`,
683
+ );
684
+
685
+ const modalBox = drawBox({
686
+ title: "Configurar API Key",
687
+ width: modalW,
688
+ height: Math.min(contentH, modalContent.length + 2),
689
+ borderColor: theme.borderActive,
690
+ titleColor: theme.peach,
691
+ content: modalContent,
692
+ });
693
+
694
+ const topPadCount = Math.max(1, Math.floor((contentH - modalBox.length) / 3));
695
+ this.lastApiKeyModalBtnRow = 5 + topPadCount + btnContentIdx;
696
+ this.lastApiKeyModalLeftPad = padCount;
697
+
698
+ const topPad = Array(topPadCount).fill(" ".repeat(width));
699
+ const res = [...topPad, ...modalBox.map((line) => padStr + line)];
700
+ while (res.length < contentH) res.push(" ".repeat(width));
701
+ return res;
702
+ }
703
+
217
704
  // Two-column layout: give left box 48-52 cols so rich metrics never truncate,
218
705
  // and right box takes the remaining width (at least 38 cols).
219
706
  const leftW = width >= 96
@@ -312,17 +799,23 @@ export class StatusView implements TuiView {
312
799
  const zerarIdx = leftContent.length + 1;
313
800
  const modoIdx = leftContent.length + 2;
314
801
  const copiarIdx = leftContent.length + 3;
802
+ const portIdx = leftContent.length + 4;
803
+ const apiKeyIdx = leftContent.length + 5;
315
804
 
316
805
  this.lastActionRecarregarRow = 5 + recarregarIdx;
317
806
  this.lastActionZerarRow = 5 + zerarIdx;
318
807
  this.lastActionModoRow = 5 + modoIdx;
319
808
  this.lastActionCopiarRow = 5 + copiarIdx;
809
+ this.lastActionPortRow = 5 + portIdx;
810
+ this.lastActionApiKeyRow = 5 + apiKeyIdx;
320
811
 
321
812
  leftContent.push(
322
813
  ` ${this.hoveredActionRow === this.lastActionRecarregarRow ? theme.bgHover(` ${theme.cyan("[ R ] Recarregar")} `) : ` ${theme.cyan("[ R ]")} Recarregar`}`,
323
814
  ` ${this.hoveredActionRow === this.lastActionZerarRow ? theme.bgHover(` ${theme.yellow("[ Z ] Zerar Cooldowns")} `) : ` ${theme.yellow("[ Z ]")} Zerar Cooldowns`}`,
324
815
  ` ${this.hoveredActionRow === this.lastActionModoRow ? theme.bgHover(` ${theme.lavender("[ M ] Alternar Modo")} `) : ` ${theme.lavender("[ M ]")} Alternar Modo`}`,
325
816
  ` ${this.hoveredActionRow === this.lastActionCopiarRow ? theme.bgHover(` ${theme.green("[ C ] Copiar URL")} `) : ` ${theme.green("[ C ]")} Copiar URL`}`,
817
+ ` ${this.hoveredActionRow === this.lastActionPortRow ? theme.bgHover(` ${theme.cyan("[ P ] Alterar Porta")} `) : ` ${theme.cyan("[ P ]")} Alterar Porta`}`,
818
+ ` ${this.hoveredActionRow === this.lastActionApiKeyRow ? theme.bgHover(` ${theme.peach("[ K ] Alterar API Key")} `) : ` ${theme.peach("[ K ]")} Alterar API Key`}`,
326
819
  );
327
820
 
328
821
  const boxHeight = Math.max(contentH, leftContent.length + 2);
@@ -61,6 +61,13 @@ export class SyncView implements TuiView {
61
61
  } catch {}
62
62
  }
63
63
 
64
+ private appendLog(msg: string): void {
65
+ this.actionLog.push(msg);
66
+ if (this.actionLog.length > 500) {
67
+ this.actionLog.shift();
68
+ }
69
+ }
70
+
64
71
  private detectClients(): void {
65
72
  const paths = getDefaultPaths();
66
73
  const defs: Array<{ id: SyncClientName; name: string; path: string }> = [
@@ -260,7 +267,7 @@ export class SyncView implements TuiView {
260
267
  for (const c of this.clients) {
261
268
  c.selected = !allSelected;
262
269
  }
263
- this.actionLog.unshift(
270
+ this.appendLog(
264
271
  allSelected ? "Desmarcados todos os clientes." : "Selecionados todos os clientes.",
265
272
  );
266
273
  return true;
@@ -289,11 +296,11 @@ export class SyncView implements TuiView {
289
296
  .map((c) => c.id);
290
297
 
291
298
  if (selectedTargets.length === 0) {
292
- this.actionLog.unshift(theme.yellow("⚠ Nenhum cliente selecionado para sincronizar."));
299
+ this.appendLog(theme.yellow("⚠ Nenhum cliente selecionado para sincronizar."));
293
300
  return;
294
301
  }
295
302
  const currentModel = this.availableModels[this.modelIndex] || "qwen3.8-max";
296
- this.actionLog.unshift(
303
+ this.appendLog(
297
304
  theme.cyan(`⏳ Sincronizando [${selectedTargets.join(", ")}] com modelo ${currentModel}...`),
298
305
  );
299
306
  try {
@@ -308,21 +315,24 @@ export class SyncView implements TuiView {
308
315
  for (const [key, clientRes] of Object.entries(res.clients)) {
309
316
  if (clientRes && clientRes.success) {
310
317
  successCount++;
311
- this.actionLog.unshift(
318
+ this.appendLog(
312
319
  theme.green(`✓ [${key}] ${clientRes.message || "Configurado com sucesso"}`),
313
320
  );
314
321
  } else if (clientRes) {
315
- this.actionLog.unshift(
322
+ this.appendLog(
316
323
  theme.red(`✗ [${key}] Falha: ${clientRes.error || "Erro desconhecido"}`),
317
324
  );
318
325
  }
319
326
  }
320
327
 
321
- this.actionLog.unshift(
328
+ this.appendLog(
322
329
  theme.green(`🎉 Concluído: ${successCount} cliente(s) sincronizado(s) com zero perdas!`),
323
330
  );
324
331
  this.detectClients();
325
332
  } catch (err: any) {
333
+ this.appendLog(
334
+ theme.red(`✗ Erro na sincronização: ${err?.message || String(err)}`),
335
+ );
326
336
  }
327
337
  }
328
338
 
@@ -332,17 +342,17 @@ export class SyncView implements TuiView {
332
342
  .map((c) => c.id);
333
343
 
334
344
  const targetDesc = selectedTargets.length > 0 ? `[${selectedTargets.join(", ")}]` : "todos os clientes";
335
- this.actionLog.unshift(theme.yellow(`⏳ Restaurando backups anteriores de ${targetDesc}...`));
345
+ this.appendLog(theme.yellow(`⏳ Restaurando backups anteriores de ${targetDesc}...`));
336
346
  try {
337
347
  const res = restoreAllClients({
338
348
  targets: selectedTargets.length > 0 ? selectedTargets : undefined,
339
349
  });
340
- this.actionLog.unshift(
350
+ this.appendLog(
341
351
  theme.green(`✓ Rollback concluído: ${res.restoredCount} arquivo(s) restaurados com sucesso.`),
342
352
  );
343
353
  this.detectClients();
344
354
  } catch (err: any) {
345
- this.actionLog.unshift(theme.red(`✗ Erro ao restaurar backups: ${err?.message || String(err)}`));
355
+ this.appendLog(theme.red(`✗ Erro ao restaurar backups: ${err?.message || String(err)}`));
346
356
  }
347
357
  }
348
358
 
@@ -446,7 +456,9 @@ export class SyncView implements TuiView {
446
456
  theme.muted(" Backups (.bak) são criados automaticamente antes de cada alteração."),
447
457
  );
448
458
  } else {
449
- for (const log of this.actionLog.slice(0, contentH - 5)) {
459
+ const maxLogs = Math.max(5, contentH - 5);
460
+ const visibleLogs = this.actionLog.slice(-maxLogs);
461
+ for (const log of visibleLogs) {
450
462
  rightContent.push(` ${log}`);
451
463
  }
452
464
  }